@go-labs-sg/rsvp 1.2.0 → 1.4.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.
package/README.md CHANGED
@@ -108,7 +108,7 @@ rsvp event seating draft preview --organization-id org_123 --event-id evt_123 \
108
108
  --expected-version 1 --ack-sensitive-data
109
109
  rsvp event seating draft publish --organization-id org_123 --event-id evt_123 \
110
110
  --expected-version 1 --request-id 123e4567-e89b-42d3-a456-426614174000 \
111
- --reason 'Seat the confirmed guest' --allow-state-change --target evt_123
111
+ --reason 'Seat the confirmed guest' --allow-state-change --allow-email --target evt_123
112
112
  ```
113
113
 
114
114
  Save and Publish require the latest `expectedVersion`. A successful Publish applies
@@ -116,12 +116,37 @@ all changed guests atomically and clears only that version of your draft. Resolv
116
116
  preview issues before publishing; conflicts leave all bookings and the draft intact.
117
117
  For an unknown outcome, retry the same UUID `requestId`, version, reason and email
118
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,
119
+ clearing a newer draft. A different intent needs a new request ID. Preserve the
120
+ exact `seatChangeEmailRegistrationIds` set on retry (order is immaterial); changing
121
+ it, including changing between omitted and `[]`, rejects reuse of the request ID.
122
+
123
+ Preview returns `noticeRecipients`: one row per affected registration, with a
124
+ registration ID, a grant-scoped name, `eligible`, and an `ineligibleReason` when
125
+ unavailable. No email addresses are exposed by this seating response. Review both
126
+ eligible and ineligible rows before choosing who receives mail; several moving
127
+ party members still produce only one recipient registration.
128
+
129
+ Use `seatChangeEmailRegistrationIds` in a full `--payload` to choose the exact
130
+ eligible affected registrations. Leave a registration out to publish its seat
131
+ changes without email. `[]` explicitly sends no email, **including when the event
132
+ has automatic seat-change notifications**. Selecting an unaffected registration,
133
+ an ineligible recipient or duplicate IDs is rejected before publishing. The API
134
+ rechecks permissions and eligibility under the publish transaction, not just in
135
+ preview. Sending requires notice permission. An explicit empty array does not.
136
+
137
+ ```sh
138
+ rsvp event seating draft publish --allow-state-change --allow-email --target evt_123 \
139
+ --payload '{"organizationId":"org_123","eventId":"evt_123","expectedVersion":1,"requestId":"123e4567-e89b-42d3-a456-426614174000","reason":"Seat the confirmed party","sendSeatChangeEmail":true,"seatChangeEmailRegistrationIds":["reg_123"]}'
140
+ ```
141
+
142
+ Do not combine `--payload` with command-local version/reason/request flags. If the
143
+ recipient field is omitted, existing behavior remains: `sendSeatChangeEmail: true`
144
+ requests all affected registrations, otherwise the automatic event policy still
145
+ applies. Every draft publish conservatively requires CLI `--allow-email` (or the
146
+ interactive effects confirmation), even for explicit `[]`; it approves an effect
147
+ but does not enable sending. Use the matching server/client revision for explicit
148
+ recipient choices; older servers reject the new field rather than silently
149
+ ignoring it. Preview counts recipient registrations, not guests. On realtime-enabled servers,
125
150
  free new targets require validated per-registration holds at Publish, supplied as
126
151
  `seatHolds: [{registrationId, holds: [{seatMapId, clientId, holdToken}]}]` in the
127
152
  payload. Acquire these through the existing organizer hold-token flow; Stage never
@@ -377,7 +402,11 @@ including tax. Omitted tax remains compatible with legacy no-tax configuration.
377
402
  Read `payment event get` back with the same scope to verify the saved policy.
378
403
 
379
404
  `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
405
+ total, plus `capturedAttempt`: the selected attempt when it is captured;
406
+ otherwise the latest captured attempt, including refunded ones. `payment mine receipt get` (`eventSlug`, optional `formSlug`,
407
+ `paymentAttemptId`) reads the provider's current hosted receipt link for the
408
+ caller's own captured attempt; links expire, so read it when needed rather than
409
+ storing it. `payment event list` exposes each attempt's original `quote` plus
381
410
  `refundBreakdown` (cumulative refunded subtotal, tax and total). Pending refunds
382
411
  are not confirmed refunded amounts. These are payment breakdowns, not statutory
383
412
  tax invoices. Organizers choose applicable settings; configuration does not
@@ -533,6 +562,56 @@ it for the active registration/edit definition. Deploy the updated server before
533
562
  saving these settings because older validators discard unsupported fields.
534
563
  No database schema migration or attendee backfill is required.
535
564
 
