@go-labs-sg/rsvp 0.17.0 → 1.1.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 +506 -39
  2. package/dist/index.js +5900 -188
  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
 
@@ -135,6 +208,76 @@ rsvp event flow versions list --organization-id org_123 --event-id evt_123
135
208
  rsvp event flow resources get --organization-id org_123 --event-id evt_123
136
209
  ```
137
210
 
211
+ ### Registration-flow admission policy
212
+
213
+ `event flow draft get` returns normalized legacy invitation forms as
214
+ `authentication.mode: "EMAIL_OTP"` with `definition.admissionPolicy: "ROSTER_ONLY"`.
215
+ Carry that independent policy through `event flow draft save`, `event flow publish`,
216
+ and `event flow version activate`; none of those operations silently opens signup.
217
+ The participant-safe `event flow published get` admission manifest also reports
218
+ `admissionPolicy` (`OPEN` for modern definitions without an explicit policy).
219
+
220
+ Save the complete returned definition with its current `expectedRevision`.
221
+ Omitting `admissionPolicy` preserves the stored draft/publication policy for older
222
+ clients. To intentionally broaden admission, explicitly set it to `"OPEN"` in the
223
+ modern email-auth definition, save, then publish; this remains subject to event
224
+ permissions, audience restrictions, and email-domain policy for new attendees.
225
+ Existing eligible roster attendees keep their existing eligibility rules.
226
+
227
+ ```sh
228
+ rsvp event flow draft get --payload '{"organizationId":"org_123","eventId":"evt_123","flowId":"flow_123"}'
229
+ # Save the complete JSON definition and expectedRevision in flow-draft.json.
230
+ # Keep admissionPolicy: "ROSTER_ONLY" unless the organiser approves open signup.
231
+ rsvp event flow draft save --allow-state-change --target evt_123 --payload "$(cat flow-draft.json)"
232
+ rsvp event flow publish --allow-state-change --target evt_123 --payload '{"organizationId":"org_123","eventId":"evt_123","flowId":"flow_123","expectedDraftRevision":2}'
233
+ rsvp event flow version activate --allow-state-change --target evt_123 --payload '{"organizationId":"org_123","eventId":"evt_123","flowId":"flow_123","flowVersionId":"version_123"}'
234
+ ```
235
+
236
+ Replace illustrative IDs and revisions with current readback values. Deploy API
237
+ policy enforcement before clients; an old API does not understand this field.
238
+ No authentication, consent flags, command names, or credential protocol changes.
239
+
240
+ ### Registration-flow date-limit messages
241
+
242
+ `DATE` and `DATETIME` custom fields accept optional
243
+ `field.validation.minDateMessage` and `maxDateMessage`: trimmed, non-empty plain
244
+ text of at most 500 characters. Each replaces only its corresponding out-of-range
245
+ explanation. Inclusive `minDate`/`maxDate` bounds, malformed-date errors and
246
+ required-field errors are unchanged; omitted messages keep the existing defaults.
247
+
248
+ An illustrative DOB validation object is:
249
+
250
+ ```json
251
+ {
252
+ "maxDate": "2008-01-01",
253
+ "maxDateMessage": "You must be at least 18 to register for this event."
254
+ }
255
+ ```
256
+
257
+ Age wording is organizer-authored, not a dynamic age rule. Choose the cutoff for
258
+ the event; do not reuse the illustrative date blindly.
259
+
260
+ Read the full current definition/revision using `event flow draft get`, edit the
261
+ intended field, then pass the complete definition to `event flow preview` and
262
+ `event flow draft save --allow-state-change --target <eventId> --payload <JSON>`.
263
+ Preview/save payloads include `organizationId`, `eventId`, optional `flowId`, and
264
+ `definition`; save also requires `expectedRevision`. Publish separately using
265
+ `event flow publish --allow-state-change --target <eventId> --payload <JSON>` with
266
+ `expectedDraftRevision`. Read back the saved definition with `event flow draft get`
267
+ and verify its compiled participant projection using the authenticated organizer
268
+ `event flow preview` command. Use `event flow versions list` to check immutable
269
+ version/publication identity; `event flow published get` provides public admission
270
+ and publication metadata in normal CLI execution, not the full participant fields.
271
+ It has no participant-session forwarding; the API returns `flow.participant` as
272
+ `null` without matching participant access. Verify the actual published date limits
273
+ and messages through the event's registration flow with a valid participant
274
+ session.
275
+
276
+ Existing commands forward the definition unchanged to the authenticated shared
277
+ API; no new flags or CLI-only validation are needed. Deploy matching API/web
278
+ support before saving these keys via the CLI; older APIs strip unrecognized
279
+ message keys. No event or published version is updated automatically.
280
+
138
281
  ### Registration-flow image upload lifecycle
139
282
 
140
283
  Registration-flow images are **named, event-local assets**. Create one with an
@@ -175,6 +318,180 @@ protocol. Reference the finalized asset from the appropriate form choice, then
175
318
  save and publish the flow separately when the complete draft is approved.
176
319
 
177
320
 
321
+ ## Stripe payments and external refunds
322
+
323
+ `payment checkout create` uses the same server-enforced finite reservation policy
324
+ as the web app. New Checkout sessions exclude known slow-settling methods while
325
+ retaining dynamic immediate methods such as cards and PayNow. Existing sessions
326
+ and late-success safeguards are unchanged.
327
+
328
+ `payment event list` reports completed refund totals including separately
329
+ recorded, verified refunds issued outside RSVP. Pending/failed provider refunds
330
+ do not count, and an external refund never consumes an unrelated app refund
331
+ request. `payment refund` still requires the existing financial/external-write
332
+ gates and original account binding. `payment exception list` remains the place
333
+ to inspect unresolved payment exceptions. Existing command inputs and gates are
334
+ unchanged. `payment refund recover` checks an existing local or external refund
335
+ adjustment through its stored provider reference; it never creates a new refund:
336
+
337
+ ```sh
338
+ rsvp payment refund recover \
339
+ --ack-sensitive-data --allow-external-write --allow-financial-write \
340
+ --allow-state-change --target adjustment_123 \
341
+ --payload '{"organizationId":"org_123","eventId":"evt_123","paymentAttemptId":"attempt_123","adjustmentId":"adjustment_123"}'
342
+ ```
343
+
344
+ The organization/event/attempt and adjustment must belong together, and the
345
+ caller must retain payment-adjustment permission through the provider lookup;
346
+ current actor authority and event lifecycle are rechecked transactionally before
347
+ any financial or audit writes. Repeated checks reconcile the
348
+ same adjustment safely; a missing provider reference requires separate operator
349
+ reconciliation, not a new key or refund request.
350
+
351
+ ### Tax configuration and breakdowns
352
+
353
+ `payment configure` accepts optional `tax` JSON. Use `{"mode":"NONE"}` for
354
+ no tax. `INCLUSIVE` extracts tax from the configured price; `EXCLUSIVE` adds
355
+ tax to it. Both taxable modes require a trimmed `label` (1–50 characters), an
356
+ integer `rateBasisPoints` from 0 to 10000, and explicit `donationTaxable`.
357
+ **100 basis points = 1%.** The example below is illustrative, not a recommended
358
+ rate or a decision about whether donations are taxable.
359
+
360
+ Read the current policy and `configurationVersion` before configuring, and
361
+ replace the IDs, version, amounts and tax settings for the intended event:
362
+
363
+ ```sh
364
+ rsvp payment event get --allow-financial-write \
365
+ --payload '{"organizationId":"org_123","eventId":"evt_123"}'
366
+
367
+ rsvp payment configure \
368
+ --allow-external-write --allow-financial-write --allow-state-change \
369
+ --target evt_123 \
370
+ --payload '{"organizationId":"org_123","eventId":"evt_123","eventVersion":7,"enabled":true,"currency":"SGD","amountMinor":1000,"pricingMode":"PER_REGISTRATION","donationMaximumMinor":500,"checkoutExpiryMinutes":30,"reservationPolicy":"HOLD_UNTIL_EXPIRY","tax":{"mode":"EXCLUSIVE","rateBasisPoints":900,"label":"Tax","donationTaxable":false}}'
371
+ ```
372
+
373
+ This is a complete configuration write, not a tax-only patch: preserve the
374
+ intended payment policy and credential selection. Add `flowId` for a named
375
+ form override; `useEventDefaults: true` makes that form inherit the event policy,
376
+ including tax. Omitted tax remains compatible with legacy no-tax configuration.
377
+ Read `payment event get` back with the same scope to verify the saved policy.
378
+
379
+ `payment mine get` exposes the attempt's saved tax quote and subtotal/tax/final
380
+ total. `payment event list` exposes each attempt's original `quote` plus
381
+ `refundBreakdown` (cumulative refunded subtotal, tax and total). Pending refunds
382
+ are not confirmed refunded amounts. These are payment breakdowns, not statutory
383
+ tax invoices. Organizers choose applicable settings; configuration does not
384
+ establish legal approval or activate an event. Deploy matching API/web/CLI
385
+ support before using these fields.
386
+
387
+ ### Pending Stripe connection preparation
388
+
389
+ Organization managers can persist separate named Sandbox and Live connections
390
+ without either secret. Generate one webhook UUID for each request, keep it for
391
+ retries, and replace all illustrative IDs below:
392
+
393
+ ```sh
394
+ rsvp stripe credential prepare \
395
+ --allow-external-write --allow-state-change --target org_123 \
396
+ --payload '{"organizationId":"org_123","name":"Client Sandbox","livemode":false,"webhookSourceId":"9dd0ba39-b55e-4e65-aa3e-d08242c45b8f"}'
397
+
398
+ rsvp stripe credential preparation list \
399
+ --payload '{"organizationId":"org_123"}'
400
+ ```
401
+
402
+ The response includes the saved preparation ID, webhook path, environment,
403
+ endpoint API version and required events. Confirm the deployment/hosting origin
404
+ before constructing the full client URL. Pending records survive reload but are
405
+ not usable payment credentials or defaults and cannot verify webhooks. Pending
406
+ names are also protected from legacy credential creation and rename operations;
407
+ unreserved names remain supported.
408
+
409
+ Prefer **Complete setup** in the encrypted integrations UI for secret entry.
410
+ For approved private automation, `stripe credential complete` calls the same
411
+ API with `organizationId`, the unchanged `name`, saved `preparationId` and
412
+ `webhookSourceId`, plus `secretKey` and `webhookSecret`. It requires
413
+ `--allow-external-write --allow-state-change --target <organizationId>`.
414
+ Both restricted `rk_` and secret `sk_` server keys are supported. Never place
415
+ secrets in chat, payment payloads, committed files, or shared shell history.
416
+ The complete command returns only public credential metadata.
417
+
418
+ Then `stripe credential test` takes `organizationId` and `credentialId` with
419
+ `--allow-external-write --allow-state-change --target <credentialId>`.
420
+ Its bounded account/Balance/Checkout/PaymentIntent/Refund reads verify the
421
+ prepared mode and read access, not write permission or webhook delivery. Only a
422
+ tested usable connection can be selected for payments; perform a separate sandbox
423
+ payment and webhook acceptance check before launch. These commands use the
424
+ normal authenticated API and never bypass organization-management permission.
425
+
426
+ Deploy with the schema/client and webhook prerequisites in
427
+ [the application guide](../../../apps/rsvp/README.md#refunds). Local generated
428
+ clients can be refreshed with `bun db:generate`; that is not a database push or
429
+ proof of deployed schema state.
430
+
431
+ ## Lucky Draw check-in cutoff
432
+
433
+ The event manager can save an optional cutoff in **Check-in → Settings → Lucky
434
+ draw eligibility**, in the event's configured timezone. The API stores an absolute
435
+ ISO timestamp with an explicit offset (`Z` or `±HH:MM`), not a browser-local time.
436
+ There is no fixed cutoff or 8 PM default. Unconfigured events return
437
+ `luckyDrawEligibility.cutoffAt: null`; malformed stored values also return `null`
438
+ rather than being interpreted in the browser's timezone.
439
+
440
+ Read the event first:
441
+
442
+ ```sh
443
+ rsvp event get --organization-id org_123 --event-id evt_123
444
+ ```
445
+
446
+ Copy its current `configurationVersion` into the setup payload's `version`.
447
+ Replace all example IDs, version `7`, and the illustrative timestamp below with
448
+ values for the intended event; the date, time, and offset are not defaults:
449
+
450
+ ```sh
451
+ rsvp event setup step update \
452
+ --allow-state-change \
453
+ --target evt_123 \
454
+ --payload '{"organizationId":"org_123","eventId":"evt_123","version":7,"section":"LUCKY_DRAW","cutoffAt":"2030-06-15T13:30:00+02:00"}'
455
+ ```
456
+
457
+ To clear the cutoff, read the event again and use its fresh version with
458
+ `cutoffAt: null` (not a blank string):
459
+
460
+ ```sh
461
+ rsvp event setup step update \
462
+ --allow-state-change \
463
+ --target evt_123 \
464
+ --payload '{"organizationId":"org_123","eventId":"evt_123","version":7,"section":"LUCKY_DRAW","cutoffAt":null}'
465
+ ```
466
+
467
+ Both writes require `event.manage`, the explicit state-change acknowledgement,
468
+ and a `--target` matching the payload's `eventId`. A stale version returns
469
+ `CONFLICT`: fetch the event, review the latest settings, then retry with its
470
+ current version rather than replaying an old payload. Read the event back after
471
+ a successful write to verify the cutoff and configuration version.
472
+
473
+ This configuration step only stores the cutoff and preserves unrelated event
474
+ metadata. It does not choose eligible people, apply category/exclusion rules,
475
+ import a draw roster, or reconcile prizes/vouchers. Deploy the updated API before
476
+ saving the new setup section; no database schema migration is required.
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
+
178
495
  ## Per-form confirmation presentation
179
496
 
180
497
  In the form builder's **Flow settings → Confirmation and check-in pass**, set
@@ -218,21 +535,37 @@ No database schema migration or attendee backfill is required.
218
535
 
219
536
  ## Administrator registration for an invitee
220
537
 
221
- Inspect the invitee's active registration form and current registration version
222
- before submitting. The context includes the published flow definition, invitee,
223
- 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`:
224
545
 
