@go-labs-sg/rsvp 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +269 -12
  2. package/dist/index.js +5930 -179
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -85,6 +85,48 @@ The installed command uses the same portable Bun launcher as the other Go Labs C
85
85
 
86
86
  Global administrators can update another account's global role from the web user manager or with `rsvp user role update --allow-state-change --target <user-id> --payload '{"userId":"<user-id>","role":"admin"}'`. Administrators cannot remove their own global admin role.
87
87
 
88
+ ## Personal seating drafts
89
+
90
+ `event seating draft get`, `save`, `preview`, and `publish` use the same server-owned
91
+ personal draft as Assign Guests. Resume it on another authenticated device; drafts
92
+ are isolated by event and signed-in owner. Do not pass an owner ID. Stored changes
93
+ contain only `registrationAttendeeId`, `fromSeatId` (the first live source, nullable)
94
+ and `toSeatId` (nullable to unseat). Stage does not book seats, acquire holds or send
95
+ email. Preview reports fresh conflicts and grant-scoped guest details.
96
+
97
+ Guest reads are live by default. Add `--include-draft` (or JSON `includeDraft: true`)
98
+ to `event seating guests list` or `count` to apply only your saved draft before
99
+ seating filters, facets and pagination. `--include-draft=false` keeps live results.
100
+ Draft-mode cursors bind the saved version; restart pagination after a draft save.
101
+ The Assign guests page always reads this personal-draft view.
102
+
103
+ ```sh
104
+ rsvp event seating draft get --organization-id org_123 --event-id evt_123 --ack-sensitive-data
105
+ rsvp event seating draft save --allow-state-change --target evt_123 \
106
+ --payload '{"organizationId":"org_123","eventId":"evt_123","expectedVersion":0,"changes":[{"registrationAttendeeId":"att_123","fromSeatId":null,"toSeatId":"seat_123"}]}'
107
+ rsvp event seating draft preview --organization-id org_123 --event-id evt_123 \
108
+ --expected-version 1 --ack-sensitive-data
109
+ rsvp event seating draft publish --organization-id org_123 --event-id evt_123 \
110
+ --expected-version 1 --request-id 123e4567-e89b-42d3-a456-426614174000 \
111
+ --reason 'Seat the confirmed guest' --allow-state-change --target evt_123
112
+ ```
113
+
114
+ Save and Publish require the latest `expectedVersion`. A successful Publish applies
115
+ all changed guests atomically and clears only that version of your draft. Resolve
116
+ preview issues before publishing; conflicts leave all bookings and the draft intact.
117
+ For an unknown outcome, retry the same UUID `requestId`, version, reason and email
118
+ choice: the durable receipt returns the previous counts without sending again or
119
+ clearing a newer draft. A different intent needs a new request ID. Email defaults
120
+ off. To opt in, use `--allow-email` and a full JSON payload including organizationId,
121
+ eventId, expectedVersion, requestId, reason and `sendSeatChangeEmail: true`. Do not
122
+ combine `--payload` with command-local version/reason/request flags;
123
+ the server additionally requires notice permission and enabled recipient policy.
124
+ Preview counts recipient registrations, not guests. On realtime-enabled servers,
125
+ free new targets require validated per-registration holds at Publish, supplied as
126
+ `seatHolds: [{registrationId, holds: [{seatMapId, clientId, holdToken}]}]` in the
127
+ payload. Acquire these through the existing organizer hold-token flow; Stage never
128
+ holds seats. Swaps and cycles of changed live sources do not require free-seat holds.
129
+
88
130
  ## Command safety
89
131
 
90
132
  Argument-parsing errors use a safe command label and never echo option values,
@@ -125,6 +167,37 @@ key to clear the image. The public event guide reuses both assets. Campaign
125
167
  HTML can link to the guide with `{{eventDetailsUrl}}`; `{{eventUrl}}` still
126
168
  points to the registration page.
127
169
 