565
+ ## Confirmation email layout, name, QR codes and attachments
566
+
567
+ In the form builder's **Flow settings → Confirmation email layout**, or through
568
+ `definition.communications.participant.email` with `event flow draft save`,
569
+ customise the participant confirmation email for every outcome of the form:
570
+
571
+ ```json
572
+ {
573
+ "communications": {
574
+ "participant": {
575
+ "email": {
576
+ "heading": "Hello {{name}}!",
577
+ "eventLine": null,
578
+ "confirmationButtonLabel": "View my Quest pass",
579
+ "supportFooter": "Questions? Email quest@example.com",
580
+ "showEventDetails": false,
581
+ "includeCheckInQrCodes": true,
582
+ "attachments": []
583
+ }
584
+ }
585
+ }
586
+ }
587
+ ```
588
+
589
+ - `{{name}}` works in template `subject`, `body`, `bodyRichText` and every
590
+ text field above. It uses the primary attendee's name, then the invitee's
591
+ name, and falls back to `there`.
592
+ - `heading`, `eventLine`, `confirmationButtonLabel` and `supportFooter`: omit to
593
+ keep the built-in text, `null` to hide that part, or a string to replace it.
594
+ A custom or hidden `supportFooter` also replaces the resubmission footer.
595
+ - `showEventDetails` (default `true`) controls the When/Where lines.
596
+ - `includeCheckInQrCodes` (default `false`) embeds each attendee's check-in QR
597
+ image once the event's check-in QR codes are released; before release the
598
+ email keeps the release message.
599
+ - `attachments`: up to 5 PDF, PNG or JPEG files, 10 MB each and 15 MB total.
600
+ Create an upload with `event flow confirmation attachment upload create`
601
+ (`organizationId`, `eventId`, `filename`, `contentType`, `byteSize`), `PUT`
602
+ the file unchanged to the returned `uploadUrl` with its `Content-Type`, then
603
+ add the returned `attachment` object to the list. Preview/publish reject keys
604
+ not uploaded for the same event, and publish verifies each uploaded object's
605
+ type and size.
606
+
607
+ Merge these fields into the complete current definition and publish as for
608
+ other form settings. Omitting `email` keeps existing emails unchanged. Deploy
609
+ the updated server before saving these settings. No database migration is
610
+ required.
611
+
612
+ `event flow preview` sends the definition as a POST body so large formatted
613
+ emails no longer exceed URL limits; it requires a server with this change.
614
+
536
615
  ## Administrator registration for an invitee
537
616
 
538
617
  Organizers can register someone on their behalf at any time, including after
@@ -908,8 +987,34 @@ configuration version. Configurable copy is
908
987
  `definition.communications.participant.seatChange: {enabled, subject, body}`;
909
988
  merge into the complete named draft, preview, save with its revision, read back,
910
989
  then publish only with separate approval. Existing responses use their saved
911
- published version. Both default disabled/blank; missing copy never falls back to
912
- confirmation copy. No event-specific copy is supplied or enabled by this change.
990
+ published version **unless** the event explicitly opts into the current automatic
991
+ policy. Both default disabled/blank; missing copy never falls back to confirmation
992
+ copy.
993
+
994
+ An event can opt into automatic notices using current event metadata
995
+ `seatChangeNotification: {automatic: true, enabled: true, subject, body}` through
996
+ that same revision-checked `REGISTRATION_RULES` update. It covers historical form
997
+ versions without rewriting responses or sending anything retroactively. Successful
998
+ draft publishes and direct organizer moves/swaps then atomically queue one notice
999
+ per affected registration even when the client checkbox is false or omitted.
1000
+ Non-seating edits, drafts, no-ops and rejected saves do not queue notices. Existing
1001
+ notification permissions, deduplication and stale-version delivery checks remain.
1002
+ Omitting `automatic` from a settings update preserves its saved value; explicitly
1003
+ send `automatic: false` to turn it off.
1004
+
1005
+ Because event policy may send automatically, registration updates containing
1006
+ `seats`, all swaps and all draft publishes now require `--allow-email` (or the
1007
+ existing interactive effects confirmation). This conservative CLI guard also
1008
+ applies to events without automatic notices; approval does not itself turn on
1009
+ notifications. Profile/menu-only updates and draft save/discard retain their
1010
+ previous effects.
1011
+
1012
+ Optional `registrationEmail` and `registrationMasterList` presentation settings
1013
+ are also written through `REGISTRATION_RULES`; omitted settings are preserved.
1014
+ See `examples/amgen-rsvp-copy/registration-presentation-patch.json` and its README
1015
+ for a reviewed activation fragment, output-key semantics, receipt/header behavior
1016
+ and the separate deployment/configuration/live-send approval gates. No event
1017
+ settings are activated by installing the code or reading this example.
913
1018
 
914
1019
  ## Audience tiers
915
1020
 