225
546
  ```sh
226
547
  rsvp event flow admin registration context get \
227
548
  --ack-sensitive-data \
228
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}}'
229
554
  ```
230
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
+
231
564
  Use the returned `flowVersionId` and `expectedRegistrationVersion` in the
232
- submission. Set `expectedOutcomeId` to the outcome selected by the answers in
233
- the published definition, and provide answers keyed by each field's `outputKey`.
234
- A new UUID `idempotencyKey` identifies this submission. The audit reason records
235
- 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`:
236
569
 
237
570
  ```sh
238
571
  rsvp event flow admin registration submit \
@@ -240,14 +573,42 @@ rsvp event flow admin registration submit \
240
573
  --allow-state-change \
241
574
  --target invitee_123 \
242
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}'
243
582
  ```
244
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
+
245
609
  Replace the example answer key and value with those required by the returned
246
- definition. If the flow requires a consent override, include a nonempty
247
- `consentOverrideReason` in the submission payload. The override records the
248
- administrator's reason in the audit trail; it does not record attendee consent
249
- or a signature. A stale registration version or flow version is rejected; fetch
250
- 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.
251
612
 
252
613
  Create an immutable menu version with an explicit mutation acknowledgement. The
253
614
  target is the event ID in the payload:
@@ -302,40 +663,24 @@ rsvp check-in by-reference \
302
663
 
303
664
  High-impact payment and retention commands remain protected by their matching financial, deletion, external-write, and sensitive-data gates. The API always re-evaluates the authenticated user's current Registration role, organization membership, event assignment, and resource permissions.
304
665
 
305
- ## Operational report exports
306
-
307
- `export request` accepts `format: "XLSX"` (the default when omitted) or `"CSV"`.
308
- Use `filters.view` to name the population rather than relying on a dataset label:
666
+ ## Registration master list export
309
667
 
310
- - RSVP: `MASTER` (all invitees, grouped employee/guest responses; default),
311
- `CURRENT` (current response rows per person), `HISTORY` (all registration
312
- records, not every edit revision), or `CLIENT_REGISTRATION` (current completed
313
- employee/guest responses, including declines; excludes nonrespondents/drafts).
314
- The client XLSX has **Attending**, **Not Attending**, **Master**, and **Metadata**
315
- sheets; its CSV contains Master data only. Guest staff/department fields
316
- describe the host, while menu and dietary answers remain person-scoped.
317
- - SEATING: `CURRENT` (active bookings; default) or `HISTORY` (active and released).
318
- - ATTENDANCE: `CURRENT` (expected people; default) or `CHECKED_IN` (expected people
319
- with a check-in that has not been undone), or `ALL_INVITEES` (roster invitees,
320
- including nonrespondents/declines, plus current guests). Registered, submitted,
321
- attending, and active check-in states remain separate. None defines draw
322
- eligibility.
323
-
324
- Form, registration-status and submission-date filters apply only to RSVP.
325
- Other datasets do not accept `view`. These combinations are validated by the API.
668
+ `export request` queues the single **Registration master list** Excel workbook
669
+ (one **Master List** sheet; question columns follow the event's form). The
670
+ payload is `organizationId`, `eventId` and an audit `reason`; the old `dataset`,
671
+ `format` and `filters` fields are ignored. See the RSVP app README for columns.
326
672
 
327
673
  ```sh