170
+ The optional event footer is one image shown at the bottom of the registration
171
+ and confirmation pages; events without one show no footer. Use
172
+ `event footer upload create` for the upload URL and key, upload the image
173
+ (PNG, JPG, WebP, or GIF up to 10 MB; SVG is not accepted), then save it with
174
+ `event footer update`. `variant` defaults to `DESKTOP`; send `MOBILE` to set a
175
+ separate image for screens under 640px. When only one variant is set it is used
176
+ on every screen size. A null `footerImageKey` clears that variant.
177
+
178
+ ```sh
179
+ rsvp event footer update \
180
+ --allow-state-change \
181
+ --target evt_123 \
182
+ --payload '{"organizationId":"org_123","eventId":"evt_123","footerImageKey":"rsvp/events/evt_123/footers/<uuid>.png","variant":"MOBILE","version":7}'
183
+ ```
184
+
185
+ Instead of one image, the footer can show separate logos scrolling across the
186
+ page. Upload each logo with `event footer upload create`, then save the mode
187
+ and the ordered list with `event footer logos update`. Each logo needs a
188
+ `name` (also its alt text); `heading` (for example "Supported by"), a smaller
189
+ `label` (for example "Principal partners") and an `https://` `url` are optional.
190
+ Adjacent logos with the same heading appear together under it. Up to 30 logos.
191
+ `footerMode` `IMAGE` switches back to the single image; both modes keep their
192
+ uploads.
193
+
194
+ ```sh
195
+ rsvp event footer logos update \
196
+ --allow-state-change \
197
+ --target evt_123 \
198
+ --payload '{"organizationId":"org_123","eventId":"evt_123","footerMode":"LOGOS","logos":[{"imageKey":"rsvp/events/evt_123/footers/<uuid>.png","name":"FARM","heading":"Produced by","url":"https://www.farm.sg"}],"version":8}'
199
+ ```
200
+
128
201
  For a read-only registration-journey diagnosis, inspect the event, draft,
129
202
  published versions, and event-owned resources together:
130
203
 
@@ -402,6 +475,23 @@ metadata. It does not choose eligible people, apply category/exclusion rules,
402
475
  import a draw roster, or reconcile prizes/vouchers. Deploy the updated API before
403
476
  saving the new setup section; no database schema migration is required.
404
477
 
478
+ ### Registration card copy
479
+
480
+ `section: "PUBLIC_PAGE"` replaces the event's saved landing-page card copy.
481
+ Omit a text key to use the default wording; set a `show…` flag to `false` to
482
+ hide that notice. Read the event first and use its current version:
483
+
484
+ ```sh
485
+ rsvp event setup step update \
486
+ --allow-state-change \
487
+ --target evt_123 \
488
+ --payload '{"organizationId":"org_123","eventId":"evt_123","version":7,"section":"PUBLIC_PAGE","publicPage":{"registrationTitle":"NELBD Quest Registration","deadlineLabel":"Register by","responseUpdateNotice":"You may return to update your registration details before the timing stated above.","accessNotice":"Continue to complete the registration for both team members.","registrationActionLabel":"Register now"}}'
489
+ ```
490
+
491
+ Limits: title 120, description 500, deadline label 60, response-update notice
492
+ 300, access notice 500 and button label 100 characters. It requires
493
+ `event.manage`; no database migration is required.
494
+
405
495
  ## Per-form confirmation presentation
406
496
 
407
497
  In the form builder's **Flow settings → Confirmation and check-in pass**, set
@@ -445,21 +535,37 @@ No database schema migration or attendee backfill is required.
445
535
 
446
536
  ## Administrator registration for an invitee
447
537
 
448
- Inspect the invitee's active registration form and current registration version
449
- before submitting. The context includes the published flow definition, invitee,
450
- and existing registration (if any):
538
+ Organizers can register someone on their behalf at any time, including after
539
+ registration has closed. This is the same action as the **Register invitee**
540
+ page under People. Look the person up first. Pass `inviteeId` for someone on
541
+ the invitee list, or `person` with their details. The response includes the
542
+ published flow definition, the invitee, `isExistingInvitee`, any existing
543
+ registration, `archivedRegistrationId`, `canAddInvitee` and
544
+ `canOverrideCapacity`:
451
545
 