package/dist/index.js CHANGED
@@ -7195,7 +7195,8 @@ var logoutCliSession = async ({
7195
7195
  // src/api-client.ts
7196
7196
  import {
7197
7197
  createTRPCProxyClient,
7198
- httpBatchLink
7198
+ httpBatchLink,
7199
+ splitLink
7199
7200
  } from "@trpc/client";
7200
7201
  import SuperJSON from "superjson";
7201
7202
 
@@ -7213,18 +7214,28 @@ var normalizeApiBaseUrl = (value) => {
7213
7214
  var getApiBaseUrl = (override) => normalizeApiBaseUrl(override ?? process.env.RSVP_API_URL ?? DEFAULT_RSVP_API_BASE_URL);
7214
7215
 
7215
7216
  // src/api-client.ts
7216
- var createRsvpClient = (apiBaseUrl = getApiBaseUrl(), accessToken) => createTRPCProxyClient({
7217
- links: [
7218
- httpBatchLink({
7219
- headers: () => accessToken ? {
7220
- authorization: `Bearer ${accessToken}`,
7221
- "x-trpc-source": "rsvp-cli"
7222
- } : { "x-trpc-source": "rsvp-cli" },
7223
- transformer: SuperJSON,
7224
- url: `${apiBaseUrl}/api/trpc`
7225
- })
7226
- ]
7227
- });
7217
+ var POST_QUERY_PATHS = new Set([
7218
+ "eventFlow.preview"
7219
+ ]);
7220
+ var createRsvpClient = (apiBaseUrl = getApiBaseUrl(), accessToken) => {
7221
+ const linkOptions = {
7222
+ headers: () => accessToken ? {
7223
+ authorization: `Bearer ${accessToken}`,
7224
+ "x-trpc-source": "rsvp-cli"
7225
+ } : { "x-trpc-source": "rsvp-cli" },
7226
+ transformer: SuperJSON,
7227
+ url: `${apiBaseUrl}/api/trpc`
7228
+ };
7229
+ return createTRPCProxyClient({
7230
+ links: [
7231
+ splitLink({
7232
+ condition: (op) => POST_QUERY_PATHS.has(op.path),
7233
+ true: httpBatchLink({ ...linkOptions, methodOverride: "POST" }),
7234
+ false: httpBatchLink(linkOptions)
7235
+ })
7236
+ ]
7237
+ });
7238
+ };
7228
7239
 
7229
7240
  // ../validators/src/assign-guests.ts
7230
7241
  var assignGuestFiltersInputSchema = object({
@@ -7565,6 +7576,45 @@ var magicLinkSignInInputSchema = object({
7565
7576
  email: email2({ error: "Enter a valid email address" }).trim(),
7566
7577
  callbackURL: string2().min(1).optional()
7567
7578
  });
7579
+ // ../../utils/src/rich-text.ts
7580
+ var richTextJsonShape = object({
7581
+ json: custom(),
7582
+ html: string2()
7583
+ });
7584
+ var htmlEntityMap = {
7585
+ amp: "&",
7586
+ apos: "'",
7587
+ cent: "\xA2",
7588
+ copy: "\xA9",
7589
+ emsp: " ",
7590
+ ensp: " ",
7591
+ gt: ">",
7592
+ lt: "<",
7593
+ nbsp: " ",
7594
+ ndash: "\u2013",
7595
+ quot: '"',
7596
+ reg: "\xAE",
7597
+ thinsp: " "
7598
+ };
7599
+ var decodeHtmlEntity = (entity) => {
7600
+ const fromCodePoint = (codePoint) => Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : " ";
7601
+ const normalizedEntity = entity.toLowerCase();
7602
+ if (normalizedEntity.startsWith("#x")) {
7603
+ const codePoint = Number.parseInt(normalizedEntity.slice(2), 16);
7604
+ return fromCodePoint(codePoint);
7605
+ }
7606
+ if (normalizedEntity.startsWith("#")) {
7607
+ const codePoint = Number.parseInt(normalizedEntity.slice(1), 10);
7608
+ return fromCodePoint(codePoint);
7609
+ }
7610
+ return htmlEntityMap[normalizedEntity] ?? `&${entity};`;
7611
+ };
7612
+ var stripHtmlToText = (html) => html.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<[^>]+>/g, " ").replace(/&(#\d+|#x[\da-f]+|[a-z][a-z0-9]+);/gi, (_, entity) => decodeHtmlEntity(entity)).replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/\s+/g, " ").trim();
7613
+ var extractTextFromRichText = (richText, fallback = "Untitled") => {
7614
+ const text = stripHtmlToText(richText?.html ?? "");
7615
+ return text || fallback;
7616
+ };
7617
+
7568
7618
  // ../validators/src/flow.ts
7569
7619
  var eventFlowDateAnswerSchema = exports_iso.date();
7570
7620
  var eventFlowDateTimeAnswerSchema = exports_iso.datetime({
@@ -7697,6 +7747,7 @@ var customFieldValidationSchema = object({
7697
7747
  });
7698
7748
  var customFieldBaseShape = {
7699
7749
  helpText: string2().trim().max(500).optional(),
7750
+ hideLabel: boolean2().optional(),
7700
7751
  label: string2().trim().min(1).max(240),
7701
7752
  outputKey: flowIdentifierSchema,
7702
7753
  required: boolean2().default(false),
@@ -7903,9 +7954,16 @@ var defaultEventSeatChangeNotification = {
7903
7954
  };
7904
7955
  var eventSeatChangeNotificationSchema = object({
7905
7956
  body: string2().trim().max(1e4).default(""),
7957
+ automatic: boolean2().optional(),
7906
7958
  enabled: boolean2().default(false),
7907
7959
  subject: string2().trim().max(240).default("")
7908
7960
  }).superRefine((notification, ctx) => {
7961
+ if (notification.automatic && !notification.enabled)
7962
+ ctx.addIssue({
7963
+ code: "custom",
7964
+ message: "Automatic seat-change notifications must be enabled.",
7965
+ path: ["enabled"]
7966
+ });
7909
7967
  if (notification.enabled && !notification.subject) {
7910
7968
  ctx.addIssue({
7911
7969
  code: "custom",
@@ -7921,7 +7979,115 @@ var eventSeatChangeNotificationSchema = object({
7921
7979
  });
7922
7980
  }
7923
7981
  });
7982
+ var getEventFlowConfirmationBodyText = (richText) => {
7983
+ const text = (node) => {
7984
+ if (node.type === "text")
7985
+ return node.text ?? "";
7986
+ if (node.type === "hardBreak")
7987
+ return `
7988
+ `;
7989
+ const content = (node.content ?? []).map(text).join("");
7990
+ return ["paragraph", "heading", "listItem"].includes(node.type ?? "") ? `${content}
7991
+ ` : content;
7992
+ };
7993
+ return text(richText.json).trim();
7994
+ };
7995
+ var confirmationBodyRichTextSchema = unknown().superRefine((value, ctx) => {
7996
+ const ancestors = new Set;
7997
+ const withinDepth = (nested, depth) => {
7998
+ if (depth > 32)
7999
+ return false;
8000
+ if (typeof nested !== "object" || nested === null)
8001
+ return true;
8002
+ if (ancestors.has(nested))
8003
+ return false;
8004
+ ancestors.add(nested);
8005
+ const valid = Object.values(nested).every((child) => withinDepth(child, depth + 1));
8006
+ ancestors.delete(nested);
8007
+ return valid;
8008
+ };
8009
+ let valid = withinDepth(value, 0);
8010
+ try {
8011
+ valid = valid && new TextEncoder().encode(JSON.stringify(value)).byteLength <= 250000;
8012
+ } catch {
8013
+ valid = false;
8014
+ }
8015
+ if (!valid)
8016
+ ctx.addIssue({
8017
+ code: "custom",
8018
+ message: "Confirmation body is too large or nested too deeply"
8019
+ });
8020
+ }).pipe(richTextJsonShape).superRefine((value, ctx) => {
8021
+ const validNode = (node) => {
8022
+ if (!node || typeof node !== "object" || !("type" in node) || typeof node.type !== "string")
8023
+ return false;
8024
+ if ("text" in node && typeof node.text !== "string")
8025
+ return false;
8026
+ if ("marks" in node && (!Array.isArray(node.marks) || !node.marks.every((mark) => mark && typeof mark === "object" && typeof mark.type === "string")))
8027
+ return false;
8028
+ return !("content" in node) || Array.isArray(node.content) && node.content.every(validNode);
8029
+ };
8030
+ if (!validNode(value.json) || value.json.type !== "doc") {
8031
+ ctx.addIssue({
8032
+ code: "custom",
8033
+ message: "Confirmation body must be an editor document"
8034
+ });
8035
+ return;
8036
+ }
8037
+ const length = getEventFlowConfirmationBodyText(value).length;
8038
+ if (length === 0 || length > 1e4)
8039
+ ctx.addIssue({
8040
+ code: "custom",
8041
+ message: "Confirmation body must contain 1\u201310,000 characters"
8042
+ });
8043
+ });
8044
+ var eventFlowConfirmationAttachmentContentTypes = [
8045
+ "application/pdf",
8046
+ "image/jpeg",
8047
+ "image/png"
8048
+ ];
8049
+ var maxEventFlowConfirmationAttachmentCount = 5;
8050
+ var maxEventFlowConfirmationAttachmentSize = 10 * 1024 * 1024;
8051
+ var maxEventFlowConfirmationAttachmentTotalSize = 15 * 1024 * 1024;
8052
+ var eventFlowConfirmationAttachmentFilenameSchema = string2().trim().min(1).max(120).regex(/^[^\\/:*?"<>|\r\n\t]+$/, "Attachment file names cannot contain path or control characters");
8053
+ var eventFlowConfirmationAttachmentSchema = strictObject({
8054
+ byteSize: number2().int().positive().max(maxEventFlowConfirmationAttachmentSize),
8055
+ contentType: _enum(eventFlowConfirmationAttachmentContentTypes),
8056
+ filename: eventFlowConfirmationAttachmentFilenameSchema,
8057
+ key: string2().trim().min(1).max(300)
8058
+ });
8059
+ var editableEmailCopySchema = (maxLength) => string2().trim().min(1).max(maxLength).nullable().optional();
8060
+ var eventFlowParticipantConfirmationEmailSchema = strictObject({
8061
+ attachments: array(eventFlowConfirmationAttachmentSchema).max(maxEventFlowConfirmationAttachmentCount).default([]),
8062
+ confirmationButtonLabel: editableEmailCopySchema(60),
8063
+ eventLine: editableEmailCopySchema(500),
8064
+ heading: editableEmailCopySchema(120),
8065
+ includeCheckInQrCodes: boolean2().default(false),
8066
+ showEventDetails: boolean2().default(true),
8067
+ supportFooter: editableEmailCopySchema(1000)
8068
+ }).superRefine((email, ctx) => {
8069
+ const totalSize = email.attachments.reduce((total, attachment) => total + attachment.byteSize, 0);
8070
+ if (totalSize > maxEventFlowConfirmationAttachmentTotalSize) {
8071
+ ctx.addIssue({
8072
+ code: "custom",
8073
+ message: "Confirmation attachments must total 15 MB or less",
8074
+ path: ["attachments"]
8075
+ });
8076
+ }
8077
+ const keys = new Set;
8078
+ for (const [index, attachment] of email.attachments.entries()) {
8079
+ if (keys.has(attachment.key)) {
8080
+ ctx.addIssue({
8081
+ code: "custom",
8082
+ message: "Each confirmation attachment can be added only once",
8083
+ path: ["attachments", index, "key"]
8084
+ });
8085
+ }
8086
+ keys.add(attachment.key);
8087
+ }
8088
+ });
7924
8089
  var participantConfirmationSchema = object({
8090
+ email: eventFlowParticipantConfirmationEmailSchema.optional(),
7925
8091
  enabled: boolean2(),
7926
8092
  includeResponseDetails: boolean2().default(false),
7927
8093
  replyTo: email2().optional(),
@@ -7929,6 +8095,7 @@ var participantConfirmationSchema = object({
7929
8095
  supportEmail: email2(),
7930
8096
  templates: array(object({
7931
8097
  body: string2().trim().min(1).max(1e4),
8098
+ bodyRichText: confirmationBodyRichTextSchema.optional(),
7932
8099
  outcomeId: eventFlowEntityIdSchema,
7933
8100
  subject: string2().trim().min(1).max(240)
7934
8101
  })).max(50)
@@ -9942,44 +10109,28 @@ var PUBLIC_EVENT_VALIDATED_FONT_FAMILIES = [
9942
10109
  "Zilla Slab Highlight"
9943
10110
  ];
9944
10111
 
9945
- // ../../utils/src/rich-text.ts
9946
- var richTextJsonShape = object({
9947
- json: custom(),
9948
- html: string2()
10112
+ // ../validators/src/registration-email-presentation.ts
10113
+ var registrationEmailPresentationSchema = strictObject({
10114
+ layout: _enum(["DEFAULT", "GROUPED"]).default("DEFAULT"),
10115
+ senderName: string2().trim().min(1).max(120).regex(/^[^<>\r\n]+$/).optional(),
10116
+ introText: string2().trim().min(1).max(1e4).optional(),
10117
+ menuDetailOutputKeys: array(string2().trim().min(1).max(120)).max(100).default([]),
10118
+ submittedConsentOutputKeys: array(string2().trim().min(1).max(120)).max(100).default([]),
10119
+ seatingDisclaimer: string2().trim().min(1).max(2000).optional(),
10120
+ useResubmissionFooter: boolean2().default(false),
10121
+ supportEmail: email2().optional(),
10122
+ showLuckyDrawId: boolean2().optional()
9949
10123
  });
9950
- var htmlEntityMap = {
9951
- amp: "&",
9952
- apos: "'",
9953
- cent: "\xA2",
9954
- copy: "\xA9",
9955
- emsp: " ",
9956
- ensp: " ",
9957
- gt: ">",
9958
- lt: "<",
9959
- nbsp: " ",
9960
- ndash: "\u2013",
9961
- quot: '"',
9962
- reg: "\xAE",
9963
- thinsp: " "
9964
- };
9965
- var decodeHtmlEntity = (entity) => {
9966
- const fromCodePoint = (codePoint) => Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : " ";
9967
- const normalizedEntity = entity.toLowerCase();
9968
- if (normalizedEntity.startsWith("#x")) {
9969
- const codePoint = Number.parseInt(normalizedEntity.slice(2), 16);
9970
- return fromCodePoint(codePoint);
9971
- }
9972
- if (normalizedEntity.startsWith("#")) {
9973
- const codePoint = Number.parseInt(normalizedEntity.slice(1), 10);
9974
- return fromCodePoint(codePoint);
9975
- }
9976
- return htmlEntityMap[normalizedEntity] ?? `&${entity};`;
9977
- };
9978
- var stripHtmlToText = (html) => html.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<[^>]+>/g, " ").replace(/&(#\d+|#x[\da-f]+|[a-z][a-z0-9]+);/gi, (_, entity) => decodeHtmlEntity(entity)).replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/\s+/g, " ").trim();
9979
- var extractTextFromRichText = (richText, fallback = "Untitled") => {
9980
- const text = stripHtmlToText(richText?.html ?? "");
9981
- return text || fallback;
9982
- };
10124
+
10125
+ // ../validators/src/registration-master-list-presentation.ts
10126
+ var outputKeysSchema = array(string2().min(1).max(160).regex(/^[A-Za-z][A-Za-z0-9_.:-]*$/)).max(100).refine((keys) => new Set(keys).size === keys.length, {
10127
+ message: "Output keys must be unique"
10128
+ });
10129
+ var eventRegistrationMasterListSchema = object({
10130
+ omittedOutputKeys: outputKeysSchema.default([]),
10131
+ finalOutputKeys: outputKeysSchema.default([]),
10132
+ showEligibilityCutoffInHeading: boolean2().default(false)
10133
+ }).strict().refine((config) => !config.finalOutputKeys.some((key) => config.omittedOutputKeys.includes(key)), { message: "Omitted output keys cannot also be final output keys" });
9983
10134
 
9984
10135
  // ../validators/src/event.ts
9985
10136
  var eventStatusSchema = _enum(EventStatus);
@@ -10296,7 +10447,10 @@ var eventTablePaginationSchema = object({
10296
10447
  var listEventInviteesInputSchema = organizationEventByIdInputSchema.extend({
10297
10448
  ...eventTablePaginationSchema.shape,
10298
10449
  audienceTierId: string2().min(1).nullable().optional(),
10299
- status: _enum(InviteeStatus).optional()
10450
+ status: _enum(InviteeStatus).optional(),
10451
+ statuses: array(_enum(InviteeStatus)).min(1).max(7).optional(),
10452
+ cursor: string2().min(1).max(64).optional(),
10453
+ direction: _enum(["NEXT", "PREVIOUS"]).default("NEXT")
10300
10454
  });
10301
10455
  var listEventRegistrationsInputSchema = organizationEventByIdInputSchema.extend({
10302
10456
  archive: _enum(["ACTIVE", "ARCHIVED", "ALL"]).default("ACTIVE"),
@@ -10474,6 +10628,8 @@ var updateEventRegistrationRulesInputSchema = setupStepScopeSchema.extend({
10474
10628
  capacityLimit: eventCapacityLimitSchema.nullable(),
10475
10629
  guestPolicy: guestPolicySchema,
10476
10630
  seatChangeNotification: eventSeatChangeNotificationSchema.optional(),
10631
+ registrationEmail: registrationEmailPresentationSchema.optional(),
10632
+ registrationMasterList: eventRegistrationMasterListSchema.optional(),
10477
10633
  waitlistEnabled: boolean2(),
10478
10634
  waitlistOfferTtlMinutes: number2().int().min(5).max(10080)
10479
10635
  }).refine((value) => !value.rsvpOpensAt || !value.rsvpClosesAt || value.rsvpOpensAt < value.rsvpClosesAt, {
@@ -10769,6 +10925,11 @@ var createEventFlowImageAssetUploadInputSchema = organizationEventByIdInputSchem
10769
10925
  byteSize: number2().int().positive().max(maxEventFlowImageAssetSize),
10770
10926
  mimeType: eventFlowImageAssetMimeTypeSchema
10771
10927
  });
10928
+ var createEventFlowConfirmationAttachmentUploadInputSchema = organizationEventByIdInputSchema.extend({
10929
+ byteSize: number2().int().positive().max(maxEventFlowConfirmationAttachmentSize),
10930
+ contentType: _enum(eventFlowConfirmationAttachmentContentTypes),
10931
+ filename: eventFlowConfirmationAttachmentFilenameSchema
10932
+ });
10772
10933
  var finalizeEventFlowImageAssetInputSchema = organizationEventByIdInputSchema.extend({
10773
10934
  assetId: eventFlowImageAssetIdSchema
10774
10935
  });
@@ -10872,6 +11033,10 @@ var createEventFlowConsentVersionInputSchema = organizationEventByIdInputSchema.
10872
11033
  var setEventFlowGroupPasscodeInputSchema = organizationEventFlowInputSchema.extend({
10873
11034
  passcode: string2().trim().min(8).max(160)
10874
11035
  });
11036
+ var getEventFlowSeatCategoryColorsInputSchema = eventBySlugInputSchema.extend({
11037
+ flowVersionId: flowVersionIdSchema,
11038
+ seatMapId: flowVersionIdSchema
11039
+ }).strict();
10875
11040
  var issueEventFlowSeatHoldTokenInputSchema = eventBySlugInputSchema.extend({
10876
11041
  clientId: uuid2(),
10877
11042
  flowVersionId: flowVersionIdSchema,
@@ -11360,6 +11525,9 @@ var createPaymentCheckoutInputSchema = object({
11360
11525
  idempotencyKey: uuid2()
11361
11526
  });
11362
11527
  var getMyPaymentInputSchema = object({ eventSlug: eventSlugSchema });
11528
+ var getMyPaymentReceiptInputSchema = getMyPaymentInputSchema.extend({
11529
+ paymentAttemptId: string2().min(1)
11530
+ });
11363
11531
  var listEventPaymentsInputSchema = eventScopeSchema4.extend({
11364
11532
  cursor: string2().min(1).optional(),
11365
11533
  limit: number2().int().min(1).max(100).default(50)
@@ -11862,6 +12030,9 @@ var publishSeatingDraftInputSchema = seatingDraftScopeSchema.extend({
11862
12030
  requestId: uuid2(),
11863
12031
  reason: string2().trim().min(1).max(500),
11864
12032
  sendSeatChangeEmail: boolean2().default(false),
12033
+ seatChangeEmailRegistrationIds: array(string2().min(1)).max(100).refine((ids) => new Set(ids).size === ids.length, {
12034
+ message: "Each recipient registration may appear only once."
12035
+ }).optional(),
11865
12036
  seatHolds: array(object({
11866
12037
  registrationId: string2().min(1),
11867
12038
  holds: array(object({
@@ -11949,7 +12120,7 @@ var commands = {
11949
12120
  effects: [],
11950
12121
  operation: (api, input) => api.eventFlow.preview.query(input),
11951
12122
  payloadOnly: true,
11952
- summary: "Validate and preview an event flow, including date-limit messages (--payload JSON)."
12123
+ summary: "Validate and preview an event flow, including date-limit messages; the definition is sent as a POST body (--payload JSON)."
11953
12124
  },
11954
12125
  "event flow draft get": {
11955
12126
  effects: [],
@@ -12304,6 +12475,7 @@ Object.assign(commands, {
12304
12475
  "audit list": payloadCommand((api, input) => api.audit.listEventActivity.query(input), "List event audit activity (--payload JSON).", ["sensitive-data"]),
12305
12476
  "event check-in qr": payloadCommand((api, input) => api.event.checkInByQr.mutate(input), "Check in using a QR token (--payload JSON).", ["state-change"]),
12306
12477
  "event flow image upload create": payloadCommand((api, input) => api.eventFlow.createImageAssetUpload.mutate(input), "Create an event-local registration-flow image upload URL (--payload JSON).", ["external-write", "state-change"], "eventId"),
12478
+ "event flow confirmation attachment upload create": payloadCommand((api, input) => api.eventFlow.createConfirmationAttachmentUpload.mutate(input), "Create a confirmation-email attachment upload URL for a PDF, PNG or JPEG up to 10 MB (filename, contentType, byteSize); PUT the file there, then add the returned attachment to communications.participant.email.attachments (--payload JSON).", ["external-write", "state-change"], "eventId"),
12307
12479
  "event flow image finalize": payloadCommand((api, input) => api.eventFlow.finalizeImageAsset.mutate(input), "Finalize an event-local registration-flow image after the external upload (--payload JSON).", ["state-change"], "assetId"),
12308
12480
  "event logo upload create": payloadCommand((api, input) => api.event.createLogoUpload.mutate(input), "Create an event-logo upload URL (--payload JSON).", ["external-write", "state-change"], "eventId"),
12309
12481
  "event logo update": payloadCommand((api, input) => api.event.updateLogo.mutate(input), "Set or clear the event logo (--payload JSON).", ["state-change"], "eventId"),
@@ -12339,13 +12511,14 @@ Object.assign(commands, {
12339
12511
  "event seating occupants list": payloadCommand((api, input) => api.event.getLiveSeatingOccupants.query(input), "List permitted occupants by seatMapId, tableId, department, categoryValue and query; follow nextCursor (--payload JSON).", ["sensitive-data"]),
12340
12512
  "people registration edit get": payloadCommand((api, input) => api.event.getRegistrationForEdit.query(input), "Get current revision, permitted attendees, eligible seats and notification readiness; mode RESTORE loads seat maps and previous seats for a cancelled registration (--payload JSON).", ["sensitive-data"]),
12341
12513
  "people registration update": {
12342
- ...payloadCommand((api, input) => api.event.updateRegistration.mutate(input), "Save organizer edits with expectedVersion and reason; seating requires full-party seats and configured holds. Optional seatChangeNotice queues a seat-change notice atomically with a seat-only save (--payload JSON).", ["sensitive-data", "state-change"], "registrationId"),
12514
+ ...payloadCommand((api, input) => api.event.updateRegistration.mutate(input), "Save organizer edits with expectedVersion and reason; seating requires full-party seats, configured holds and --allow-email because event policy may automatically queue notices. Optional seatChangeNotice queues a notice with a seat-only save (--payload JSON).", ["sensitive-data", "state-change"], "registrationId"),
12343
12515
  conditionalEffects: [
12344
- { effect: "email", whenPayloadField: "seatChangeNotice" }
12516
+ { effect: "email", whenPayloadField: "seatChangeNotice" },
12517
+ { effect: "email", whenPayloadField: "seats" }
12345
12518
  ]
12346
12519
  },
12347
12520
  "event seating swap": {
12348
- ...payloadCommand((api, input) => api.event.swapAttendeeSeats.mutate(input), "Swap the seats of two seated attendees in one save, given each attendee's current seatId and a reason. Optional seatChangeNotice queues a seat-change notice for each registration involved (--payload JSON).", ["sensitive-data", "state-change"]),
12521
+ ...payloadCommand((api, input) => api.event.swapAttendeeSeats.mutate(input), "Swap the seats of two seated attendees in one save, given each attendee's current seatId and a reason. Requires --allow-email because event policy may automatically queue one notice per affected registration; optional seatChangeNotice also requests notices (--payload JSON).", ["sensitive-data", "state-change", "email"]),
12349
12522
  conditionalEffects: [
12350
12523
  { effect: "email", whenPayloadField: "seatChangeNotice" }
12351
12524
  ]
@@ -12363,7 +12536,7 @@ Object.assign(commands, {
12363
12536
  "event mine list": payloadCommand((api, input) => api.event.listMyEvents.query(input), "List the caller's events (--payload JSON)."),
12364
12537
  "event reminder list": payloadCommand((api, input) => api.event.listReminderSegments.query(input), "List reminder segments (--payload JSON).", ["sensitive-data"]),
12365
12538
  "event rsvp submit": payloadCommand((api, input) => api.event.submitRsvp.mutate(input), "Submit the caller's registration (--payload JSON).", ["sensitive-data", "state-change"]),
12366
- "event setup step update": payloadCommand((api, input) => api.event.updateSetupStep.mutate(input), "Update one event setup step (--payload JSON).", ["state-change"]),
12539
+ "event setup step update": payloadCommand((api, input) => api.event.updateSetupStep.mutate(input), "Update one revision-checked event setup step (--payload JSON). REGISTRATION_RULES accepts registrationEmail presentation, registrationMasterList output-key ordering/omissions and seatChangeNotification automatic policy; omitted settings are preserved.", ["state-change"]),
12367
12540
  "event page update": payloadCommand((api, input) => api.event.updateSetupStep.mutate({
12368
12541
  ...input,
12369
12542
  section: "DETAILS_PAGE"
@@ -12376,6 +12549,7 @@ Object.assign(commands, {
12376
12549
  Object.assign(commands, {
12377
12550
  "payment event get": payloadCommand((api, input) => api.payment.getEventPayment.query(input), "Get event payment configuration (--payload JSON).", ["financial-write"]),
12378
12551
  "payment mine get": payloadCommand((api, input) => api.payment.getMyPayment.query(input), "Get the caller's payment with its saved subtotal, tax and final total breakdown (--payload JSON).", ["financial-write", "sensitive-data"]),
12552
+ "payment mine receipt get": payloadCommand((api, input) => api.payment.getMyPaymentReceipt.query(input), "Get the provider's current hosted receipt link for the caller's captured payment attempt; receiptUrl is null when unavailable (--payload JSON).", ["financial-write", "sensitive-data"]),
12379
12553
  "payment event list": payloadCommand((api, input) => api.payment.listEventPayments.query(input), "List event payments with original quote and cumulative refunded subtotal/tax/total, including verified external refunds (--payload JSON).", ["financial-write", "sensitive-data"]),
12380
12554
  "payment exception list": payloadCommand((api, input) => api.payment.listPaymentExceptions.query(input), "List payment exceptions (--payload JSON).", ["financial-write", "sensitive-data"]),
12381
12555
  "payment exception resolve": payloadCommand((api, input) => api.payment.resolvePaymentException.mutate(input), "Resolve a payment exception (--payload JSON).", ["financial-write", "sensitive-data", "state-change"]),
@@ -12440,11 +12614,11 @@ Object.assign(commands, {
12440
12614
  "--expected-version": { key: "expectedVersion", type: "number" }
12441
12615
  },
12442
12616
  operation: (api, input) => api.event.getSeatingDraftPreview.query(getSeatingDraftPreviewInputSchema.parse(input)),
12443
- summary: "Check a saved draft against fresh live seats, grants and notification policy; --expected-version is required."
12617
+ summary: "Check fresh seats, grants and notification policy. Returns noticeRecipients: one eligible/ineligible row per affected registration with grant-scoped names, never email addresses; --expected-version is required."
12444
12618
  },
12445
12619
  "event seating draft publish": {
12446
12620
  scope: "event",
12447
- effects: ["state-change"],
12621
+ effects: ["state-change", "email"],
12448
12622
  targetKey: "eventId",
12449
12623
  conditionalEffects: [
12450
12624
  { effect: "email", whenPayloadField: "sendSeatChangeEmail" }
@@ -12456,7 +12630,7 @@ Object.assign(commands, {
12456
12630
  },
12457
12631
  validateInput: (input) => validateSeatingDraftInput(publishSeatingDraftInputSchema, input),
12458
12632
  operation: (api, input) => api.event.publishSeatingDraft.mutate(publishSeatingDraftInputSchema.parse(input)),
12459
- summary: "Atomically publish all draft changes with expectedVersion, UUID requestId and reason. Email defaults off; retries reuse the requestId and intent. Realtime-enabled free targets need per-registration seatHolds (--payload JSON)."
12633
+ summary: "Atomically publish with expectedVersion, UUID requestId and reason. Optional seatChangeEmailRegistrationIds in --payload chooses exact affected eligible registration recipients; [] sends none, including automatic policy. Omit to retain sendSeatChangeEmail/event-policy behavior. Requires --allow-email. Retry the same requestId and exact recipient set. Realtime free targets need per-registration seatHolds."
12460
12634
  }
12461
12635
  });
12462
12636
  var getCommandManifest = () => ({
@@ -76,3 +76,75 @@ rsvp event setup update \
76
76
  ```
77
77
 
78
78
  This command is documented for later use and was not executed for this task.
79
+
80
+ ## Receipt, master-list and automatic seat-change presentation (#1657)
81
+
82
+ `registration-presentation-patch.json` is a **configuration fragment**, not an
83
+ executable mutation payload. It contains reusable event settings with Amgen copy
84
+ and confirmed output keys. No staff-exclusion filter is needed or included.
85
+
86
+ After the compatible API/web/CLI release and **separate approval**, refresh the
87
+ test event first. Build a `REGISTRATION_RULES` input for
88
+ `rsvp event setup step update` with its current `organizationId`, `eventId`,
89
+ `version: configurationVersion`, `rsvpOpensAt`, `rsvpClosesAt`, `dietaryClosesAt`,
90
+ `capacityLimit`, `guestPolicy`, `waitlistEnabled` and
91
+ `waitlistOfferTtlMinutes`. Merge the fragment's three top-level keys into that
92
+ input; retain every other current setting. Do not send the fragment to the
93
+ full `event setup update` endpoint. Review the exact payload before writing,
94
+ read the same event back afterwards, and verify each key. Repeat independently
95
+ for the official event only after test-event acceptance and approval. Do not
96
+ reuse cached version numbers, event IDs or schedules. No commands in this
97
+ section have been executed.
98
+
99
+ - **Receipt:** `registrationEmail` groups check-in, Table Seating, Menu Selection
100
+ and other responses. The current presentation can update old receipts without
101
+ changing their saved form, outcome or answers. Accepted `indemnityAccepted`
102
+ displays “Submitted”; false/missing values do not become accepted. Saved
103
+ `confirmation.showLuckyDrawId: false` remains authoritative; the event may
104
+ additionally hide IDs, but cannot reveal IDs a saved form hid. The display name
105
+ becomes “ASM DND Registration” while retaining the configured verified sender
106
+ address/domain. The one footer derives the event's current registration URL
107
+ and named form slug at delivery, so the test event cannot point to the official
108
+ form by copying an example URL. This fragment replaces the old free-text body
109
+ with one short introduction, avoiding duplicate acknowledgement/footer copy.
110
+ - **Export:** `registrationMasterList.omittedOutputKeys: ["menu:diet"]` omits only
111
+ the derived Vegetarian/non-vegetarian classification. The actual employee/guest
112
+ menus and dietary answers remain. `finalOutputKeys: ["indemnityAccepted"]`
113
+ moves consent after operational columns. The cutoff heading uses the event's
114
+ existing lucky-draw cutoff/timezone; this fragment does **not** change it. The
115
+ cached approved cutoff is 27 November 2026, 8pm Asia/Singapore; verify it afresh.
116
+ - **Automatic notices:** event metadata `seatChangeNotification.automatic: true`
117
+ deliberately enables the current policy for historical form versions too.
118
+ It does not migrate stored forms, responses, or seating. Draft publish and
119
+ direct committed organizer move/swap queue one minimal notice per affected
120
+ registration in the same transaction, regardless of a manual checkbox. The
121
+ current registration email and committed seats are re-read at delivery. A
122
+ failed/denied operation, no-op or non-seating edit does not queue a notice;
123
+ retries do not queue another notice for the same committed request. Existing
124
+ stale-version delivery suppression remains. Enabling this policy **does not
125
+ enqueue or resend anything retroactively**.
126
+
127
+ The fragment's seat-change copy and subject are a proposed activation payload;
128
+ review them before a live write. The support address `events@getout.sg` is
129
+ confirmed. Sender/recipient/contact and active policy must be verified with a
130
+ controlled test send only after separate email approval. Queue acceptance is
131
+ not provider delivery.
132
+
133
+ `confirmation.resend` permission is still required when an automatic notice
134
+ would be queued. A seating edit without that permission fails atomically rather
135
+ than silently skipping the notice. Registration-settings clients that omit the
136
+ new `automatic` property retain the stored value; explicitly set `automatic:
137
+ false` when turning the policy off. An automatic policy cannot be disabled merely
138
+ by saving `enabled: false` without explicitly clearing `automatic`.
139
+
140
+ CLI safety remains separate from the automatic policy: direct registration
141
+ updates containing `seats`, all swaps, and all seating-draft publishes require
142
+ `--allow-email` (or the existing interactive effects confirmation), even when
143
+ `sendSeatChangeEmail` is false or omitted. This conservative guard also applies
144
+ to events without automatic notices; the flag grants approval but does not
145
+ itself enable sending. Non-seating profile/menu updates and saving or discarding
146
+ a draft do not acquire an email effect.
147
+
148
+ Production deployment, event configuration writes, live submissions/reseating,
149
+ export jobs, resends and any real provider send require explicit approval and
150
+ readback. Local HTML/XLSX and synthetic browser checks are not live acceptance.
@@ -0,0 +1,29 @@
1
+ {
2
+ "registrationEmail": {
3
+ "layout": "GROUPED",
4
+ "senderName": "ASM DND Registration",
5
+ "introText": "Thank you for your response.",
6
+ "menuDetailOutputKeys": [
7
+ "hasDietaryRestrictions",
8
+ "dietaryRestrictions",
9
+ "guestHasDietaryRestrictions",
10
+ "guestDietaryRestrictions"
11
+ ],
12
+ "submittedConsentOutputKeys": ["indemnityAccepted"],
13
+ "seatingDisclaimer": "Table seating is subject to changes based on the discretion of the organisation committee, due to seating capacity and to accommodate all guests.",
14
+ "useResubmissionFooter": true,
15
+ "supportEmail": "events@getout.sg",
16
+ "showLuckyDrawId": false
17
+ },
18
+ "registrationMasterList": {
19
+ "omittedOutputKeys": ["menu:diet"],
20
+ "finalOutputKeys": ["indemnityAccepted"],
21
+ "showEligibilityCutoffInHeading": true
22
+ },
23
+ "seatChangeNotification": {
24
+ "automatic": true,
25
+ "enabled": true,
26
+ "subject": "ASM Dinner and Dance 2026 — seating updated",
27
+ "body": "Your table seating for ASM Dinner and Dance 2026 has been updated. Please refer to your current seat assignments below.\n\nTable seating is subject to changes based on the discretion of the organisation committee, due to seating capacity and to accommodate all guests.\n\nFor further queries, please contact events@getout.sg."
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@go-labs-sg/rsvp",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "RSVP CLI for authorized event operations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",