328
674
  rsvp export request \
329
675
  --ack-sensitive-data \
330
676
  --allow-state-change \
331
677
  --target evt_123 \
332
- --payload '{"organizationId":"org_123","eventId":"evt_123","dataset":"RSVP","format":"XLSX","filters":{"view":"MASTER"},"reason":"Organizer master list"}'
678
+ --payload '{"organizationId":"org_123","eventId":"evt_123","reason":"Organizer master list"}'
333
679
  ```
334
680
 
335
681
  Use `export list` to check the requester-scoped job, then `export download` for
336
- its current authorized download URL. For the client workbook, replace `MASTER`
337
- in the example with `CLIENT_REGISTRATION`. For the all-invitee attendance report,
338
- set `dataset` to `ATTENDANCE` and `filters.view` to `ALL_INVITEES`.
682
+ its current authorized download URL. `event registrations export` and
683
+ `event check-ins export` were removed; use `export request` instead.
339
684
 
340
685
  Operational check-in accepts `ATTENDING` and legacy `SUBMITTED`; roster/detail,
341
686
  manual/QR/reference entry, `report overview`, and `event people overview` use that same
@@ -343,9 +688,8 @@ population. Non-operational statuses remain excluded, and permissions, payment
343
688
  gates, duplicate protection, and undo behavior are unchanged. The web panel's
344
689
  realtime connection and manual refresh controls do not add a CLI subscription.
345
690
 
346
- Deploy matching API and export-worker versions before requesting the new views;
347
- old servers/workers do not understand them. Existing CLI commands pass the JSON
348
- payload to the shared typed API, so no new command or client-only rules are needed.
691
+ Deploy matching API and export-worker versions together; an old worker would build the retired
692
+ registration-records report for master-list jobs.
349
693
 
350
694
  ## Seating updates
351
695
 
@@ -374,15 +718,38 @@ Deploy the matching API and worker before using these commands from an updated
374
718
  CLI. They retain browser authentication, event permissions and sensitive-field
375
719
  grants; the CLI does not implement separate seating rules.
376
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
+
377
729
  ```sh
