events: whole-series booking + 9 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 59s

ev/book-series! / ev/cancel-series! apply a booking/cancel to every occurrence
of one event in a window (RSVP the whole weekly class), returning per-
occurrence (occ-key status) results; capacity still enforced per occurrence
(some :booked, some :full), idempotent re-book (:already). ev/series-count,
ev/series-booked. 341/341 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 15:20:27 +00:00
parent ddc6635fa8
commit 94aaf0e433
5 changed files with 126 additions and 7 deletions

View File

@@ -275,3 +275,55 @@
((ev/would-time-conflict? b store actor occ)
{:status :time-conflict :actor actor :occ-key (ev-occ-key occ)})
(else (ev/book-occ! b store actor occ)))))
;; ---- whole-series operations ----
;; Apply a booking action to every occurrence of one event in [ws, we) — e.g.
;; "RSVP to the whole weekly class". Returns a list of (occ-key status) results,
;; one per occurrence (empty if the event id is unknown).
(define
ev/book-series!
(fn
(b store actor event-id ws we)
(let
((ev (ev/event-by-id store event-id)))
(if
(nil? ev)
(list)
(map
(fn (occ) (list (ev-occ-key occ) (get (ev/book-occ! b store actor occ) :status)))
(ev-expand ev ws we))))))
;; Cancel `actor` from every occurrence of one event in [ws, we).
(define
ev/cancel-series!
(fn
(b store actor event-id ws we)
(let
((ev (ev/event-by-id store event-id)))
(if
(nil? ev)
(list)
(map
(fn (occ) (list (ev-occ-key occ) (get (ev/cancel! b (ev-occ-key occ) actor) :status)))
(ev-expand ev ws we))))))
;; How many statuses in a series-result list equal `status`.
(define
ev/series-count
(fn
(results status)
(len (filter (fn (r) (= (first (rest r)) status)) results))))
;; The occurrences of one event in [ws, we) that `actor` is booked into.
(define
ev/series-booked
(fn
(b store actor event-id ws we)
(let
((ev (ev/event-by-id store event-id)))
(if
(nil? ev)
(list)
(filter
(fn (occ) (ev-actor-booked? b (ev-occ-key occ) actor))
(ev-expand ev ws we))))))

View File

@@ -1,14 +1,14 @@
{
"lang": "events",
"total_passed": 332,
"total_passed": 341,
"total_failed": 0,
"total": 332,
"total": 341,
"suites": [
{"name":"calendar","passed":51,"failed":0,"total":51},
{"name":"timezone","passed":17,"failed":0,"total":17},
{"name":"ical","passed":21,"failed":0,"total":21},
{"name":"availability","passed":22,"failed":0,"total":22},
{"name":"api","passed":32,"failed":0,"total":32},
{"name":"api","passed":41,"failed":0,"total":41},
{"name":"booking","passed":82,"failed":0,"total":82},
{"name":"booking-notify","passed":11,"failed":0,"total":11},
{"name":"ticket","passed":31,"failed":0,"total":31},
@@ -17,5 +17,5 @@
{"name":"federation","passed":29,"failed":0,"total":29},
{"name":"integration","passed":8,"failed":0,"total":8}
],
"generated": "2026-06-07T14:40:54+00:00"
"generated": "2026-06-07T15:20:08+00:00"
}

View File

@@ -1,6 +1,6 @@
# events scoreboard
**332 / 332 passing** (0 failure(s)).
**341 / 341 passing** (0 failure(s)).
| Suite | Passed | Total | Status |
|-------|--------|-------|--------|
@@ -8,7 +8,7 @@
| timezone | 17 | 17 | ok |
| ical | 21 | 21 | ok |
| availability | 22 | 22 | ok |
| api | 32 | 32 | ok |
| api | 41 | 41 | ok |
| booking | 82 | 82 | ok |
| booking-notify | 11 | 11 | ok |
| ticket | 31 | 31 | ok |

View File

