@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.
- package/CHANGELOG.md +19 -0
- package/CamphoricEventCreator.ts +210 -15
- package/README.md +59 -5
- package/dist/CamphoricEventCreator.d.ts +30 -4
- package/dist/CamphoricEventCreator.d.ts.map +1 -1
- package/dist/CamphoricEventCreator.js +163 -15
- package/dist/CamphoricEventCreator.js.map +1 -1
- package/dist/e2e.d.ts +17 -3
- package/dist/e2e.d.ts.map +1 -1
- package/dist/e2e.js +60 -7
- package/dist/e2e.js.map +1 -1
- package/dist/e2eAdmin.d.ts +59 -0
- package/dist/e2eAdmin.d.ts.map +1 -0
- package/dist/e2eAdmin.js +103 -0
- package/dist/e2eAdmin.js.map +1 -0
- package/dist/e2eImport.d.ts +1 -0
- package/dist/e2eImport.d.ts.map +1 -1
- package/dist/e2eImport.js +8 -4
- package/dist/e2eImport.js.map +1 -1
- package/dist/e2ePayPal.d.ts +34 -0
- package/dist/e2ePayPal.d.ts.map +1 -0
- package/dist/e2ePayPal.js +104 -0
- package/dist/e2ePayPal.js.map +1 -0
- package/dist/e2eRegistrationForm.d.ts +42 -0
- package/dist/e2eRegistrationForm.d.ts.map +1 -1
- package/dist/e2eRegistrationForm.js +99 -0
- package/dist/e2eRegistrationForm.js.map +1 -1
- package/dist/e2eServer.d.ts +2 -0
- package/dist/e2eServer.d.ts.map +1 -1
- package/dist/e2eServer.js +14 -1
- package/dist/e2eServer.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/e2e-compose.yml +38 -3
- package/e2e.ts +67 -9
- package/e2eAdmin.ts +174 -0
- package/e2eFakePayPal.py +112 -0
- package/e2eImport.ts +13 -2
- package/e2ePayPal.ts +129 -0
- package/e2eRegistrationForm.ts +108 -0
- package/e2eServer.ts +19 -1
- package/package.json +4 -1
- 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
|
|
package/CamphoricEventCreator.ts
CHANGED
|
@@ -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
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
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
|
|
241
|
+
// The confirmation and invoice emails are the event's email templates,
|
|
242
|
+
// updated below.
|
|
179
243
|
const {
|
|
180
|
-
confirmation_email_subject
|
|
181
|
-
confirmation_email_template
|
|
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.
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
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
|
|
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"}
|