@go-labs-sg/rsvp 1.1.0 → 1.3.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
@@ -377,7 +377,11 @@ including tax. Omitted tax remains compatible with legacy no-tax configuration.
377
377
  Read `payment event get` back with the same scope to verify the saved policy.
378
378
 
379
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
380
+ total, plus `capturedAttempt`: the selected attempt when it is captured;
381
+ otherwise the latest captured attempt, including refunded ones. `payment mine receipt get` (`eventSlug`, optional `formSlug`,
382
+ `paymentAttemptId`) reads the provider's current hosted receipt link for the
383
+ caller's own captured attempt; links expire, so read it when needed rather than
384
+ storing it. `payment event list` exposes each attempt's original `quote` plus
381
385
  `refundBreakdown` (cumulative refunded subtotal, tax and total). Pending refunds
382
386
  are not confirmed refunded amounts. These are payment breakdowns, not statutory
383
387
  tax invoices. Organizers choose applicable settings; configuration does not
@@ -908,8 +912,34 @@ configuration version. Configurable copy is
908
912
  `definition.communications.participant.seatChange: {enabled, subject, body}`;
909
913
  merge into the complete named draft, preview, save with its revision, read back,
910
914
  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.
915
+ published version **unless** the event explicitly opts into the current automatic
916
+ policy. Both default disabled/blank; missing copy never falls back to confirmation
917
+ copy.
918
+
919
+ An event can opt into automatic notices using current event metadata
920
+ `seatChangeNotification: {automatic: true, enabled: true, subject, body}` through
921
+ that same revision-checked `REGISTRATION_RULES` update. It covers historical form
922
+ versions without rewriting responses or sending anything retroactively. Successful
923
+ draft publishes and direct organizer moves/swaps then atomically queue one notice
924
+ per affected registration even when the client checkbox is false or omitted.
925
+ Non-seating edits, drafts, no-ops and rejected saves do not queue notices. Existing
926
+ notification permissions, deduplication and stale-version delivery checks remain.
927
+ Omitting `automatic` from a settings update preserves its saved value; explicitly
928
+ send `automatic: false` to turn it off.
929
+
930
+ Because event policy may send automatically, registration updates containing
931
+ `seats`, all swaps and all draft publishes now require `--allow-email` (or the
932
+ existing interactive effects confirmation). This conservative CLI guard also
933
+ applies to events without automatic notices; approval does not itself turn on
934
+ notifications. Profile/menu-only updates and draft save/discard retain their
935
+ previous effects.
936
+
937
+ Optional `registrationEmail` and `registrationMasterList` presentation settings
938
+ are also written through `REGISTRATION_RULES`; omitted settings are preserved.
939
+ See `examples/amgen-rsvp-copy/registration-presentation-patch.json` and its README
940
+ for a reviewed activation fragment, output-key semantics, receipt/header behavior
941
+ and the separate deployment/configuration/live-send approval gates. No event
942
+ settings are activated by installing the code or reading this example.
913
943
 
914
944
  ## Audience tiers
915
945
 
package/dist/index.js CHANGED
@@ -7565,6 +7565,45 @@ var magicLinkSignInInputSchema = object({
7565
7565
  email: email2({ error: "Enter a valid email address" }).trim(),
7566
7566
  callbackURL: string2().min(1).optional()
7567
7567
  });
7568
+ // ../../utils/src/rich-text.ts
7569
+ var richTextJsonShape = object({
7570
+ json: custom(),
7571
+ html: string2()
7572
+ });
7573
+ var htmlEntityMap = {
7574
+ amp: "&",
7575
+ apos: "'",
7576
+ cent: "\xA2",
7577
+ copy: "\xA9",
7578
+ emsp: " ",
7579
+ ensp: " ",
7580
+ gt: ">",
7581
+ lt: "<",
7582
+ nbsp: " ",
7583
+ ndash: "\u2013",
7584
+ quot: '"',
7585
+ reg: "\xAE",
7586
+ thinsp: " "
7587
+ };
7588
+ var decodeHtmlEntity = (entity) => {
7589
+ const fromCodePoint = (codePoint) => Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : " ";
7590
+ const normalizedEntity = entity.toLowerCase();
7591
+ if (normalizedEntity.startsWith("#x")) {
7592
+ const codePoint = Number.parseInt(normalizedEntity.slice(2), 16);
7593
+ return fromCodePoint(codePoint);
7594
+ }
7595
+ if (normalizedEntity.startsWith("#")) {
7596
+ const codePoint = Number.parseInt(normalizedEntity.slice(1), 10);
7597
+ return fromCodePoint(codePoint);
7598
+ }
7599
+ return htmlEntityMap[normalizedEntity] ?? `&${entity};`;
7600
+ };
7601
+ 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();
7602
+ var extractTextFromRichText = (richText, fallback = "Untitled") => {
7603
+ const text = stripHtmlToText(richText?.html ?? "");
7604
+ return text || fallback;
7605
+ };
7606
+
7568
7607
  // ../validators/src/flow.ts