@@ -319,6 +319,65 @@
(ev/would-time-conflict? b store (quote zed) ob)
false))))))
;; ---- whole-series booking ----
(define
ev-api-sr-run-all!
(fn
()
(let
((b (persist/open))
(store
(ev/schedule
(ev/empty)
(quote yoga)
(ev-dt 2026 6 1 18 0)
60
{:freq :weekly :byday (list 0 2) :count 4}
20))
(ws (ev-date 2026 6 1))
(we (ev-date 2026 7 1)))
(do
(let
((res (ev/book-series! b store (quote nia) (quote yoga) ws we)))
(do
(ev-api-check! "series booking covers all four occurrences" (len res) 4)
(ev-api-check! "all occurrences booked" (ev/series-count res :booked) 4)
(ev-api-check!
"actor is now booked into the whole series"
(len (ev/series-booked b store (quote nia) (quote yoga) ws we))
4)))
;; re-booking the series is idempotent
(ev-api-check!
"re-booking the series is idempotent"
(ev/series-count (ev/book-series! b store (quote nia) (quote yoga) ws we) :already)
4)
;; cancel the whole series
(let
((res (ev/cancel-series! b store (quote nia) (quote yoga) ws we)))
(do
(ev-api-check! "series cancel reports four cancellations" (ev/series-count res :cancelled) 4)
(ev-api-check!
"actor booked into nothing after series cancel"
(len (ev/series-booked b store (quote nia) (quote yoga) ws we))
0)))
;; capacity interacts per-occurrence: fill one occurrence first
(let
((b2 (persist/open))
(s2
(ev/schedule (ev/empty) (quote clinic) (ev-dt 2026 6 1 9 0) 30 {:freq :daily :count 3} 1)))
(do
(ev/book-occ! b2 s2 (quote x) (ev-occ (quote clinic) (ev-dt 2026 6 2 9 0) 30))
(let
((res (ev/book-series! b2 s2 (quote nia) (quote clinic) (ev-date 2026 6 1) (ev-date 2026 6 10))))
(do
(ev-api-check! "series booking succeeds on free occurrences" (ev/series-count res :booked) 2)
(ev-api-check! "series booking hits :full where capacity is taken" (ev/series-count res :full) 1)))))
;; unknown event id
(ev-api-check!
"series booking an unknown event yields no results"
(ev/book-series! b store (quote nia) (quote nope) ws we)
(list))))))
(define
ev-api-tests-run!
(fn
@@ -329,4 +388,5 @@
(set! ev-api-failures (list))
(ev-api-run-all!)
(ev-api-cf-run-all!)
(ev-api-sr-run-all!)
{:failures ev-api-failures :total (+ ev-api-pass ev-api-fail) :passed ev-api-pass :failed ev-api-fail})))

View File

@@ -18,7 +18,7 @@ capacity rules, transactional booking, and a flow-driven notification dispatcher
## Status (rolling)
`bash lib/events/conformance.sh`**332/332** (Phases 1-4 + 11 ext: …e2e delivery, conflict-checked booking, iCalendar export)
`bash lib/events/conformance.sh`**341/341** (Phases 1-4 + 12 ext: …conflict-checked booking, iCalendar export, whole-series booking)
## Ground rules
@@ -88,6 +88,13 @@ lib/events/api.sx ── (events/schedule) (events/book) (events/agenda) ──
## Progress log
- 2026-06-07 — Whole-series booking (extension). `ev/book-series!` /
`ev/cancel-series!` apply a booking/cancel to every occurrence of one event
in a window (e.g. RSVP the whole weekly class), returning per-occurrence
(occ-key status) results; capacity is still enforced per occurrence (some
:booked, some :full). Idempotent re-book (all :already). `ev/series-count`
(tally a status), `ev/series-booked` (which occurrences the actor holds).
+9 tests, 341/341 green. This was the last flagged feature — surface saturated.
- 2026-06-07 — iCalendar (RFC 5545) export (extension). `ical.sx` serializes
events to VEVENT / VCALENDAR text for import by standard clients. UTC
basic-format stamps (YYYYMMDDTHHMM00Z), DURATION (PT#H#M), and the full RRULE