378
730
  rsvp event seating live get --ack-sensitive-data \
379
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"}'
380
736
  rsvp event seating occupants list --ack-sensitive-data \
381
737
  --payload '{"organizationId":"org_123","eventId":"evt_123","seatMapId":"map_123","categoryValue":"finance","limit":25,"cursor":0}'
382
738
  rsvp people registration edit get --ack-sensitive-data \
383
739
  --payload '{"organizationId":"org_123","eventId":"evt_123","registrationId":"reg_123"}'
384
740
  ```
385
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
+
386
753
  Occupants supports optional `tableId`, `department`, `categoryValue` and `query`
387
754
  filters. `categoryValue` is the canonical (trimmed, case-normalized) value
388
755
  returned by the occupant response's category legend; it is not an arbitrary
@@ -438,12 +805,88 @@ notice is queued in the move's database transaction, so the move and notice
438
805
  request commit together or not at all; this additionally requires
439
806
  `--allow-email`. It is rejected alongside identity, contact or response edits.
440
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
+
441
829
  ```sh
442
830
  rsvp people registration update \
443
831
  --ack-sensitive-data --allow-state-change --allow-email --target reg_123 \
444
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"}}'
445
833
  ```
446
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
+
447
890
  A separate manual notice for an already committed revision remains available:
448
891
 
449
892
  ```sh
@@ -556,3 +999,27 @@ email, external-write, and state-change effect approvals. It creates a workspace
556
999
  awaiting owner acceptance and sends a 48-hour first-owner invitation. The internal
557
1000
  provisioner does not receive customer organization membership. The response
558
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.