7569
7608
  var eventFlowDateAnswerSchema = exports_iso.date();
7570
7609
  var eventFlowDateTimeAnswerSchema = exports_iso.datetime({
@@ -7697,6 +7736,7 @@ var customFieldValidationSchema = object({
7697
7736
  });
7698
7737
  var customFieldBaseShape = {
7699
7738
  helpText: string2().trim().max(500).optional(),
7739
+ hideLabel: boolean2().optional(),
7700
7740
  label: string2().trim().min(1).max(240),
7701
7741
  outputKey: flowIdentifierSchema,
7702
7742
  required: boolean2().default(false),
@@ -7903,9 +7943,16 @@ var defaultEventSeatChangeNotification = {
7903
7943
  };
7904
7944
  var eventSeatChangeNotificationSchema = object({
7905
7945
  body: string2().trim().max(1e4).default(""),
7946
+ automatic: boolean2().optional(),
7906
7947
  enabled: boolean2().default(false),
7907
7948
  subject: string2().trim().max(240).default("")
7908
7949
  }).superRefine((notification, ctx) => {
7950
+ if (notification.automatic && !notification.enabled)
7951
+ ctx.addIssue({
7952
+ code: "custom",
7953
+ message: "Automatic seat-change notifications must be enabled.",
7954
+ path: ["enabled"]
7955
+ });
7909
7956
  if (notification.enabled && !notification.subject) {
7910
7957
  ctx.addIssue({
7911
7958
  code: "custom",
@@ -7921,6 +7968,68 @@ var eventSeatChangeNotificationSchema = object({
7921
7968
  });
7922
7969
  }
7923
7970
  });
7971
+ var getEventFlowConfirmationBodyText = (richText) => {
7972
+ const text = (node) => {
7973
+ if (node.type === "text")
7974
+ return node.text ?? "";
7975
+ if (node.type === "hardBreak")
7976
+ return `
7977
+ `;
7978
+ const content = (node.content ?? []).map(text).join("");
7979
+ return ["paragraph", "heading", "listItem"].includes(node.type ?? "") ? `${content}
7980
+ ` : content;
7981
+ };
7982
+ return text(richText.json).trim();
7983
+ };
7984
+ var confirmationBodyRichTextSchema = unknown().superRefine((value, ctx) => {
7985
+ const ancestors = new Set;
7986
+ const withinDepth = (nested, depth) => {
7987
+ if (depth > 32)
7988
+ return false;
7989
+ if (typeof nested !== "object" || nested === null)
7990
+ return true;
7991
+ if (ancestors.has(nested))
7992
+ return false;
7993
+ ancestors.add(nested);
7994
+ const valid = Object.values(nested).every((child) => withinDepth(child, depth + 1));
7995
+ ancestors.delete(nested);
7996
+ return valid;
7997
+ };
7998
+ let valid = withinDepth(value, 0);
7999
+ try {
8000
+ valid = valid && new TextEncoder().encode(JSON.stringify(value)).byteLength <= 250000;
8001
+ } catch {
8002
+ valid = false;
8003
+ }
8004
+ if (!valid)
8005
+ ctx.addIssue({
8006
+ code: "custom",
8007
+ message: "Confirmation body is too large or nested too deeply"
8008
+ });
8009
+ }).pipe(richTextJsonShape).superRefine((value, ctx) => {
8010
+ const validNode = (node) => {
8011
+ if (!node || typeof node !== "object" || !("type" in node) || typeof node.type !== "string")
8012
+ return false;
8013
+ if ("text" in node && typeof node.text !== "string")
8014
+ return false;
8015
+ if ("marks" in node && (!Array.isArray(node.marks) || !node.marks.every((mark) => mark && typeof mark === "object" && typeof mark.type === "string")))
8016
+ return false;
8017
+ return !("content" in node) || Array.isArray(node.content) && node.content.every(validNode);
8018
+ };
8019
+ if (!validNode(value.json) || value.json.type !== "doc") {
8020
+ ctx.addIssue({
8021
+ code: "custom",
8022
+ message: "Confirmation body must be an editor document"
8023
+ });
8024
+ return;
8025
+ }
8026
+ const length = getEventFlowConfirmationBodyText(value).length;
8027
+ if (length === 0 || length > 1e4)
8028
+ ctx.addIssue({
8029
+ code: "custom",
8030
+ message: "Confirmation body must contain 1\u201310,000 characters"
8031
+ });
8032
+ });
7924
8033
  var participantConfirmationSchema = object({
7925
8034
  enabled: boolean2(),
7926
8035
  includeResponseDetails: boolean2().default(false),
@@ -7929,6 +8038,7 @@ var participantConfirmationSchema = object({
7929
8038
  supportEmail: email2(),
7930
8039
  templates: array(object({
7931
8040
  body: string2().trim().min(1).max(1e4),
8041
+ bodyRichText: confirmationBodyRichTextSchema.optional(),
7932
8042
  outcomeId: eventFlowEntityIdSchema,
7933
8043
  subject: string2().trim().min(1).max(240)
7934
8044
  })).max(50)
@@ -9942,44 +10052,28 @@ var PUBLIC_EVENT_VALIDATED_FONT_FAMILIES = [
9942
10052
  "Zilla Slab Highlight"
9943
10053
  ];
9944
10054
 
9945
- // ../../utils/src/rich-text.ts
9946
- var richTextJsonShape = object({
9947
- json: custom(),
9948
- html: string2()
10055
+ // ../validators/src/registration-email-presentation.ts
10056
+ var registrationEmailPresentationSchema = strictObject({
10057
+ layout: _enum(["DEFAULT", "GROUPED"]).default("DEFAULT"),
10058
+ senderName: string2().trim().min(1).max(120).regex(/^[^<>\r\n]+$/).optional(),
10059
+ introText: string2().trim().min(1).max(1e4).optional(),
10060
+ menuDetailOutputKeys: array(string2().trim().min(1).max(120)).max(100).default([]),
10061
+ submittedConsentOutputKeys: array(string2().trim().min(1).max(120)).max(100).default([]),
10062
+ seatingDisclaimer: string2().trim().min(1).max(2000).optional(),
10063
+ useResubmissionFooter: boolean2().default(false),
10064
+ supportEmail: email2().optional(),
10065
+ showLuckyDrawId: boolean2().optional()
9949
10066
  });
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
- };
10067
+
10068
+ // ../validators/src/registration-master-list-presentation.ts
10069
+ 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, {
10070
+ message: "Output keys must be unique"
10071
+ });
10072
+ var eventRegistrationMasterListSchema = object({
10073
+ omittedOutputKeys: outputKeysSchema.default([]),
10074
+ finalOutputKeys: outputKeysSchema.default([]),
10075
+ showEligibilityCutoffInHeading: boolean2().default(false)
10076
+ }).strict().refine((config) => !config.finalOutputKeys.some((key) => config.omittedOutputKeys.includes(key)), { message: "Omitted output keys cannot also be final output keys" });
9983
10077
 
9984
10078
  // ../validators/src/event.ts
9985
10079
  var eventStatusSchema = _enum(EventStatus);
@@ -10078,10 +10172,13 @@ var eventCheckInQrReleaseSchema = discriminatedUnion("mode", [
10078
10172
  var eventPublicPageSchema = object({
10079
10173
  accessNotice: string2().trim().min(1).max(500).optional(),
10080
10174
  deadlineLabel: string2().trim().min(1).max(60).optional(),
10175
+ paymentRedirectMessage: string2().trim().min(1).max(500).optional(),
10176
+ paymentRedirectTitle: string2().trim().min(1).max(120).optional(),
10081
10177
  registrationDescription: string2().trim().min(1).max(500).optional(),
10082
10178
  registrationActionLabel: string2().trim().min(1).max(100).optional(),
10083
10179
  registrationTitle: string2().trim().min(1).max(120).optional(),
10084
10180
  responseUpdateNotice: string2().trim().min(1).max(300).optional(),
10181
+ scheduleNote: string2().trim().min(1).max(300).optional(),
10085
10182
  showAccessNotice: boolean2().optional(),
10086
10183
  showDeadlineNotice: boolean2().optional(),
10087
10184
  showResponseUpdateNotice: boolean2().optional()
@@ -10139,6 +10236,44 @@ var requiredEventDetailsRichTextSchema = (label, maxLength) => eventDetailsRichT
10139
10236
  });
10140
10237
  }
10141
10238
  });
10239
+ var maxEventDescriptionLength = 2000;
10240
+ var isJsonRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10241
+ var getEventDescriptionPlainText = (richText) => {
10242
+ const lines = [];
10243
+ let line = "";
10244
+ const flush = () => {
10245
+ const text = line.replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/\s+/g, " ").trim();
10246
+ if (text)
10247
+ lines.push(text);
10248
+ line = "";
10249
+ };
10250
+ const visit = (node, depth) => {
10251
+ if (!isJsonRecord(node) || depth > 32)
10252
+ return;
10253
+ if (node.type === "text") {
10254
+ if (typeof node.text === "string")
10255
+ line += node.text;
10256
+ return;
10257
+ }
10258
+ if (node.type === "hardBreak") {
10259
+ flush();
10260
+ return;
10261
+ }
10262
+ const children = Array.isArray(node.content) ? node.content : [];
10263
+ const isTextBlock = children.some((child) => isJsonRecord(child) && (child.type === "text" || child.type === "hardBreak"));
10264
+ if (!isTextBlock)
10265
+ flush();
10266
+ for (const child of children)
10267
+ visit(child, depth + 1);
10268
+ flush();
10269
+ };
10270
+ visit(richText?.json, 0);
10271
+ return lines.join(`
10272
+ `);
10273
+ };
10274
+ var eventDescriptionRichTextSchema = eventDetailsRichTextSchema.refine((value) => getEventDescriptionPlainText(value).length <= maxEventDescriptionLength, {
10275
+ message: `Description must be ${maxEventDescriptionLength} characters or fewer`
10276
+ });
10142
10277
  var eventDetailsPageSchema = object({
10143
10278
  titleRichText: requiredEventDetailsRichTextSchema("a page title", 180),
10144
10279
  subtitleRichText: eventDetailsRichTextSchema.nullable().optional(),
@@ -10402,7 +10537,8 @@ var updateRetentionSetupStepInputSchema = setupStepScopeSchema.extend({
10402
10537
  var updateEventDetailsInputSchema = setupStepScopeSchema.extend({
10403
10538
  section: literal("DETAILS"),
10404
10539
  name: createEventInputSchema.shape.name,
10405
- description: string2().trim().max(2000).nullable(),
10540
+ description: string2().trim().max(maxEventDescriptionLength).nullable(),
10541
+ descriptionRichText: eventDescriptionRichTextSchema.nullable().optional(),
10406
10542
  venueName: string2().trim().max(220).nullable(),
10407
10543
  slug: eventSlugSchema,
10408
10544
  timeZone: eventTimeZoneSchema,
@@ -10432,6 +10568,8 @@ var updateEventRegistrationRulesInputSchema = setupStepScopeSchema.extend({
10432
10568
  capacityLimit: eventCapacityLimitSchema.nullable(),
10433
10569
  guestPolicy: guestPolicySchema,
10434
10570
  seatChangeNotification: eventSeatChangeNotificationSchema.optional(),
10571
+ registrationEmail: registrationEmailPresentationSchema.optional(),
10572
+ registrationMasterList: eventRegistrationMasterListSchema.optional(),
10435
10573
  waitlistEnabled: boolean2(),
10436
10574
  waitlistOfferTtlMinutes: number2().int().min(5).max(10080)
10437
10575
  }).refine((value) => !value.rsvpOpensAt || !value.rsvpClosesAt || value.rsvpOpensAt < value.rsvpClosesAt, {
@@ -11318,6 +11456,9 @@ var createPaymentCheckoutInputSchema = object({
11318
11456
  idempotencyKey: uuid2()
11319
11457
  });
11320
11458
  var getMyPaymentInputSchema = object({ eventSlug: eventSlugSchema });
11459
+ var getMyPaymentReceiptInputSchema = getMyPaymentInputSchema.extend({
11460
+ paymentAttemptId: string2().min(1)
11461
+ });
11321
11462
  var listEventPaymentsInputSchema = eventScopeSchema4.extend({
11322
11463
  cursor: string2().min(1).optional(),
11323
11464
  limit: number2().int().min(1).max(100).default(50)
@@ -12297,13 +12438,14 @@ Object.assign(commands, {
12297
12438
  "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"]),
12298
12439
  "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"]),
12299
12440
  "people registration update": {
12300
- ...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"),
12441
+ ...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"),
12301
12442
  conditionalEffects: [
12302
- { effect: "email", whenPayloadField: "seatChangeNotice" }
12443
+ { effect: "email", whenPayloadField: "seatChangeNotice" },
12444
+ { effect: "email", whenPayloadField: "seats" }
12303
12445
  ]
12304
12446
  },
12305
12447
  "event seating swap": {
12306
- ...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"]),
12448
+ ...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"]),
12307
12449
  conditionalEffects: [
12308
12450
  { effect: "email", whenPayloadField: "seatChangeNotice" }
12309
12451
  ]
@@ -12321,7 +12463,7 @@ Object.assign(commands, {
12321
12463
  "event mine list": payloadCommand((api, input) => api.event.listMyEvents.query(input), "List the caller's events (--payload JSON)."),
12322
12464
  "event reminder list": payloadCommand((api, input) => api.event.listReminderSegments.query(input), "List reminder segments (--payload JSON).", ["sensitive-data"]),
12323
12465
  "event rsvp submit": payloadCommand((api, input) => api.event.submitRsvp.mutate(input), "Submit the caller's registration (--payload JSON).", ["sensitive-data", "state-change"]),
12324
- "event setup step update": payloadCommand((api, input) => api.event.updateSetupStep.mutate(input), "Update one event setup step (--payload JSON).", ["state-change"]),
12466
+ "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"]),
12325
12467
  "event page update": payloadCommand((api, input) => api.event.updateSetupStep.mutate({
12326
12468
  ...input,
12327
12469
  section: "DETAILS_PAGE"
@@ -12334,6 +12476,7 @@ Object.assign(commands, {
12334
12476
  Object.assign(commands, {
12335
12477
  "payment event get": payloadCommand((api, input) => api.payment.getEventPayment.query(input), "Get event payment configuration (--payload JSON).", ["financial-write"]),
12336
12478
  "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"]),
12479
+ "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"]),
12337
12480
  "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"]),
12338
12481
  "payment exception list": payloadCommand((api, input) => api.payment.listPaymentExceptions.query(input), "List payment exceptions (--payload JSON).", ["financial-write", "sensitive-data"]),
12339
12482
  "payment exception resolve": payloadCommand((api, input) => api.payment.resolvePaymentException.mutate(input), "Resolve a payment exception (--payload JSON).", ["financial-write", "sensitive-data", "state-change"]),
@@ -12402,7 +12545,7 @@ Object.assign(commands, {
12402
12545
  },
12403
12546
  "event seating draft publish": {
12404
12547
  scope: "event",
12405
- effects: ["state-change"],
12548
+ effects: ["state-change", "email"],
12406
12549
  targetKey: "eventId",
12407
12550
  conditionalEffects: [
12408
12551
  { effect: "email", whenPayloadField: "sendSeatChangeEmail" }
@@ -12414,7 +12557,7 @@ Object.assign(commands, {
12414
12557
  },
12415
12558
  validateInput: (input) => validateSeatingDraftInput(publishSeatingDraftInputSchema, input),
12416
12559
  operation: (api, input) => api.event.publishSeatingDraft.mutate(publishSeatingDraftInputSchema.parse(input)),
12417
- 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)."
12560
+ summary: "Atomically publish all draft changes with expectedVersion, UUID requestId and reason. Requires --allow-email: event policy can automatically send notices even when sendSeatChangeEmail is false. Retries reuse requestId and intent. Realtime-enabled free targets need per-registration seatHolds (--payload JSON)."
12418
12561
  }
12419
12562
  });
12420
12563
  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.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "RSVP CLI for authorized event operations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",