events: reschedule notifications + 7 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 31s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 31s
ev/reschedule-notifications: when an event carries per-occurrence overrides, reads the roster at each overridden occurrence's original occ-key and emits a reschedule message per booked attendee (old-start/new-start/new-duration). Idempotency key = original-key/reschedule/new-start. 272/272 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,3 +94,54 @@
|
|||||||
;; A single digest message summarising an actor's upcoming booked occurrences.
|
;; A single digest message summarising an actor's upcoming booked occurrences.
|
||||||
;; :items is ({:event :start} ...); empty when the actor has nothing booked.
|
;; :items is ({:event :start} ...); empty when the actor has nothing booked.
|
||||||
(define ev/agenda-digest (fn (b store actor ws we) {:items (map (fn (occ) {:event (get occ :id) :start (get occ :start)}) (ev/agenda-for-p b store actor ws we)) :id (str actor "/digest/" ws "-" we) :recipient actor}))
|
(define ev/agenda-digest (fn (b store actor ws we) {:items (map (fn (occ) {:event (get occ :id) :start (get occ :start)}) (ev/agenda-for-p b store actor ws we)) :id (str actor "/digest/" ws "-" we) :recipient actor}))
|
||||||
|
|
||||||
|
;; ---- reschedule notifications ----
|
||||||
|
;; When an event carries per-occurrence overrides (ev-with-override), every
|
||||||
|
;; attendee booked at the ORIGINAL start should be told the new time. Bookings
|
||||||
|
;; were made against the original occ-key (id@orig-start), so we read that
|
||||||
|
;; roster. Idempotency key encodes the original key and the new start, so
|
||||||
|
;; re-deriving the same reschedule never double-notifies.
|
||||||
|
(define
|
||||||
|
ev/reschedule-notifications
|
||||||
|
(fn
|
||||||
|
(b event)
|
||||||
|
(let
|
||||||
|
((overrides (ev-or (get event :overrides) (list)))
|
||||||
|
(evid (get event :id))
|
||||||
|
(dur (get event :duration)))
|
||||||
|
(reduce
|
||||||
|
(fn
|
||||||
|
(acc entry)
|
||||||
|
(let
|
||||||
|
((orig-start (first entry))
|
||||||
|
(ov (first (rest entry))))
|
||||||
|
(let
|
||||||
|
((occ (ev-occ evid orig-start dur))
|
||||||
|
(new-start (get ov :start))
|
||||||
|
(new-duration (get ov :duration)))
|
||||||
|
(let
|
||||||
|
((key (ev-occ-key occ)))
|
||||||
|
(append
|
||||||
|
acc
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(actor)
|
||||||
|
{:id (str key "/reschedule/" new-start)
|
||||||
|
:recipient actor
|
||||||
|
:event evid
|
||||||
|
:old-start orig-start
|
||||||
|
:new-start new-start
|
||||||
|
:new-duration new-duration})
|
||||||
|
(ev/roster-occ b occ)))))))
|
||||||
|
(list)
|
||||||
|
overrides))))
|
||||||
|
|
||||||
|
;; Project a reschedule notification to notify's (id recipient body) shape.
|
||||||
|
(define
|
||||||
|
ev/reschedule-notify->msg
|
||||||
|
(fn
|
||||||
|
(r)
|
||||||
|
(list
|
||||||
|
(get r :id)
|
||||||
|
(get r :recipient)
|
||||||
|
(list :rescheduled (get r :event) (get r :old-start) (get r :new-start)))))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"lang": "events",
|
"lang": "events",
|
||||||
"total_passed": 265,
|
"total_passed": 272,
|
||||||
"total_failed": 0,
|
"total_failed": 0,
|
||||||
"total": 265,
|
"total": 272,
|
||||||
"suites": [
|
"suites": [
|
||||||
{"name":"calendar","passed":51,"failed":0,"total":51},
|
{"name":"calendar","passed":51,"failed":0,"total":51},
|
||||||
{"name":"availability","passed":22,"failed":0,"total":22},
|
{"name":"availability","passed":22,"failed":0,"total":22},
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
{"name":"booking-notify","passed":11,"failed":0,"total":11},
|
{"name":"booking-notify","passed":11,"failed":0,"total":11},
|
||||||
{"name":"ticket","passed":31,"failed":0,"total":31},
|
{"name":"ticket","passed":31,"failed":0,"total":31},
|
||||||
{"name":"notify","passed":7,"failed":0,"total":7},
|
{"name":"notify","passed":7,"failed":0,"total":7},
|
||||||
{"name":"reminders","passed":14,"failed":0,"total":14},
|
{"name":"reminders","passed":21,"failed":0,"total":21},
|
||||||
{"name":"federation","passed":23,"failed":0,"total":23}
|
{"name":"federation","passed":23,"failed":0,"total":23}
|
||||||
],
|
],
|
||||||
"generated": "2026-06-07T07:20:13+00:00"
|
"generated": "2026-06-07T07:46:42+00:00"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# events scoreboard
|
# events scoreboard
|
||||||
|
|
||||||
**265 / 265 passing** (0 failure(s)).
|
**272 / 272 passing** (0 failure(s)).
|
||||||
|
|
||||||
| Suite | Passed | Total | Status |
|
| Suite | Passed | Total | Status |
|
||||||
|-------|--------|-------|--------|
|
|-------|--------|-------|--------|
|
||||||
@@ -11,5 +11,5 @@
|
|||||||
| booking-notify | 11 | 11 | ok |
|
| booking-notify | 11 | 11 | ok |
|
||||||
| ticket | 31 | 31 | ok |
|
| ticket | 31 | 31 | ok |
|
||||||
| notify | 7 | 7 | ok |
|
| notify | 7 | 7 | ok |
|
||||||
| reminders | 14 | 14 | ok |
|
| reminders | 21 | 21 | ok |
|
||||||
| federation | 23 | 23 | ok |
|
| federation | 23 | 23 | ok |
|
||||||
|
|||||||
@@ -208,6 +208,61 @@
|
|||||||
(get empty-dig :items)
|
(get empty-dig :items)
|
||||||
(list)))))))))
|
(list)))))))))
|
||||||
|
|
||||||
|
;; ---- reschedule notifications ----
|
||||||
|
(define
|
||||||
|
ev-rm-rs-run-all!
|
||||||
|
(fn
|
||||||
|
()
|
||||||
|
(let
|
||||||
|
((b (persist/open))
|
||||||
|
(ev (ev-event (quote yoga) (ev-dt 2026 6 1 9 0) 60 {:freq :daily :count 3} 20)))
|
||||||
|
(let
|
||||||
|
((occ2 (ev-occ (quote yoga) (ev-dt 2026 6 2 9 0) 60)))
|
||||||
|
(do
|
||||||
|
(ev/book-occ! b (ev/add-event (ev/empty) ev) (quote nia) occ2)
|
||||||
|
(ev/book-occ! b (ev/add-event (ev/empty) ev) (quote ola) occ2)
|
||||||
|
;; reschedule the Jun 2 occurrence to 14:00 / 90 min
|
||||||
|
(let
|
||||||
|
((moved (ev-with-override ev (ev-dt 2026 6 2 9 0) (ev-dt 2026 6 2 14 0) 90)))
|
||||||
|
(let
|
||||||
|
((ns (ev/reschedule-notifications b moved)))
|
||||||
|
(do
|
||||||
|
(ev-rm-check!
|
||||||
|
"every booked attendee is notified of the reschedule"
|
||||||
|
(map (fn (n) (get n :recipient)) ns)
|
||||||
|
(list (quote nia) (quote ola)))
|
||||||
|
(ev-rm-check!
|
||||||
|
"reschedule carries old and new start"
|
||||||
|
(list (get (first ns) :old-start) (get (first ns) :new-start))
|
||||||
|
(list (ev-dt 2026 6 2 9 0) (ev-dt 2026 6 2 14 0)))
|
||||||
|
(ev-rm-check!
|
||||||
|
"reschedule carries the new duration"
|
||||||
|
(get (first ns) :new-duration)
|
||||||
|
90)
|
||||||
|
(ev-rm-check!
|
||||||
|
"reschedule idempotency key encodes original key + new start"
|
||||||
|
(get (first ns) :id)
|
||||||
|
(str (ev-occ-key occ2) "/reschedule/" (ev-dt 2026 6 2 14 0)))
|
||||||
|
(ev-rm-check!
|
||||||
|
"reschedule projects to notify wire shape"
|
||||||
|
(ev/reschedule-notify->msg (first ns))
|
||||||
|
(list
|
||||||
|
(str (ev-occ-key occ2) "/reschedule/" (ev-dt 2026 6 2 14 0))
|
||||||
|
(quote nia)
|
||||||
|
(list :rescheduled (quote yoga) (ev-dt 2026 6 2 9 0) (ev-dt 2026 6 2 14 0)))))))
|
||||||
|
;; an override on an occurrence nobody booked notifies no one
|
||||||
|
(let
|
||||||
|
((moved2 (ev-with-override ev (ev-dt 2026 6 3 9 0) (ev-dt 2026 6 3 10 0) 60)))
|
||||||
|
(ev-rm-check!
|
||||||
|
"rescheduling an unbooked occurrence notifies no one"
|
||||||
|
(len (ev/reschedule-notifications b moved2))
|
||||||
|
0))
|
||||||
|
;; an event with no overrides yields no reschedule notifications
|
||||||
|
(ev-rm-check!
|
||||||
|
"event without overrides has no reschedule notifications"
|
||||||
|
(len (ev/reschedule-notifications b ev))
|
||||||
|
0))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
ev-reminders-tests-run!
|
ev-reminders-tests-run!
|
||||||
(fn
|
(fn
|
||||||
@@ -217,4 +272,5 @@
|
|||||||
(set! ev-rm-fail 0)
|
(set! ev-rm-fail 0)
|
||||||
(set! ev-rm-failures (list))
|
(set! ev-rm-failures (list))
|
||||||
(ev-rm-run-all!)
|
(ev-rm-run-all!)
|
||||||
|
(ev-rm-rs-run-all!)
|
||||||
{:failures ev-rm-failures :total (+ ev-rm-pass ev-rm-fail) :passed ev-rm-pass :failed ev-rm-fail})))
|
{:failures ev-rm-failures :total (+ ev-rm-pass ev-rm-fail) :passed ev-rm-pass :failed ev-rm-fail})))
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ capacity rules, transactional booking, and a flow-driven notification dispatcher
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/events/conformance.sh` → **265/265** (Phases 1-4 + ext: fed free/busy, waitlist, EXDATE/RDATE, overrides, booking-notify)
|
`bash lib/events/conformance.sh` → **272/272** (Phases 1-4 + 6 ext: fed f/b, waitlist, EXDATE/RDATE, overrides, booking-notify, reschedule-notify)
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
@@ -86,6 +86,13 @@ lib/events/api.sx ── (events/schedule) (events/book) (events/agenda) ──
|
|||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
|
||||||
|
- 2026-06-07 — Reschedule notifications (extension). When an event carries
|
||||||
|
per-occurrence overrides, `ev/reschedule-notifications` reads the roster at
|
||||||
|
each overridden occurrence's ORIGINAL occ-key and produces a reschedule
|
||||||
|
message per booked attendee (old-start, new-start, new-duration). Idempotency
|
||||||
|
key = original-key/reschedule/new-start. `ev/reschedule-notify->msg` for the
|
||||||
|
notify wire shape. Combines overrides (calendar) + rosters (booking) + the
|
||||||
|
message-derivation pattern. +7 tests, 272/272 green.
|
||||||
- 2026-06-07 — Booking lifecycle notifications (extension). `booking-notify.sx`
|
- 2026-06-07 — Booking lifecycle notifications (extension). `booking-notify.sx`
|
||||||
walks the booking stream into ordered notifications classified by kind:
|
walks the booking stream into ordered notifications classified by kind:
|
||||||
:booked / :promoted / :held / :confirmed / :released / :cancelled /
|
:booked / :promoted / :held / :confirmed / :released / :cancelled /
|
||||||
|
|||||||
Reference in New Issue
Block a user