@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.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
912
|
-
|
|
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
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
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
|
-
//
|
|
9946
|
-
var
|
|
9947
|
-
|
|
9948
|
-
|
|
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
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
+
}
|