452
546
  ```sh
453
547
  rsvp event flow admin registration context get \
454
548
  --ack-sensitive-data \
455
549
  --payload '{"organizationId":"org_123","eventId":"evt_123","inviteeId":"invitee_123"}'
550
+
551
+ rsvp event flow admin registration context get \
552
+ --ack-sensitive-data \
553
+ --payload '{"organizationId":"org_123","eventId":"evt_123","person":{"email":"ava@example.com","employeeId":"E1042","audienceTierId":null}}'
456
554
  ```
457
555
 
556
+ A `person` lookup matches an invitee with the same email (on the invitee or
557
+ their account) or the same employee ID. If the email and employee ID belong to
558
+ two different invitees, the lookup is refused. When nothing matches,
559
+ `isExistingInvitee` is `false`, and the context previews the form and seats
560
+ that a new invitee in the given audience tier would get. If
561
+ `archivedRegistrationId` is set, restore that registration instead (`people
562
+ registration restore`), which keeps the existing QR code.
563
+
458
564
  Use the returned `flowVersionId` and `expectedRegistrationVersion` in the
459
- submission. Set `expectedOutcomeId` to the outcome selected by the answers in
460
- the published definition, and provide answers keyed by each field's `outputKey`.
461
- A new UUID `idempotencyKey` identifies this submission. The audit reason records
462
- why an administrator submitted on the invitee's behalf.
565
+ submission. Set `expectedOutcomeId` to the outcome selected by the answers, and
566
+ key the answers by each field's `outputKey`. A new UUID `idempotencyKey`
567
+ identifies the submission. The audit reason records why an organizer submitted
568
+ on the person's behalf. Give exactly one of `inviteeId` or `newInvitee`:
463
569
 
464
570
  ```sh
465
571
  rsvp event flow admin registration submit \
@@ -467,14 +573,42 @@ rsvp event flow admin registration submit \
467
573
  --allow-state-change \
468
574
  --target invitee_123 \
469
575
  --payload '{"organizationId":"org_123","eventId":"evt_123","inviteeId":"invitee_123","flowVersionId":"flow_version_123","answers":{"attendance":"ATTENDING"},"expectedOutcomeId":"attending","expectedRegistrationVersion":0,"idempotencyKey":"9dd0ba39-b55e-4e65-aa3e-d08242c45b8f","auditReason":"Recorded response by phone"}'
576
+
577
+ rsvp event flow admin registration submit \
578
+ --ack-sensitive-data \
579
+ --allow-state-change \
580
+ --target ava@example.com \
581
+ --payload '{"organizationId":"org_123","eventId":"evt_123","newInvitee":{"displayName":"Ava Tan","email":"ava@example.com","employeeId":"E1042","audienceTierId":null},"flowVersionId":"flow_version_123","answers":{"attendance":"ATTENDING"},"expectedOutcomeId":"attending","expectedRegistrationVersion":0,"idempotencyKey":"3f0c5a4e-2a43-4d8b-9a59-6d0e8f5b8a11","auditReason":"Late addition approved by the client","sendConfirmation":true}'
470
582
  ```
471
583
 
