@go-labs-sg/rsvp 1.2.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);
@@ -10474,6 +10568,8 @@ var updateEventRegistrationRulesInputSchema = setupStepScopeSchema.extend({
10474
10568
  capacityLimit: eventCapacityLimitSchema.nullable(),
10475
10569
  guestPolicy: guestPolicySchema,
10476
10570
  seatChangeNotification: eventSeatChangeNotificationSchema.optional(),
10571
+ registrationEmail: registrationEmailPresentationSchema.optional(),
10572
+ registrationMasterList: eventRegistrationMasterListSchema.optional(),
10477
10573
  waitlistEnabled: boolean2(),
10478
10574
  waitlistOfferTtlMinutes: number2().int().min(5).max(10080)
10479
10575
  }).refine((value) => !value.rsvpOpensAt || !value.rsvpClosesAt || value.rsvpOpensAt < value.rsvpClosesAt, {
@@ -11360,6 +11456,9 @@ var createPaymentCheckoutInputSchema = object({
11360
11456
  idempotencyKey: uuid2()
11361
11457
  });
11362
11458
  var getMyPaymentInputSchema = object({ eventSlug: eventSlugSchema });
11459
+ var getMyPaymentReceiptInputSchema = getMyPaymentInputSchema.extend({
11460
+ paymentAttemptId: string2().min(1)
11461
+ });
11363
11462
  var listEventPaymentsInputSchema = eventScopeSchema4.extend({
11364
11463
  cursor: string2().min(1).optional(),
11365
11464
  limit: number2().int().min(1).max(100).default(50)
@@ -12339,13 +12438,14 @@ Object.assign(commands, {
12339
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"]),
12340
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"]),
12341
12440
  "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"),
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"),
12343
12442
  conditionalEffects: [
12344
- { effect: "email", whenPayloadField: "seatChangeNotice" }
12443
+ { effect: "email", whenPayloadField: "seatChangeNotice" },
12444
+ { effect: "email", whenPayloadField: "seats" }
12345
12445
  ]
12346
12446
  },
12347
12447
  "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"]),
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"]),
12349
12449
  conditionalEffects: [
12350
12450
  { effect: "email", whenPayloadField: "seatChangeNotice" }
12351
12451
  ]
@@ -12363,7 +12463,7 @@ Object.assign(commands, {
12363
12463
  "event mine list": payloadCommand((api, input) => api.event.listMyEvents.query(input), "List the caller's events (--payload JSON)."),
12364
12464
  "event reminder list": payloadCommand((api, input) => api.event.listReminderSegments.query(input), "List reminder segments (--payload JSON).", ["sensitive-data"]),
12365
12465
  "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"]),
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"]),
12367
12467
  "event page update": payloadCommand((api, input) => api.event.updateSetupStep.mutate({
12368
12468
  ...input,
12369
12469
  section: "DETAILS_PAGE"
@@ -12376,6 +12476,7 @@ Object.assign(commands, {
12376
12476
  Object.assign(commands, {
12377
12477
  "payment event get": payloadCommand((api, input) => api.payment.getEventPayment.query(input), "Get event payment configuration (--payload JSON).", ["financial-write"]),
12378
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"]),
12379
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"]),
12380
12481
  "payment exception list": payloadCommand((api, input) => api.payment.listPaymentExceptions.query(input), "List payment exceptions (--payload JSON).", ["financial-write", "sensitive-data"]),
12381
12482
  "payment exception resolve": payloadCommand((api, input) => api.payment.resolvePaymentException.mutate(input), "Resolve a payment exception (--payload JSON).", ["financial-write", "sensitive-data", "state-change"]),
@@ -12444,7 +12545,7 @@ Object.assign(commands, {
12444
12545
  },
12445
12546
  "event seating draft publish": {
12446
12547
  scope: "event",
12447
- effects: ["state-change"],
12548
+ effects: ["state-change", "email"],
12448
12549
  targetKey: "eventId",
12449
12550
  conditionalEffects: [
12450
12551
  { effect: "email", whenPayloadField: "sendSeatChangeEmail" }
@@ -12456,7 +12557,7 @@ Object.assign(commands, {
12456
12557
  },
12457
12558
  validateInput: (input) => validateSeatingDraftInput(publishSeatingDraftInputSchema, input),
12458
12559
  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)."
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)."
12460
12561
  }
12461
12562
  });
12462
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.2.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",