@camphoric/camphoric-event-import-tools 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/CamphoricEventCreator.ts +210 -15
  3. package/README.md +59 -5
  4. package/dist/CamphoricEventCreator.d.ts +30 -4
  5. package/dist/CamphoricEventCreator.d.ts.map +1 -1
  6. package/dist/CamphoricEventCreator.js +163 -15
  7. package/dist/CamphoricEventCreator.js.map +1 -1
  8. package/dist/e2e.d.ts +17 -3
  9. package/dist/e2e.d.ts.map +1 -1
  10. package/dist/e2e.js +60 -7
  11. package/dist/e2e.js.map +1 -1
  12. package/dist/e2eAdmin.d.ts +59 -0
  13. package/dist/e2eAdmin.d.ts.map +1 -0
  14. package/dist/e2eAdmin.js +103 -0
  15. package/dist/e2eAdmin.js.map +1 -0
  16. package/dist/e2eImport.d.ts +1 -0
  17. package/dist/e2eImport.d.ts.map +1 -1
  18. package/dist/e2eImport.js +8 -4
  19. package/dist/e2eImport.js.map +1 -1
  20. package/dist/e2ePayPal.d.ts +34 -0
  21. package/dist/e2ePayPal.d.ts.map +1 -0
  22. package/dist/e2ePayPal.js +104 -0
  23. package/dist/e2ePayPal.js.map +1 -0
  24. package/dist/e2eRegistrationForm.d.ts +42 -0
  25. package/dist/e2eRegistrationForm.d.ts.map +1 -1
  26. package/dist/e2eRegistrationForm.js +99 -0
  27. package/dist/e2eRegistrationForm.js.map +1 -1
  28. package/dist/e2eServer.d.ts +2 -0
  29. package/dist/e2eServer.d.ts.map +1 -1
  30. package/dist/e2eServer.js +14 -1
  31. package/dist/e2eServer.js.map +1 -1
  32. package/dist/types.d.ts +21 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/e2e-compose.yml +38 -3
  35. package/e2e.ts +67 -9
  36. package/e2eAdmin.ts +174 -0
  37. package/e2eFakePayPal.py +112 -0
  38. package/e2eImport.ts +13 -2
  39. package/e2ePayPal.ts +129 -0
  40. package/e2eRegistrationForm.ts +108 -0
  41. package/e2eServer.ts +19 -1
  42. package/package.json +4 -1
  43. package/types.ts +21 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.2.0](https://github.com/camphoric/camphoric/compare/camphoric-event-import-tools-v2.1.0...camphoric-event-import-tools-v2.2.0) (2026-11-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * **data:** add and remove list items and campers in the e2e page object (list, removeCamper) ([57da044](https://github.com/camphoric/camphoric/commit/57da044c1eee79d92a6811572b9b8f012c2b79ef))
9
+ * **data:** create invitations in the e2e harness (admin.invite) ([57da044](https://github.com/camphoric/camphoric/commit/57da044c1eee79d92a6811572b9b8f012c2b79ef))
10
+ * **data:** raise the e2e server's registration throttles ([57da044](https://github.com/camphoric/camphoric/commit/57da044c1eee79d92a6811572b9b8f012c2b79ef))
11
+ * **data:** read the email the e2e server sent (admin.emails) ([57da044](https://github.com/camphoric/camphoric/commit/57da044c1eee79d92a6811572b9b8f012c2b79ef))
12
+ * **data:** read the payment step's totals in the e2e page object (paymentTotal) ([57da044](https://github.com/camphoric/camphoric/commit/57da044c1eee79d92a6811572b9b8f012c2b79ef))
13
+ * **data:** take online payments in e2e tests through a fake PayPal (onlinePayment, startOnlinePayment, payPal.order) ([57da044](https://github.com/camphoric/camphoric/commit/57da044c1eee79d92a6811572b9b8f012c2b79ef))
14
+
15
+ ## [2.1.0](https://github.com/camphoric/camphoric/compare/camphoric-event-import-tools-v2.0.0...camphoric-event-import-tools-v2.1.0) (2026-11-06)
16
+
17
+
18
+ ### Features
19
+
20
+ * **data:** import group emails, promo codes and the invoice email ([03e7ab5](https://github.com/camphoric/camphoric/commit/03e7ab51b9ca9d7283f80facddd38ee623c96645)), closes [#970](https://github.com/camphoric/camphoric/issues/970)
21
+
3
22
  ## [2.0.0](https://github.com/camphoric/camphoric/compare/camphoric-event-import-tools-v1.2.1...camphoric-event-import-tools-v2.0.0) (2026-11-05)
4
23
 
5
24
 
@@ -18,10 +18,12 @@
18
18
  * ---------
19
19
  *
20
20
  * data: an event import object (the spec's EventImport), checked first with
21
- * `validate`, the spec's validateEventImport. Every field of `data.event` is sent to the
22
- * events API as-is, including optional ones such as
23
- * `registration_error_messages` (custom validation messages for the
24
- * registration form).
21
+ * `validate`, the spec's validateEventImport, and then for names it refers to
22
+ * (a promo code's excluded registration types must be the event's). Every
23
+ * field of `data.event` is sent to the events API as-is, including optional
24
+ * ones such as `registration_error_messages` (custom validation messages for
25
+ * the registration form), except its emails' fields, which are set on the
26
+ * event's confirmation and invoice email templates.
25
27
  *
26
28
  * sampleRegGenerator: a SampleRegGenerator, called with
27
29
  * - fetch: a fetch function
@@ -66,6 +68,61 @@ export interface CamphoricEventCreatorOptions {
66
68
  // A sample registration, with the server's answer to its last step
67
69
  type PostedRegistration = SampleRegistration & { response: Record<string, unknown> };
68
70
 
71
+ // An email template's fields the import sets: undefined leaves one as it is
72
+ type EmailTemplateFields = Record<string, string | undefined>;
73
+
74
+ // One template's result from GET /api/events/<id>/templates/check
75
+ interface TemplateCheckResult {
76
+ kind: string;
77
+ label: string;
78
+ diagnostics: { severity: string, message: string, line?: number | null, field?: string | null }[];
79
+ }
80
+
81
+ // A group email's fields, as a new template has them: one the data leaves out
82
+ // is set to this, so a template matched by name ends up as the data declares it.
83
+ const GROUP_EMAIL_DEFAULTS = {
84
+ from_email: '',
85
+ reply_to: '',
86
+ recipient_source: 'registrations',
87
+ filter: {},
88
+ filter_expression: '',
89
+ address_expression: '',
90
+ name_expression: '',
91
+ recipient_list: '',
92
+ include_incomplete: false,
93
+ };
94
+
95
+ // Likewise for a promo code
96
+ const PROMO_CODE_DEFAULTS = {
97
+ scope: 'registration',
98
+ enabled: true,
99
+ expiration_date: null,
100
+ excluded_registration_types: [],
101
+ excludes_uninvited: false,
102
+ };
103
+
104
+ /**
105
+ * What the import data names that it doesn't hold: each promo code's excluded
106
+ * registration types must be among the event's. An empty list when there are
107
+ * none.
108
+ */
109
+ export function referenceProblems(data: ImportData): string[] {
110
+ const typeNames = new Set(data.registration_types.map(t => t.name));
111
+ return (data.promo_codes ?? []).flatMap(promo => (promo.excluded_registration_types ?? [])
112
+ .filter(name => !typeNames.has(name))
113
+ .map(name => `promo code '${promo.code}' excludes registration type '${name}', which the event doesn't have`));
114
+ }
115
+
116
+ /** The problems a template check found that are errors, by template. */
117
+ export function templateErrors(results: TemplateCheckResult[]): string[] {
118
+ return results.flatMap(result => result.diagnostics
119
+ .filter(d => d.severity === 'error')
120
+ .map(d => {
121
+ const where = [d.field, d.line ? `line ${d.line}` : ''].filter(Boolean).join(', ');
122
+ return `${result.label}${where ? ` (${where})` : ''}: ${d.message}`;
123
+ }));
124
+ }
125
+
69
126
  /**
70
127
  * Requests started ahead of the Promise.all that collects them, pausing
71
128
  * between batches so the server keeps up. Each is handled as it starts, so
@@ -105,6 +162,12 @@ export default class CamphoricEventCreator extends Fetcher {
105
162
  );
106
163
  }
107
164
 
165
+ const references = referenceProblems(data);
166
+
167
+ if (references.length) {
168
+ throw this.error('event data object names what it does not hold', ...references);
169
+ }
170
+
108
171
  this.setOrganizations(organizations, data.organization);
109
172
 
110
173
  if (sampleRegGenerator) {
@@ -175,10 +238,16 @@ export default class CamphoricEventCreator extends Fetcher {
175
238
  const emailAccounts: Saved[] = await this.fetch('GET', '/api/emailaccounts/');
176
239
  const email = emailAccounts.find(e => e.organization === org.id);
177
240
 
178
- // The confirmation email is the event's email template, updated below.
241
+ // The confirmation and invoice emails are the event's email templates,
242
+ // updated below.
179
243
  const {
180
- confirmation_email_subject: subject,
181
- confirmation_email_template: body,
244
+ confirmation_email_subject,
245
+ confirmation_email_template,
246
+ confirmation_email_reply_to,
247
+ invoice_email_subject,
248
+ invoice_email_template,
249
+ invoice_email_from,
250
+ invoice_email_reply_to,
182
251
  ...eventData
183
252
  } = this.data.event;
184
253
  const event: Record<string, unknown> = {
@@ -207,7 +276,17 @@ export default class CamphoricEventCreator extends Fetcher {
207
276
  event,
208
277
  );
209
278
 
210
- await this.loadEmailTemplate(eventResponse.confirmation_template, subject, body);
279
+ await this.updateEmailTemplate(eventResponse.confirmation_template, {
280
+ subject: confirmation_email_subject,
281
+ body: confirmation_email_template,
282
+ reply_to: confirmation_email_reply_to,
283
+ });
284
+ await this.updateEmailTemplate(eventResponse.invoice_template, {
285
+ subject: invoice_email_subject,
286
+ body: invoice_email_template,
287
+ from_email: invoice_email_from,
288
+ reply_to: invoice_email_reply_to,
289
+ });
211
290
 
212
291
  this.results.event = eventResponse;
213
292
  return eventResponse;
@@ -216,11 +295,15 @@ export default class CamphoricEventCreator extends Fetcher {
216
295
  // An event's confirmation and a registration type's invitation are email
217
296
  // templates, created with them (SPEC DR-45); set the text the data gives.
218
297
  async loadEmailTemplate(templateId: unknown, subject?: string, body?: string) {
219
- if (!templateId || (subject === undefined && body === undefined)) return;
220
- await this.fetch('PATCH', `/api/emailtemplates/${templateId}/`, {
221
- ...(subject !== undefined ? { subject } : {}),
222
- ...(body !== undefined ? { body } : {}),
223
- });
298
+ await this.updateEmailTemplate(templateId, { subject, body });
299
+ }
300
+
301
+ // Set the fields of an email template created with its event or
302
+ // registration type that the data gives (an undefined one is left as is).
303
+ async updateEmailTemplate(templateId: unknown, fields: EmailTemplateFields) {
304
+ const given = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
305
+ if (!templateId || !Object.keys(given).length) return;
306
+ await this.fetch('PATCH', `/api/emailtemplates/${templateId}/`, given);
224
307
  }
225
308
 
226
309
  async loadLodgings() {
@@ -258,6 +341,7 @@ export default class CamphoricEventCreator extends Fetcher {
258
341
  reserved: lodging.reserved || 0,
259
342
  sharing_multiplier: lodging.sharing_multiplier || 1,
260
343
  notes: lodging.notes || '',
344
+ availability: lodging.availability || 'auto',
261
345
  };
262
346
  const saved: Saved = await this.fetch('POST', '/api/lodgings/', data);
263
347
  createdLodging = saved;
@@ -340,7 +424,7 @@ export default class CamphoricEventCreator extends Fetcher {
340
424
 
341
425
  const existingRegTypes = response.filter(regType => regType.event === event.id);
342
426
 
343
- await Promise.all(
427
+ this.results.registration_types = await Promise.all(
344
428
  this.data.registration_types.map(
345
429
  async (regType) => {
346
430
  const exists = existingRegTypes.find(r => r.name === regType.name);
@@ -348,6 +432,8 @@ export default class CamphoricEventCreator extends Fetcher {
348
432
  const {
349
433
  invitation_email_subject: subject,
350
434
  invitation_email_template: body,
435
+ invitation_email_from: from_email,
436
+ invitation_email_reply_to: reply_to,
351
437
  ...typeData
352
438
  } = regType;
353
439
 
@@ -360,13 +446,117 @@ export default class CamphoricEventCreator extends Fetcher {
360
446
  event: event.id,
361
447
  },
362
448
  );
363
- await this.loadEmailTemplate(saved.invitation_template, subject, body);
449
+ await this.updateEmailTemplate(saved.invitation_template, { subject, body, from_email, reply_to });
364
450
  return saved;
365
451
  }
366
452
  )
367
453
  );
368
454
  }
369
455
 
456
+ // The event's group emails (SPEC §8.9): each one the data declares is
457
+ // created, or, when the event has one by its name, updated to match.
458
+ // Others the event has are left alone.
459
+ async loadGroupEmails() {
460
+ const { email_templates } = this.data;
461
+
462
+ if (!email_templates || !email_templates.length) return;
463
+
464
+ const event = this.savedEvent();
465
+ const all: Saved[] = await this.fetch('GET', '/api/emailtemplates/');
466
+ const existing = all.filter(t => t.event === event.id && t.purpose === 'group');
467
+
468
+ this.results.email_templates = await Promise.all(email_templates.map((template) => {
469
+ const exists = existing.find(t => t.name === template.name);
470
+
471
+ return this.fetch(
472
+ exists ? 'PUT' : 'POST',
473
+ `/api/emailtemplates/${exists ? `${exists.id}/` : ''}`,
474
+ { ...GROUP_EMAIL_DEFAULTS, ...template, purpose: 'group', event: event.id },
475
+ ) as Promise<Saved>;
476
+ }));
477
+ }
478
+
479
+ // The event's promo codes (SPEC DR-67): each one the data declares is
480
+ // created, or, when the event has a live one with its code (ignoring case),
481
+ // updated to match. Its excluded registration types are given by name, so
482
+ // this runs once they're saved. Others the event has are left alone.
483
+ async loadPromoCodes() {
484
+ const { promo_codes } = this.data;
485
+
486
+ if (!promo_codes || !promo_codes.length) return;
487
+
488
+ const event = this.savedEvent();
489
+ const regTypes = this.results.registration_types ?? [];
490
+ // Every code is worked out before any is sent, so a problem sends none.
491
+ const bodies = promo_codes.map((promo) => {
492
+ const excluded = (promo.excluded_registration_types ?? []).map((name) => {
493
+ const regType = regTypes.find(t => t.name === name);
494
+ if (!regType) {
495
+ throw this.error(`promo code '${promo.code}' excludes registration type '${name}', which was not imported`);
496
+ }
497
+ return regType.id;
498
+ });
499
+
500
+ // An expiry that can't be read must not save as "never expires"
501
+ const expiration = formatDateTime(promo.expiration_date);
502
+ if (promo.expiration_date !== undefined && expiration === undefined) {
503
+ throw this.error(`promo code '${promo.code}' has an expiration_date that can't be read: '${promo.expiration_date}'`);
504
+ }
505
+
506
+ return {
507
+ ...PROMO_CODE_DEFAULTS,
508
+ ...promo,
509
+ expiration_date: expiration ?? null,
510
+ excluded_registration_types: excluded,
511
+ event: event.id,
512
+ };
513
+ });
514
+
515
+ const all: Saved[] = await this.fetch('GET', '/api/promocodes/');
516
+ const existing = all.filter(p => p.event === event.id);
517
+ const sameCode = (a: unknown, b: string) => String(a).trim().toLowerCase() === b.trim().toLowerCase();
518
+
519
+ this.results.promo_codes = await Promise.all(bodies.map((body) => {
520
+ const exists = existing.find(p => sameCode(p.code, body.code));
521
+
522
+ return this.fetch(
523
+ exists ? 'PUT' : 'POST',
524
+ `/api/promocodes/${exists ? `${exists.id}/` : ''}`,
525
+ body,
526
+ ) as Promise<Saved>;
527
+ }));
528
+ }
529
+
530
+ // Check every template of the event through the server (SPEC §9.3), as
531
+ // `manage.py check_templates` does: each is rendered against the event's
532
+ // registrations (or, with none, parsed), so one that won't render fails the
533
+ // import rather than its first send.
534
+ async checkTemplates() {
535
+ const event = this.savedEvent();
536
+ const token = await this.getAuthToken();
537
+ // (Not this.fetch, which ends every path with a slash: this one has none.)
538
+ const res = await fetch(`${this.urlBase}/api/events/${event.id}/templates/check`, {
539
+ headers: { 'Authorization': `Token ${token}` },
540
+ });
541
+ const text = await res.text();
542
+
543
+ let json;
544
+ try {
545
+ json = JSON.parse(text);
546
+ } catch {
547
+ throw this.error(`template check returned non-JSON (${res.status})`, { status: res.status, body: text });
548
+ }
549
+
550
+ if (!res.ok) {
551
+ throw this.error(`template check failed (${res.status})`, { status: res.status, response: json });
552
+ }
553
+
554
+ const errors = templateErrors(json.results ?? []);
555
+ if (errors.length) {
556
+ throw this.error(`${errors.length} template problem(s): ${errors.join('; ')}`, ...errors);
557
+ }
558
+ }
559
+
370
560
  async loadTestRegs() {
371
561
  if (!this.sampleRegGenerator) return [];
372
562
 
@@ -517,6 +707,8 @@ export default class CamphoricEventCreator extends Fetcher {
517
707
  await imp('custom charge types', this.loadCustomChargeTypes());
518
708
  await imp('reports', this.loadReports());
519
709
  await imp('registration types', this.loadRegTypes());
710
+ await imp('group emails', this.loadGroupEmails());
711
+ await imp('promo codes', this.loadPromoCodes());
520
712
 
521
713
  await this.loadLodgings();
522
714
  await imp('lodging', Promise.resolve());
@@ -534,6 +726,9 @@ export default class CamphoricEventCreator extends Fetcher {
534
726
  await imp('test registrations', this.loadTestRegs());
535
727
  }
536
728
 
729
+ // After the sample registrations, so the templates render against them
730
+ await imp('template check', this.checkTemplates());
731
+
537
732
  this.log(`🎉 Finished Importing ${this.data.event.name}!`);
538
733
 
539
734
  return true;
package/README.md CHANGED
@@ -17,7 +17,12 @@ importer:
17
17
  the same check.
18
18
  - `@camphoric/camphoric-event-import-tools/creator`: `CamphoricEventCreator`, which imports an
19
19
  event module into a Camphoric server. Pass it the spec's `validateEventImport` as `validate`;
20
- the tools don't depend on any particular spec version.
20
+ the tools don't depend on any particular spec version. Group emails are matched to the event's
21
+ saved ones by name and promo codes by code (ignoring case), so importing again updates them
22
+ rather than adding copies; ones the event has that the data doesn't name are left alone. Once
23
+ everything (and any sample registrations) is in, the server checks the event's templates
24
+ (`GET /api/events/<id>/templates/check`, as `manage.py check_templates` does), and a template
25
+ with an error fails the import, rather than its first send.
21
26
  - `@camphoric/camphoric-event-import-tools/organizations`: `organizationLoader()` (loads each
22
27
  organization once, creating it and its email account when missing) and `Organizations`.
23
28
  - `@camphoric/camphoric-event-import-tools/e2e`: Playwright scaffolding for testing an event's
@@ -42,7 +47,11 @@ importer:
42
47
  - **The server:** `setUpEvent()` starts a throwaway Camphoric from the release image, pulled
43
48
  fresh each run (`CAMPHORIC_IMAGE_TAG` picks the tag, default `latest`;
44
49
  `CAMPHORIC_E2E_IMAGE` names a locally built image instead), imports the event, and removes the
45
- server when the tests end.
50
+ server when the tests end. Its registration throttles (registering, saving drafts, opening an
51
+ invitation, checking a promo code) allow `1000/minute` from one address, so a suite isn't told
52
+ to wait (`CAMPHORIC_E2E_REGISTER_RATE`, `_REGISTRATION_DRAFT_RATE`, `_INVITATION_LOOKUP_RATE`
53
+ and `_PROMO_CODE_CHECK_RATE` change them; a real server's are 10 to 30 a minute). It sends email as it's queued, to its console, keeping what it sent in its outbox
54
+ (see `admin.emails()` below).
46
55
  - **An existing server:** `CAMPHORIC_E2E_SERVER_URL`, with `CAMPHORIC_E2E_SERVER_USERNAME` /
47
56
  `_PASSWORD` for its admin, makes it import into that server instead. Not `CAMPHORIC_URL`, so
48
57
  tests never import into a development server by accident. One that isn't local also needs
@@ -51,9 +60,54 @@ importer:
51
60
  can be tested whatever its dates say (`openRegistration: false` keeps them).
52
61
  - **One shared event:** every test, on desktop and mobile Chrome, shares the one imported event.
53
62
  A registration one test completes stays, taking lodging capacity the others see.
54
- - **What tests use:** a `test` with `registration` (a `RegistrationForm` page object: fields by
55
- label, campers, lodging, the price ticker, submitting and paying) and `eventId` fixtures, and
56
- `expect`.
63
+ - **What tests use:** a `test` with these fixtures, and `expect`:
64
+ - `registration`: a `RegistrationForm` page object (fields by label, campers, lodging, lists,
65
+ the price ticker, submitting and paying);
66
+ - `admin`: a `CamphoricAdmin`, what an organizer does, through the admin API as the admin the
67
+ event was imported as (invitations, and the email sent);
68
+ - `payPal`: a `FakePayPal`, the orders the server asked the throwaway server's fake PayPal for;
69
+ - `eventId`: the imported event's id.
70
+ - **Campers and lists:** `addCamper()` adds one and `removeCamper(n)` removes the nth (from 1;
71
+ the ones after it move up). A list question (an array of objects in the schema, such as a
72
+ camper's parking passes) is `list(title)`, on the form or a camper (a camper's list through
73
+ `camper(n)`: on the form, every camper's list has that title): `items()` (to count),
74
+ `item(n)` (its questions), `add()` (returns the new item's questions) and `remove(n)`:
75
+
76
+ ```ts
77
+ const passes = registration.camper(1).list('Parking Passes');
78
+ const pass = await passes.add();
79
+ await pass.select('Vehicle Type', 'Regular car');
80
+ await passes.remove(1);
81
+ await expect(passes.items()).toHaveCount(0);
82
+ ```
83
+ - **The payment step:** `choosePaymentOption(title)` picks a payment option, and
84
+ `paymentTotal()` is what it costs as the step shows it, by check, or with `'online'`, with
85
+ the event's e-payment handling. `payByCheck()` completes the registration.
86
+ - **Paying online:** `setUpEvent({ ..., onlinePayment: true })` imports the event with PayPal
87
+ enabled (a client id only the fakes accept), so the payment step offers PayPal's buttons and
88
+ adds the handling. The throwaway server's PayPal API is then a fake (`e2eFakePayPal.py`, run
89
+ on the app image and mounted from this package, so Docker must see its files), and the browser
90
+ gets a fake of PayPal's script. Neither is there without `onlinePayment`, or with an existing
91
+ server, whose own PayPal would refuse the client id: `onlinePayment` needs the throwaway
92
+ server. `startOnlinePayment()` (or `('Card')`) presses a
93
+ PayPal button: the registration is completed and the server creates its PayPal order, but the
94
+ payer never approves, so nothing is paid. `payPal.order(id)` is what the server asked for,
95
+ its amount as sent:
96
+
97
+ ```ts
98
+ const online = await registration.paymentTotal('online');
99
+ const order = await payPal.order(await registration.startOnlinePayment());
100
+ expect(order.amount).toBe(online.toFixed(2)); // to the cent, as PayPal takes it
101
+ ```
102
+ - **Invitations:** `admin.invite(type)` creates an invitation to register as the registration
103
+ type with that `name`, for a new `@example.com` address (or `{ email, name }`), as the
104
+ admin's "Invite" does, and returns its `code`, `email` and `registerUrl` (the form with the
105
+ invitation, to `page.goto()`). `{ send: true }` emails it too.
106
+ - **Sent email:** `admin.emails({ to, kind })` is the email the server sent about the event
107
+ (from its outbox, newest first): each one's `kind` (`confirmation`, `invitation`, …),
108
+ `status`, `subject`, `text` and `html`. Email is recorded before the request that sends it
109
+ answers, so after a registration's last step it's there; `to` matches the address in any
110
+ case. It works with an existing server too.
57
111
  - **Lodging:** a camper's `chooseLodging(...names)` picks a name at each level and returns the
58
112
  choices each level offered; `lodgingLevels()` is the lodging dropdowns shown now, one per
59
113
  level. A lodging offers nothing below it when no dropdown appears under it: after
@@ -18,10 +18,12 @@
18
18
  * ---------
19
19
  *
20
20
  * data: an event import object (the spec's EventImport), checked first with
21
- * `validate`, the spec's validateEventImport. Every field of `data.event` is sent to the
22
- * events API as-is, including optional ones such as
23
- * `registration_error_messages` (custom validation messages for the
24
- * registration form).
21
+ * `validate`, the spec's validateEventImport, and then for names it refers to
22
+ * (a promo code's excluded registration types must be the event's). Every
23
+ * field of `data.event` is sent to the events API as-is, including optional
24
+ * ones such as `registration_error_messages` (custom validation messages for
25
+ * the registration form), except its emails' fields, which are set on the
26
+ * event's confirmation and invoice email templates.
25
27
  *
26
28
  * sampleRegGenerator: a SampleRegGenerator, called with
27
29
  * - fetch: a fetch function
@@ -43,6 +45,25 @@ export interface CamphoricEventCreatorOptions {
43
45
  organizations?: Organizations;
44
46
  url?: string;
45
47
  }
48
+ type EmailTemplateFields = Record<string, string | undefined>;
49
+ interface TemplateCheckResult {
50
+ kind: string;
51
+ label: string;
52
+ diagnostics: {
53
+ severity: string;
54
+ message: string;
55
+ line?: number | null;
56
+ field?: string | null;
57
+ }[];
58
+ }
59
+ /**
60
+ * What the import data names that it doesn't hold: each promo code's excluded
61
+ * registration types must be among the event's. An empty list when there are
62
+ * none.
63
+ */
64
+ export declare function referenceProblems(data: ImportData): string[];
65
+ /** The problems a template check found that are errors, by template. */
66
+ export declare function templateErrors(results: TemplateCheckResult[]): string[];
46
67
  /**
47
68
  * Requests started ahead of the Promise.all that collects them, pausing
48
69
  * between batches so the server keeps up. Each is handled as it starts, so
@@ -67,14 +88,19 @@ export default class CamphoricEventCreator extends Fetcher {
67
88
  deleteAll(path: string): Promise<void>;
68
89
  loadEvent(): Promise<Saved>;
69
90
  loadEmailTemplate(templateId: unknown, subject?: string, body?: string): Promise<void>;
91
+ updateEmailTemplate(templateId: unknown, fields: EmailTemplateFields): Promise<void>;
70
92
  loadLodgings(): Promise<void>;
71
93
  loadReports(): Promise<void>;
72
94
  loadCustomChargeTypes(): Promise<void>;
73
95
  loadRegTypes(): Promise<void>;
96
+ loadGroupEmails(): Promise<void>;
97
+ loadPromoCodes(): Promise<void>;
98
+ checkTemplates(): Promise<void>;
74
99
  loadTestRegs(): Promise<(SampleRegistration & {
75
100
  response: Record<string, unknown>;
76
101
  })[]>;
77
102
  create(): Promise<boolean>;
78
103
  setOrganizations: (organizations: Organizations | undefined, organization: string | number) => Promise<void>;
79
104
  }
105
+ export {};
80
106
  //# sourceMappingURL=CamphoricEventCreator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CamphoricEventCreator.d.ts","sourceRoot":"","sources":["../CamphoricEventCreator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAMH,OAAO,OAAO,MAAM,uBAAuB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAEb,QAAQ,EACR,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,EACL,YAAY,EACb,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,UAAU,CAAC;IAGjB,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,EAAE,CAAC;IACvC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IAEvB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAKD;;;;;;GAMG;AACH,wBAAgB,YAAY;;UAIlB,CAAC,WAAW,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;EAK5C;AAED,MAAM,CAAC,OAAO,OAAO,qBAAsB,SAAQ,OAAO;IAChD,IAAI,EAAE,UAAU,CAAC;IACzB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;gBAErC,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,kBAAkB,EAAE,SAAc,EAAE,GAAG,EAAE,EAAE,4BAA4B;IAqCpH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAShC,UAAU,IAAI,KAAK;IASnB,YAAY,IAAI,YAAY;IAMtB,SAAS,CAAC,IAAI,EAAE,MAAM;IAUtB,SAAS;IAqDT,iBAAiB,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;IAQtE,YAAY;IAoEZ,WAAW;IAYX,qBAAqB;IA8BrB,YAAY;IAkCZ,YAAY;kBA/SuC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;IAqb1E,MAAM;IAsCZ,gBAAgB,GAAU,eAAe,aAAa,GAAG,SAAS,EAAE,cAAc,MAAM,GAAG,MAAM,mBAe/F;CACH"}
1
+ {"version":3,"file":"CamphoricEventCreator.d.ts","sourceRoot":"","sources":["../CamphoricEventCreator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAMH,OAAO,OAAO,MAAM,uBAAuB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAEb,QAAQ,EACR,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,EACL,YAAY,EACb,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,UAAU,CAAC;IAGjB,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,EAAE,CAAC;IACvC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IAEvB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAMD,KAAK,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAG9D,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;CACnG;AAyBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,CAK5D;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,MAAM,EAAE,CAOvE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY;;UAIlB,CAAC,WAAW,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;EAK5C;AAED,MAAM,CAAC,OAAO,OAAO,qBAAsB,SAAQ,OAAO;IAChD,IAAI,EAAE,UAAU,CAAC;IACzB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;gBAErC,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,EAAE,kBAAkB,EAAE,SAAc,EAAE,GAAG,EAAE,EAAE,4BAA4B;IA2CpH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAShC,UAAU,IAAI,KAAK;IASnB,YAAY,IAAI,YAAY;IAMtB,SAAS,CAAC,IAAI,EAAE,MAAM;IAUtB,SAAS;IAqET,iBAAiB,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;IAMtE,mBAAmB,CAAC,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB;IAMpE,YAAY;IAqEZ,WAAW;IAYX,qBAAqB;IA8BrB,YAAY;IAuCZ,eAAe;IAwBf,cAAc;IAmDd,cAAc;IA0Bd,YAAY;kBA3euC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;IAinB1E,MAAM;IA2CZ,gBAAgB,GAAU,eAAe,aAAa,GAAG,SAAS,EAAE,cAAc,MAAM,GAAG,MAAM,mBAe/F;CACH"}