584
+ With `newInvitee`, the target is the email. Matching uses the same identity
585
+ rules as roster import and admission: email case and Gmail dot/`+alias`
586
+ spellings are ignored, and so is employee ID case. If an invitee already matches
587
+ the details, that invitee is registered. Otherwise a new invitee is added to the
588
+ list and registered in the same transaction. Adding someone new needs the
589
+ `roster.manage` permission. Invitees who declined can be registered; anyone
590
+ with an active registration is changed through `people registration update`
591
+ instead, and archived registrations go through restore.
592
+
593
+ Optional fields:
594
+
595
+ - `sendConfirmation` (default `true`): set it to `false` to skip the
596
+ participant confirmation email with the QR code.
597
+ The result's `confirmationQueued` says whether the email was actually queued:
598
+ it is `false` when the form's email settings send no confirmation for this
599
+ registration, even if `sendConfirmation` is `true`.
600
+ - `overCapacityReason` (10–500 characters): registers the person even when the
601
+ event is full. This needs the `capacity.override` permission (event and
602
+ organization admins). The reason is recorded in the activity log. Without it,
603
+ a full event returns a conflict with `eventAtCapacity: true` in the error
604
+ data. Menu (dish) capacity is still enforced.
605
+ - `consentOverrideReason`: required when the flow has a consent step. It
606
+ records the organizer's reason in the audit trail; it does not record
607
+ attendee consent or a signature.
608
+
472
609
  Replace the example answer key and value with those required by the returned
473
- definition. If the flow requires a consent override, include a nonempty
474
- `consentOverrideReason` in the submission payload. The override records the
475
- administrator's reason in the audit trail; it does not record attendee consent
476
- or a signature. A stale registration version or flow version is rejected; fetch
477
- the context again before retrying with a new idempotency key.
610
+ definition. A stale registration version or flow version is rejected; fetch the
611
+ context again before retrying with a new idempotency key.
478
612
 
479
613
  Create an immutable menu version with an explicit mutation acknowledgement. The
480
614
  target is the event ID in the payload:
@@ -584,15 +718,38 @@ Deploy the matching API and worker before using these commands from an updated
584
718
  CLI. They retain browser authentication, event permissions and sensitive-field
585
719
  grants; the CLI does not implement separate seating rules.
586
720
 
721
+ `event seating swap` refuses invalid swaps using first/second selected-attendee
722
+ labels, never attendee names. This applies even when the caller can view identity
723
+ fields: reseating permission alone is not identity access. Notice failures retain
724
+ their API error code but do not forward attendee names or nested private details;
725
+ the entire swap rolls back if its requested notices cannot be queued. A notice
726
+ precondition refusal can be retried without the optional notice. Input/output
727
+ schemas, acknowledgement gates and seating/payment rules are unchanged.
728
+
587
729
  ```sh
588
730
  rsvp event seating live get --ack-sensitive-data \
589
731
  --payload '{"organizationId":"org_123","eventId":"evt_123"}'
732
+ rsvp event seating guests list --ack-sensitive-data \
733
+ --payload '{"organizationId":"org_123","eventId":"evt_123","seatMapId":"map_123","seating":"UNSEATED","sort":"GROUP","limit":25}'
734
+ rsvp event seating guests count --ack-sensitive-data \
735
+ --payload '{"organizationId":"org_123","eventId":"evt_123","seatMapId":"map_123","seating":"UNSEATED"}'
590
736
  rsvp event seating occupants list --ack-sensitive-data \
591
737
  --payload '{"organizationId":"org_123","eventId":"evt_123","seatMapId":"map_123","categoryValue":"finance","limit":25,"cursor":0}'
592
738
  rsvp people registration edit get --ack-sensitive-data \
593
739
  --payload '{"organizationId":"org_123","eventId":"evt_123","registrationId":"reg_123"}'
594
740
  ```
595
741
 
742
+ `event seating category colors get` returns only the configured category
743
+ colour of each booked seat (for example job function) and the legend for one
744
+ seat map, which is what the live map paints without opening the occupant
745
+ workspace. It needs `seating.view` and registration view access, and respects
746
+ sensitive-field access to the configured field:
747
+
748
+ ```sh
749
+ rsvp event seating category colors get --ack-sensitive-data \
750
+ --payload '{"organizationId":"org_123","eventId":"evt_123","seatMapId":"map_123"}'
751
+ ```
752
+
596
753
  Occupants supports optional `tableId`, `department`, `categoryValue` and `query`
597
754
  filters. `categoryValue` is the canonical (trimmed, case-normalized) value
598
755
  returned by the occupant response's category legend; it is not an arbitrary
@@ -648,12 +805,88 @@ notice is queued in the move's database transaction, so the move and notice
648
805
  request commit together or not at all; this additionally requires
