@camphoric/camphoric-event-import-tools 0.0.0-stage โ 1.2.1
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 +50 -0
- package/CamphoricEventCreator.ts +555 -0
- package/CamphoricFetcher.ts +163 -0
- package/README.md +68 -2
- package/bulkRegistrations.ts +273 -0
- package/dist/CamphoricEventCreator.d.ts +80 -0
- package/dist/CamphoricEventCreator.d.ts.map +1 -0
- package/dist/CamphoricEventCreator.js +384 -0
- package/dist/CamphoricEventCreator.js.map +1 -0
- package/dist/CamphoricFetcher.d.ts +28 -0
- package/dist/CamphoricFetcher.d.ts.map +1 -0
- package/dist/CamphoricFetcher.js +121 -0
- package/dist/CamphoricFetcher.js.map +1 -0
- package/dist/bulkRegistrations.d.ts +59 -0
- package/dist/bulkRegistrations.d.ts.map +1 -0
- package/dist/bulkRegistrations.js +225 -0
- package/dist/bulkRegistrations.js.map +1 -0
- package/dist/e2e.d.ts +55 -0
- package/dist/e2e.d.ts.map +1 -0
- package/dist/e2e.js +99 -0
- package/dist/e2e.js.map +1 -0
- package/dist/e2eImport.d.ts +16 -0
- package/dist/e2eImport.d.ts.map +1 -0
- package/dist/e2eImport.js +38 -0
- package/dist/e2eImport.js.map +1 -0
- package/dist/e2eRegistrationForm.d.ts +71 -0
- package/dist/e2eRegistrationForm.d.ts.map +1 -0
- package/dist/e2eRegistrationForm.js +167 -0
- package/dist/e2eRegistrationForm.js.map +1 -0
- package/dist/e2eServer.d.ts +22 -0
- package/dist/e2eServer.d.ts.map +1 -0
- package/dist/e2eServer.js +93 -0
- package/dist/e2eServer.js.map +1 -0
- package/dist/getAuthInfo.d.ts +19 -0
- package/dist/getAuthInfo.d.ts.map +1 -0
- package/dist/getAuthInfo.js +79 -0
- package/dist/getAuthInfo.js.map +1 -0
- package/dist/organizations.d.ts +29 -0
- package/dist/organizations.d.ts.map +1 -0
- package/dist/organizations.js +107 -0
- package/dist/organizations.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +27 -0
- package/dist/utils.js.map +1 -0
- package/e2e-compose.yml +40 -0
- package/e2e.ts +111 -0
- package/e2eImport.ts +61 -0
- package/e2eRegistrationForm.ts +190 -0
- package/e2eServer.ts +114 -0
- package/getAuthInfo.ts +92 -0
- package/organizations.ts +144 -0
- package/package.json +102 -4
- package/types.ts +103 -0
- package/utils.ts +29 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.2.1](https://github.com/camphoric/camphoric/compare/camphoric-event-import-tools-v1.2.0...camphoric-event-import-tools-v1.2.1) (2026-10-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **data:** keep why the throwaway Camphoric didn't start as the error's cause ([37f8656](https://github.com/camphoric/camphoric/commit/37f865618b8d17c1c724f8474b60fc75cd14b787)), closes [#821](https://github.com/camphoric/camphoric/issues/821)
|
|
9
|
+
* **data:** only import into a local server unless CAMPHORIC_ALLOW_REMOTE=1 ([0d86923](https://github.com/camphoric/camphoric/commit/0d86923ca65837433d8bd1c85e61f7e74220e4c6)), closes [#815](https://github.com/camphoric/camphoric/issues/815)
|
|
10
|
+
* **data:** retry only GET, PUT and DELETE after a reset connection ([0d86923](https://github.com/camphoric/camphoric/commit/0d86923ca65837433d8bd1c85e61f7e74220e4c6))
|
|
11
|
+
* **data:** sign in once per server in the import tools ([470db1e](https://github.com/camphoric/camphoric/commit/470db1e85bae5c8525939dd44caa8c56facd978c)), closes [#800](https://github.com/camphoric/camphoric/issues/800)
|
|
12
|
+
* **data:** sign the import tools' registration posts in with their token ([ad10c5a](https://github.com/camphoric/camphoric/commit/ad10c5aed143758df0fd344a728d5c69f967db65)), closes [#801](https://github.com/camphoric/camphoric/issues/801)
|
|
13
|
+
* **deps:** bump dotenv in /data in the minor-and-patch group ([8432761](https://github.com/camphoric/camphoric/commit/84327618353ecf915461f4d409ad067aeee62da6))
|
|
14
|
+
* **deps:** bump the minor-and-patch group in /data with 8 updates ([28d3e09](https://github.com/camphoric/camphoric/commit/28d3e0972c03cc86e24f0c040c40fe6f428f18fc))
|
|
15
|
+
|
|
16
|
+
## [1.2.0](https://github.com/camphoric/camphoric/compare/camphoric-event-import-tools-v1.1.0...camphoric-event-import-tools-v1.2.0) (2026-10-17)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **data:** count a camper's lodging levels in the e2e page object ([9f7ac3b](https://github.com/camphoric/camphoric/commit/9f7ac3b06c5647ea1b5796756034eb4694bce01d)), closes [#780](https://github.com/camphoric/camphoric/issues/780)
|
|
22
|
+
|
|
23
|
+
## [1.1.0](https://github.com/camphoric/camphoric/compare/camphoric-event-import-tools-v1.0.0...camphoric-event-import-tools-v1.1.0) (2026-10-17)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
* **data:** add e2e scaffolding for testing an event's registration form ([e98f014](https://github.com/camphoric/camphoric/commit/e98f01486f51fa83773c2840bd32734ed3e61e67))
|
|
29
|
+
* **data:** move the event creator and organization loader into import-tools ([c6e8468](https://github.com/camphoric/camphoric/commit/c6e8468a6b2326885a74e24eb036e3c57aa67c1e))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
* **data:** keep reset-data-and-test from importing alongside docker compose up ([7543433](https://github.com/camphoric/camphoric/commit/754343397c8cfdb94f473b0ebd98b7262db61534))
|
|
35
|
+
* **data:** open an e2e event's registration before its sample registrations ([7543433](https://github.com/camphoric/camphoric/commit/754343397c8cfdb94f473b0ebd98b7262db61534))
|
|
36
|
+
* **data:** report an event module's validation errors when it has no event ([c6e8468](https://github.com/camphoric/camphoric/commit/c6e8468a6b2326885a74e24eb036e3c57aa67c1e))
|
|
37
|
+
* **data:** stop a failed import request from ending the process ([7543433](https://github.com/camphoric/camphoric/commit/754343397c8cfdb94f473b0ebd98b7262db61534))
|
|
38
|
+
|
|
39
|
+
## 1.0.0 (2026-10-17)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
* **data:** let each event declare the organization it belongs to ([70853bd](https://github.com/camphoric/camphoric/commit/70853bd5223b0134fd7e259a241c2dc05d22b6c1))
|
|
45
|
+
* **data:** publish the event import spec and tools to GitHub Packages ([d26b182](https://github.com/camphoric/camphoric/commit/d26b182d5fe44cc31dbd1747572a267439053574))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Bug Fixes
|
|
49
|
+
|
|
50
|
+
* **data:** keep the event-import packages' versions independent ([fd96655](https://github.com/camphoric/camphoric/commit/fd96655bf7b707b7771c964a79781879e2a0c4af))
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import test event data
|
|
3
|
+
*
|
|
4
|
+
* Sample use
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* const event = new CamphoricEventCreator({
|
|
8
|
+
* data,
|
|
9
|
+
* sampleRegGenerator
|
|
10
|
+
* overrides,
|
|
11
|
+
* url,
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* event.send();
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Arguments
|
|
18
|
+
* ---------
|
|
19
|
+
*
|
|
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).
|
|
25
|
+
*
|
|
26
|
+
* sampleRegGenerator: a SampleRegGenerator, called with
|
|
27
|
+
* - fetch: a fetch function
|
|
28
|
+
* - results: the data generated so far of the event creation
|
|
29
|
+
* - log: a log function
|
|
30
|
+
* This function should return an array of objects which can be POSTed to the
|
|
31
|
+
* register endpoint.
|
|
32
|
+
*
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { oraPromise } from 'ora';
|
|
37
|
+
import cliProgress from 'cli-progress';
|
|
38
|
+
import lodash from 'lodash';
|
|
39
|
+
|
|
40
|
+
import Fetcher from './CamphoricFetcher.ts';
|
|
41
|
+
import { Organizations, type Organization } from './organizations.ts';
|
|
42
|
+
import type {
|
|
43
|
+
ImportData,
|
|
44
|
+
ImportResults,
|
|
45
|
+
LodgingNode,
|
|
46
|
+
Override,
|
|
47
|
+
SampleRegGenerator,
|
|
48
|
+
SampleRegistration,
|
|
49
|
+
Saved,
|
|
50
|
+
SavedResults,
|
|
51
|
+
} from './types.ts';
|
|
52
|
+
import { formatDate, formatDateTime } from './utils.ts';
|
|
53
|
+
|
|
54
|
+
export interface CamphoricEventCreatorOptions {
|
|
55
|
+
data: ImportData;
|
|
56
|
+
// Checks the data before anything is sent, returning its problems (none
|
|
57
|
+
// when it's valid): pass the spec's validateEventImport.
|
|
58
|
+
validate: (data: unknown) => unknown[];
|
|
59
|
+
sampleRegGenerator?: SampleRegGenerator;
|
|
60
|
+
overrides?: Override[];
|
|
61
|
+
// The organizations already loaded; fetched from the server when not given
|
|
62
|
+
organizations?: Organizations;
|
|
63
|
+
url?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A sample registration, with the server's answer to its last step
|
|
67
|
+
type PostedRegistration = SampleRegistration & { response: Record<string, unknown> };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Requests started ahead of the Promise.all that collects them, pausing
|
|
71
|
+
* between batches so the server keeps up. Each is handled as it starts, so
|
|
72
|
+
* one that fails during a pause isn't an unhandled rejection (which ends the
|
|
73
|
+
* process, skipping the other events and any cleanup); once one has failed,
|
|
74
|
+
* `failed` says to start no more. Promise.all still rejects with its error.
|
|
75
|
+
*/
|
|
76
|
+
export function requestBatch() {
|
|
77
|
+
let failed = false;
|
|
78
|
+
return {
|
|
79
|
+
get failed() { return failed; },
|
|
80
|
+
start<T>(request: Promise<T>): Promise<T> {
|
|
81
|
+
request.catch(() => { failed = true; });
|
|
82
|
+
return request;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default class CamphoricEventCreator extends Fetcher {
|
|
88
|
+
declare data: ImportData;
|
|
89
|
+
sampleRegGenerator?: SampleRegGenerator;
|
|
90
|
+
overrides: Override[];
|
|
91
|
+
results: ImportResults;
|
|
92
|
+
organization?: Promise<Organization | undefined>;
|
|
93
|
+
|
|
94
|
+
constructor({ data, validate, organizations, sampleRegGenerator, overrides = [], url }: CamphoricEventCreatorOptions) {
|
|
95
|
+
super(url);
|
|
96
|
+
|
|
97
|
+
this.data = data;
|
|
98
|
+
|
|
99
|
+
const errors = validate(data);
|
|
100
|
+
|
|
101
|
+
if (errors.length) {
|
|
102
|
+
throw this.error(
|
|
103
|
+
'event data object did not pass JSON schema validation',
|
|
104
|
+
...errors,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.setOrganizations(organizations, data.organization);
|
|
109
|
+
|
|
110
|
+
if (sampleRegGenerator) {
|
|
111
|
+
if (typeof sampleRegGenerator !== 'function') {
|
|
112
|
+
throw new Error('sample registration generator passed should be a function');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.sampleRegGenerator = sampleRegGenerator;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!Array.isArray(overrides)) {
|
|
119
|
+
throw new Error('overrides passed should be an array');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (overrides.find(e => typeof e !== 'function')) {
|
|
123
|
+
throw new Error('all items in overrides be functions');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.overrides = overrides;
|
|
127
|
+
|
|
128
|
+
this.results = {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pathFormat(path: string): string {
|
|
132
|
+
if (path.charAt(path.length - 1) !== '/') {
|
|
133
|
+
path += '/';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return path;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The saved event, which every step after loadEvent() needs
|
|
140
|
+
savedEvent(): Saved {
|
|
141
|
+
if (!this.results.event) {
|
|
142
|
+
throw this.error('the event has not been saved yet');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return this.results.event;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// What has been saved, once the event has
|
|
149
|
+
savedResults(): SavedResults {
|
|
150
|
+
this.savedEvent();
|
|
151
|
+
|
|
152
|
+
return this.results as SavedResults;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async deleteAll(path: string) {
|
|
156
|
+
const event = this.savedEvent();
|
|
157
|
+
const existing: Saved[] = await this.fetch('GET', path)
|
|
158
|
+
.then((all: Saved[]) => all.filter(i => i.event === event.id));
|
|
159
|
+
|
|
160
|
+
await Promise.all(existing.map(
|
|
161
|
+
l => this.fetch('DELETE', `${this.pathFormat(path)}${l.id}/`)
|
|
162
|
+
));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async loadEvent() {
|
|
166
|
+
const org = await this.organization;
|
|
167
|
+
if (!org) {
|
|
168
|
+
throw this.error(`organization '${this.data.organization}' not found`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const events: Saved[] = await this.fetch('GET', '/api/events/');
|
|
172
|
+
|
|
173
|
+
const existingEvent = events.find(event => event.name === this.data.event.name);
|
|
174
|
+
|
|
175
|
+
const emailAccounts: Saved[] = await this.fetch('GET', '/api/emailaccounts/');
|
|
176
|
+
const email = emailAccounts.find(e => e.organization === org.id);
|
|
177
|
+
|
|
178
|
+
// The confirmation email is the event's email template, updated below.
|
|
179
|
+
const {
|
|
180
|
+
confirmation_email_subject: subject,
|
|
181
|
+
confirmation_email_template: body,
|
|
182
|
+
...eventData
|
|
183
|
+
} = this.data.event;
|
|
184
|
+
const event: Record<string, unknown> = {
|
|
185
|
+
organization: org.id,
|
|
186
|
+
...eventData,
|
|
187
|
+
// The event's dates are days; its registration window is instants.
|
|
188
|
+
start: formatDate(eventData.start),
|
|
189
|
+
end: formatDate(eventData.end),
|
|
190
|
+
registration_start: formatDateTime(eventData.registration_start),
|
|
191
|
+
registration_end: formatDateTime(eventData.registration_end),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (existingEvent) {
|
|
195
|
+
event.id = existingEvent.id;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (email) {
|
|
199
|
+
event.email_account = email.id;
|
|
200
|
+
event.confirmation_email_from =
|
|
201
|
+
event.confirmation_email_from || email.account;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const eventResponse: Saved = await this.fetch(
|
|
205
|
+
existingEvent ? 'PUT' : 'POST',
|
|
206
|
+
`/api/events/${existingEvent ? event.id : ''}`,
|
|
207
|
+
event,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
await this.loadEmailTemplate(eventResponse.confirmation_template, subject, body);
|
|
211
|
+
|
|
212
|
+
this.results.event = eventResponse;
|
|
213
|
+
return eventResponse;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// An event's confirmation and a registration type's invitation are email
|
|
217
|
+
// templates, created with them (SPEC DR-45); set the text the data gives.
|
|
218
|
+
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
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async loadLodgings() {
|
|
227
|
+
const event = this.savedEvent();
|
|
228
|
+
const progBar = new cliProgress.SingleBar(
|
|
229
|
+
{
|
|
230
|
+
format: 'importing lodging [{bar}] {percentage}% | {value}/{total}',
|
|
231
|
+
clearOnComplete: true,
|
|
232
|
+
},
|
|
233
|
+
cliProgress.Presets.shades_classic
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// delete existing lodgings associated with this event, so we start clean
|
|
237
|
+
await this.deleteAll('/api/lodgings/');
|
|
238
|
+
|
|
239
|
+
if (!this.data.lodgings) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const lodgingLookup = this.data.lodgings;
|
|
244
|
+
const lodgingEntries = Object.entries(lodgingLookup);
|
|
245
|
+
let completed = 0;
|
|
246
|
+
progBar.start(lodgingEntries.length, 0);
|
|
247
|
+
|
|
248
|
+
const createLodging = async (key: string, lodging: LodgingNode): Promise<unknown> => {
|
|
249
|
+
let data, createdLodging: Saved | undefined;
|
|
250
|
+
try {
|
|
251
|
+
data = {
|
|
252
|
+
event: event.id,
|
|
253
|
+
parent: lodging.parentKey ? lodgingLookup[lodging.parentKey].id : null,
|
|
254
|
+
name: lodging.name,
|
|
255
|
+
children_title: lodging.children_title,
|
|
256
|
+
visible: lodging.visible,
|
|
257
|
+
capacity: lodging.capacity || 0,
|
|
258
|
+
reserved: lodging.reserved || 0,
|
|
259
|
+
sharing_multiplier: lodging.sharing_multiplier || 1,
|
|
260
|
+
notes: lodging.notes || '',
|
|
261
|
+
};
|
|
262
|
+
const saved: Saved = await this.fetch('POST', '/api/lodgings/', data);
|
|
263
|
+
createdLodging = saved;
|
|
264
|
+
|
|
265
|
+
completed += 1;
|
|
266
|
+
progBar.update(completed);
|
|
267
|
+
this.logVerbose(`created lodging ${completed}/${lodgingEntries.length}`);
|
|
268
|
+
|
|
269
|
+
lodgingLookup[key].id = saved.id;
|
|
270
|
+
} catch (e) {
|
|
271
|
+
throw this.error(e, {type: 'lodgingDataAttempted', key, data, createdLodging});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// batch requests so that it doesn't time out
|
|
275
|
+
const promises = [];
|
|
276
|
+
const requests = requestBatch();
|
|
277
|
+
const all = lodgingEntries.filter(([, l]) => l.parentKey === key);
|
|
278
|
+
for (let i = 0; i < all.length && !requests.failed; ++i) {
|
|
279
|
+
promises.push(requests.start(createLodging(...all[i])));
|
|
280
|
+
|
|
281
|
+
if (promises.length % 10 === 0) await this.wait(5000);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return Promise.all(promises);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// recursively add all lodgings starting with root
|
|
288
|
+
await createLodging('root', lodgingLookup.root);
|
|
289
|
+
progBar.stop();
|
|
290
|
+
|
|
291
|
+
this.results.lodging = lodgingLookup;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async loadReports() {
|
|
295
|
+
const event = this.savedEvent();
|
|
296
|
+
this.results.reports = await Promise.all(this.data.reports.map(
|
|
297
|
+
report => this.fetch('POST', '/api/reports/', {
|
|
298
|
+
event: event.id,
|
|
299
|
+
// data/ reports use the browser's variable bundle unless they say otherwise.
|
|
300
|
+
variables_source: 'client',
|
|
301
|
+
...report,
|
|
302
|
+
})
|
|
303
|
+
));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async loadCustomChargeTypes() {
|
|
307
|
+
const { custom_charge_types } = this.data;
|
|
308
|
+
|
|
309
|
+
if (!custom_charge_types || !custom_charge_types.length) return;
|
|
310
|
+
|
|
311
|
+
const event = this.savedEvent();
|
|
312
|
+
|
|
313
|
+
const response: Saved[] = await this.fetch('GET', '/api/customchargetypes/');
|
|
314
|
+
|
|
315
|
+
const existing = response.filter(ctype => ctype.event === event.id);
|
|
316
|
+
|
|
317
|
+
await Promise.all(
|
|
318
|
+
custom_charge_types.map(
|
|
319
|
+
async (customChargeType) => {
|
|
320
|
+
const exists = existing.find(r => r.name === customChargeType.name);
|
|
321
|
+
|
|
322
|
+
return this.fetch(
|
|
323
|
+
exists ? 'PUT' : 'POST',
|
|
324
|
+
`/api/customchargetypes${exists ? `/${exists.id}` : ''}/`,
|
|
325
|
+
{
|
|
326
|
+
...customChargeType,
|
|
327
|
+
...(exists ? { id: exists.id } : {}),
|
|
328
|
+
event: event.id,
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async loadRegTypes() {
|
|
337
|
+
const event = this.savedEvent();
|
|
338
|
+
|
|
339
|
+
const response: Saved[] = await this.fetch('GET', '/api/registrationtypes/');
|
|
340
|
+
|
|
341
|
+
const existingRegTypes = response.filter(regType => regType.event === event.id);
|
|
342
|
+
|
|
343
|
+
await Promise.all(
|
|
344
|
+
this.data.registration_types.map(
|
|
345
|
+
async (regType) => {
|
|
346
|
+
const exists = existingRegTypes.find(r => r.name === regType.name);
|
|
347
|
+
|
|
348
|
+
const {
|
|
349
|
+
invitation_email_subject: subject,
|
|
350
|
+
invitation_email_template: body,
|
|
351
|
+
...typeData
|
|
352
|
+
} = regType;
|
|
353
|
+
|
|
354
|
+
const saved: Saved = await this.fetch(
|
|
355
|
+
exists ? 'PUT' : 'POST',
|
|
356
|
+
`/api/registrationtypes${exists ? `/${exists.id}` : ''}/`,
|
|
357
|
+
{
|
|
358
|
+
...typeData,
|
|
359
|
+
...(exists ? { id: exists.id } : {}),
|
|
360
|
+
event: event.id,
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
await this.loadEmailTemplate(saved.invitation_template, subject, body);
|
|
364
|
+
return saved;
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async loadTestRegs() {
|
|
371
|
+
if (!this.sampleRegGenerator) return [];
|
|
372
|
+
|
|
373
|
+
const token = await this.getAuthToken();
|
|
374
|
+
const registrations = await this.sampleRegGenerator.bind(this)(
|
|
375
|
+
this.fetch.bind(this),
|
|
376
|
+
this.savedResults(),
|
|
377
|
+
this.log.bind(this),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const event = this.savedEvent();
|
|
381
|
+
|
|
382
|
+
const postReg = async (testData: SampleRegistration): Promise<PostedRegistration> => {
|
|
383
|
+
const res = await fetch(`${this.urlBase}/api/events/${event.id}/register`, {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
headers: {
|
|
386
|
+
'Content-Type': 'application/json',
|
|
387
|
+
// Signed in, so the server's throttle on anonymous registration steps
|
|
388
|
+
// (#801) leaves the sample registrations alone.
|
|
389
|
+
'Authorization': `Token ${token}`,
|
|
390
|
+
},
|
|
391
|
+
body: JSON.stringify({
|
|
392
|
+
step: 'registration',
|
|
393
|
+
formData: testData.formData,
|
|
394
|
+
pricingResults: testData.pricingResults,
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const text = await res.text();
|
|
399
|
+
|
|
400
|
+
// A rejected test registration is a real import failure (a broken schema or
|
|
401
|
+
// pricing logic), so fail loudly instead of logging and exiting 0.
|
|
402
|
+
let json;
|
|
403
|
+
try {
|
|
404
|
+
json = JSON.parse(text);
|
|
405
|
+
} catch {
|
|
406
|
+
throw this.error(
|
|
407
|
+
`[reg step] test registration returned non-JSON (${res.status}), see Django logs`,
|
|
408
|
+
{ status: res.status, body: text },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!res.ok) {
|
|
413
|
+
throw this.error(
|
|
414
|
+
`[reg step] test registration rejected (${res.status})`,
|
|
415
|
+
{ type: 'testRegistration', response: json, sent: testData.formData },
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
...testData,
|
|
421
|
+
response: json || {},
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const postPayment = async (testData: PostedRegistration): Promise<PostedRegistration> => {
|
|
426
|
+
// Test registrations pay by check: a PayPal payment needs a payer to
|
|
427
|
+
// approve it in PayPal's window (SPEC ยง7.2). The server works out the
|
|
428
|
+
// amount from the payment option (the event's default when none is given).
|
|
429
|
+
const paymentOption = lodash.get(testData, 'paymentData.paymentOption');
|
|
430
|
+
|
|
431
|
+
const body = {
|
|
432
|
+
step: 'payment',
|
|
433
|
+
registrationUUID: testData.response.registrationUUID,
|
|
434
|
+
paymentType: 'Check',
|
|
435
|
+
...(paymentOption ? { paymentOption } : {}),
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const res = await fetch(`${this.urlBase}/api/events/${event.id}/register`, {
|
|
439
|
+
method: 'POST',
|
|
440
|
+
headers: {
|
|
441
|
+
'Content-Type': 'application/json',
|
|
442
|
+
// Signed in, so the server's throttle on anonymous registration steps
|
|
443
|
+
// (#801) leaves the sample registrations alone.
|
|
444
|
+
'Authorization': `Token ${token}`,
|
|
445
|
+
},
|
|
446
|
+
body: JSON.stringify(body),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const text = await res.text();
|
|
450
|
+
|
|
451
|
+
let json;
|
|
452
|
+
try {
|
|
453
|
+
json = JSON.parse(text);
|
|
454
|
+
} catch {
|
|
455
|
+
throw this.error(
|
|
456
|
+
`[pay step] test payment returned non-JSON (${res.status}), see Django logs`,
|
|
457
|
+
{ status: res.status, body: text },
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!res.ok) {
|
|
462
|
+
throw this.error(
|
|
463
|
+
`[pay step] test payment rejected (${res.status})`,
|
|
464
|
+
{ type: 'testPayment', response: json, sent: body },
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
...testData,
|
|
470
|
+
response: json || {},
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
if (!Array.isArray(registrations)) {
|
|
476
|
+
throw new Error('sample reg generator function returned a non-array');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// get first step responses
|
|
480
|
+
const step1 = [];
|
|
481
|
+
const registering = requestBatch();
|
|
482
|
+
for (let i = 0; i < registrations.length && !registering.failed; ++i) {
|
|
483
|
+
step1.push(registering.start(postReg(registrations[i])));
|
|
484
|
+
|
|
485
|
+
if (i % 10 === 0) await this.wait(1500);
|
|
486
|
+
}
|
|
487
|
+
const responses = await Promise.all(step1);
|
|
488
|
+
|
|
489
|
+
// process payment step
|
|
490
|
+
const step2 = [];
|
|
491
|
+
const paying = requestBatch();
|
|
492
|
+
for (let i = 0; i < responses.length && !paying.failed; ++i) {
|
|
493
|
+
step2.push(paying.start(postPayment(responses[i])));
|
|
494
|
+
|
|
495
|
+
if (i % 10 === 0) await this.wait(1500);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return Promise.all(step2);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async create() {
|
|
502
|
+
this.log(`๐ข Starting Import of ${this.data.event.name}!`);
|
|
503
|
+
|
|
504
|
+
const imp = (label: string, p: Promise<unknown>) => oraPromise(
|
|
505
|
+
p,
|
|
506
|
+
{
|
|
507
|
+
text: `[${this.data.event.name}] import ${label}`,
|
|
508
|
+
spinner: 'clock',
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
await imp('organization', Promise.resolve(this.organization));
|
|
513
|
+
await imp('event', this.loadEvent());
|
|
514
|
+
await imp('custom charge types', this.loadCustomChargeTypes());
|
|
515
|
+
await imp('reports', this.loadReports());
|
|
516
|
+
await imp('registration types', this.loadRegTypes());
|
|
517
|
+
|
|
518
|
+
await this.loadLodgings();
|
|
519
|
+
await imp('lodging', Promise.resolve());
|
|
520
|
+
|
|
521
|
+
// Run the overrides in series in case order matters
|
|
522
|
+
for (let i = 0; i < this.overrides.length; i++) {
|
|
523
|
+
const fn = this.overrides[i].bind(this);
|
|
524
|
+
|
|
525
|
+
await imp(`override[${i}]`, fn(this.fetch.bind(this), this.savedResults(), this.log.bind(this)));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// SKIP_TEST_REGS=1 skips the sample registrations (e.g. where a sample would
|
|
529
|
+
// need a live PayPal sandbox); everything else still imports.
|
|
530
|
+
if (this.sampleRegGenerator && process.env.SKIP_TEST_REGS !== '1') {
|
|
531
|
+
await imp('test registrations', this.loadTestRegs());
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this.log(`๐ Finished Importing ${this.data.event.name}!`);
|
|
535
|
+
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
setOrganizations = async (organizations: Organizations | undefined, organization: string | number) => {
|
|
540
|
+
if (!(organizations instanceof Organizations)) {
|
|
541
|
+
organizations = new Organizations(
|
|
542
|
+
await this.fetch('GET', '/api/organizations/')
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// get organization
|
|
547
|
+
this.organization = new Promise(
|
|
548
|
+
r => r(organizations.byId[organization] || organizations.byName[organization])
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
if (!await this.organization) {
|
|
552
|
+
throw new Error(`organization '${organization}' not found`);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|