649
806
  `--allow-email`. It is rejected alongside identity, contact or response edits.
650
807
 
808
+ To swap two seated people, use `event seating swap`. Give each attendee's
809
+ `registrationAttendeeId` with the `seatId` they hold now, plus a `reason`. Both
810
+ seats change in one transaction, so neither is free for anyone else in between.
811
+ If either person has moved since you looked, the swap is refused; list the
812
+ occupants again and retry. Each person must be attending or submitted with no
813
+ payment pending, and each seat must be allowed for the other person's audience
814
+ tier, form and published seat map. `--target` is either attendee ID:
815
+
816
+ ```sh
817
+ rsvp event seating swap \
818
+ --ack-sensitive-data \
819
+ --allow-state-change \
820
+ --target attendee_a \
821
+ --payload '{"organizationId":"org_123","eventId":"evt_123","attendees":[{"registrationAttendeeId":"attendee_a","seatId":"seat_1"},{"registrationAttendeeId":"attendee_b","seatId":"seat_2"}],"reason":"Client asked to seat them with their team"}'
822
+ ```
823
+
824
+ Add `"seatChangeNotice":{"idempotencyKey":"<uuid>"}` and `--allow-email` to
825
+ queue a seat-change notice for each registration involved. Use
826
+ `event seating occupants list` to find attendee and seat IDs. Swaps need the
827
+ `seating.reseat` permission; notices also need `confirmation.resend`.
828
+
651
829
  ```sh
652
830
  rsvp people registration update \
653
831
  --ack-sensitive-data --allow-state-change --allow-email --target reg_123 \
654
832
  --payload '{"organizationId":"org_123","eventId":"evt_123","registrationId":"reg_123","expectedVersion":4,"reason":"Moved to finance table","seats":[{"registrationAttendeeId":"att_1","seatId":"seat_9"},{"registrationAttendeeId":"att_2","seatId":"seat_2"}],"seatChangeNotice":{"idempotencyKey":"4d68a320-013d-43c7-bfcc-d1126827d8c5"}}'
655
833
  ```
656
834
 
835
+ ### Unarchive for a fresh response versus attending Restore
836
+
837
+ `people registration unarchive` forwards the permissioned, audited
838
+ `event.bulkManageRegistrations` RESTORE operation. Supply 1–100 unique
839
+ `registrations`, each with its current `expectedVersion` (positive integer), and
840
+ an audit `reason` (3–500 characters after trimming). `action` defaults to
841
+ `RESTORE` when omitted; any other action is rejected. The approval target is
842
+ exactly the payload's **event ID**, not an individual registration ID.
843
+
844
+ ```sh
845
+ rsvp people registration unarchive \
846
+ --allow-state-change --target evt_123 \
847
+ --payload '{"organizationId":"org_123","eventId":"evt_123","reason":"Allow a fresh response","registrations":[{"id":"reg_123","expectedVersion":6}]}'
848
+ ```
849
+
850
+ Unarchive clears the archive marker and lets an eligible cancelled nonattending
851
+ invitee return to `IMPORTED` for a fresh response. The server checks current
852
+ permissions, revisions, eligibility and settled financial state; the CLI does
853
+ not interpret cancelled history or make eligibility decisions. Historical
854
+ registration/payment records remain history, and the old cancelled
855
+ registration and QR code are **not reinstated**. Seats, capacity, session
856
+ selections and waitlist entries are not recovered. A fresh submission uses the
857
+ normal flow and current checks, with new payment and credentials where required.
858
+ Unarchive cannot shortcut attending recovery. The result's
859
+ `readmittedInviteeCount` reports invitees reopened for a fresh response;
860
+ `changedCount` and `releasedAttendeeCount` retain their existing meanings.
861
+
862
+ `people registration restore` instead reinstates a cancelled or archived
863
+ **attending registration**, with its existing identity/payment/QR subject to the
864
+ server's restoration checks. It is not Unarchive and does not start a fresh response.
865
+ Read it first with `people registration edit get` and `"mode":"RESTORE"` to see
866
+ eligible seats and each attendee's previous seat. Supply `expectedVersion`, an
867
+ audit `reason`, and a seat for every attendee who had one; the server claims
868
+ capacity, books the seats and rejects full events, re-registered invitees and
869
+ unresolved or refunded payments. A full event is rejected unless
870
+ `overCapacityReason` (10–500 characters) is given, which needs the
871
+ `capacity.override` permission (event and org admins); the override and reason
872
+ are recorded in the activity log. Menu (dish) capacity is still enforced and
873
+ cannot be overridden. While an event is over capacity, declines and smaller
874
+ parties, payments confirmed while their hold is still live, and unexpired
875
+ waitlist offers (up to the offered party size) still go through. A payment
876
+ confirmed after its hold lapsed gets the normal capacity check and goes to
877
+ review if the place was reused. The current limit can be saved unchanged; any
878
+ new limit must be at least the confirmed count. The existing QR code works
879
+ again. Add
880
+ `confirmation` (with `--allow-email`) to queue the participant confirmation.
881
+ Session selections cancelled by archiving are not restored; the response
882
+ reports `droppedSessionSelections`.
883
+
884
+ ```sh
885
+ rsvp people registration restore \
886
+ --ack-sensitive-data --allow-state-change --allow-email --target reg_123 \
887
+ --payload '{"organizationId":"org_123","eventId":"evt_123","registrationId":"reg_123","expectedVersion":6,"reason":"Archived by mistake","seats":[{"registrationAttendeeId":"att_1","seatId":"seat_9"}],"confirmation":{"idempotencyKey":"4d68a320-013d-43c7-bfcc-d1126827d8c5"}}'
888
+ ```
889
+
657
890
  A separate manual notice for an already committed revision remains available:
658
891
 
659
892
  ```sh
@@ -766,3 +999,27 @@ email, external-write, and state-change effect approvals. It creates a workspace
766
999
  awaiting owner acceptance and sends a 48-hour first-owner invitation. The internal
767
1000
  provisioner does not receive customer organization membership. The response
768
1001
  reports invitation delivery failure so the invitation can be resent safely.
1002
+
1003
+ ### Assign guests rail reads
1004
+
1005
+ `event seating guests list` includes unarchived, confirmed attendees (legacy
1006
+ `ATTENDING` and current `SUBMITTED`), including guests with no active seat. An
1007
+ active booking on any layout counts as seated. Groups are registration parties.
1008
+ Both reads require `seating.view` and `rsvp.view`; names/search/name sorting and
1009
+ dietary filters/labels additionally require their sensitive-data grants.
1010
+
1011
+ Shared filters: `seating` (`UNSEATED`, `SEATED`, `ALL`), `query` (max 120),
1012
+ `audienceTierId`, `registrationId`, `dietaryTag`, `guestKind` (`ALL`, `HOST`,
1013
+ `GUEST`). List adds `limit` (1–100; default 25), `sort` (`GROUP`, `NAME`), opaque
1014
+ `cursor`, and `direction` (`forward`, `backward`). Backward requires a cursor.
1015
+ The response is `{items,pageInfo}`; use its `nextCursor`/`previousCursor`, not a
1016
+ row ID. Cursors are bound to filters, ordering, scope and sensitive grants; a
1017
+ changed context requires restarting the list. Deleted cursor anchor rows do not
1018
+ break paging. Offset `page`/`perPage` and unknown keys are rejected.
1019
+
1020
+ `event seating guests count` rejects paging/sort keys. It returns the matching
1021
+ count, seating-tab counts (ignore their own seating filter), tier counts (ignore
1022
+ their own tier filter), dietary choices (ignore their own dietary filter), and
1023
+ permission flags. Facets expose at most 100 choices with truncation flags; tier
1024
+ counts stream compact registration aggregates in bounded reads, without a hard
1025
+ registration-count ceiling. Rail page rows are never loaded event-wide.