@unchainedshop/core-enrollments 1.1.3

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 (46) hide show
  1. package/.npm/package/README +7 -0
  2. package/.npm/package/npm-shrinkwrap.json +205 -0
  3. package/.versions +55 -0
  4. package/README.md +5 -0
  5. package/lib/db/EnrollmentStatus.d.ts +6 -0
  6. package/lib/db/EnrollmentStatus.js +8 -0
  7. package/lib/db/EnrollmentStatus.js.map +1 -0
  8. package/lib/db/EnrollmentsCollection.d.ts +3 -0
  9. package/lib/db/EnrollmentsCollection.js +34 -0
  10. package/lib/db/EnrollmentsCollection.js.map +1 -0
  11. package/lib/db/EnrollmentsSchema.d.ts +2 -0
  12. package/lib/db/EnrollmentsSchema.js +46 -0
  13. package/lib/db/EnrollmentsSchema.js.map +1 -0
  14. package/lib/director/EnrollmentAdapter.d.ts +2 -0
  15. package/lib/director/EnrollmentAdapter.js +67 -0
  16. package/lib/director/EnrollmentAdapter.js.map +1 -0
  17. package/lib/director/EnrollmentDirector.d.ts +2 -0
  18. package/lib/director/EnrollmentDirector.js +46 -0
  19. package/lib/director/EnrollmentDirector.js.map +1 -0
  20. package/lib/director/EnrollmentError.d.ts +6 -0
  21. package/lib/director/EnrollmentError.js +8 -0
  22. package/lib/director/EnrollmentError.js.map +1 -0
  23. package/lib/enrollments-index.d.ts +5 -0
  24. package/lib/enrollments-index.js +6 -0
  25. package/lib/enrollments-index.js.map +1 -0
  26. package/lib/enrollments-settings.d.ts +16 -0
  27. package/lib/enrollments-settings.js +17 -0
  28. package/lib/enrollments-settings.js.map +1 -0
  29. package/lib/module/configureEnrollmentsModule.d.ts +3 -0
  30. package/lib/module/configureEnrollmentsModule.js +323 -0
  31. package/lib/module/configureEnrollmentsModule.js.map +1 -0
  32. package/package.js +37 -0
  33. package/package.json +47 -0
  34. package/plugins/licensed.ts +49 -0
  35. package/src/db/EnrollmentStatus.ts +6 -0
  36. package/src/db/EnrollmentsCollection.ts +38 -0
  37. package/src/db/EnrollmentsSchema.js +53 -0
  38. package/src/director/EnrollmentAdapter.ts +77 -0
  39. package/src/director/EnrollmentDirector.ts +67 -0
  40. package/src/director/EnrollmentError.ts +6 -0
  41. package/src/enrollments-index.ts +8 -0
  42. package/src/enrollments-settings.js +22 -0
  43. package/src/module/configureEnrollmentsModule.ts +502 -0
  44. package/tests/enrollments-index.test.ts +91 -0
  45. package/tsconfig.build.json +26 -0
  46. package/tsconfig.json +8 -0
@@ -0,0 +1,67 @@
1
+ import {
2
+ EnrollmentData,
3
+ IEnrollmentAdapter,
4
+ IEnrollmentDirector,
5
+ } from '@unchainedshop/types/enrollments';
6
+ import { ProductPlan } from '@unchainedshop/types/products';
7
+ import { log, LogLevel } from '@unchainedshop/logger';
8
+ import { BaseDirector } from '@unchainedshop/utils';
9
+
10
+ const baseDirector = BaseDirector<IEnrollmentAdapter>('EnrollmentDirector', {
11
+ adapterSortKey: 'orderIndex',
12
+ });
13
+
14
+ const findAppropriateAdapters = (productPlan?: ProductPlan) =>
15
+ baseDirector.getAdapters({
16
+ adapterFilter: (Adapter: IEnrollmentAdapter) => {
17
+ const activated = Adapter.isActivatedFor(productPlan);
18
+ if (!activated) {
19
+ log(`Enrollment Director -> ${Adapter.key} (${Adapter.version}) skipped`, {
20
+ level: LogLevel.Warning,
21
+ });
22
+ }
23
+ return activated;
24
+ },
25
+ });
26
+
27
+ export const EnrollmentDirector: IEnrollmentDirector = {
28
+ ...baseDirector,
29
+
30
+ transformOrderItemToEnrollment: async ({ orderPosition, product }, doc, requestContext) => {
31
+ const Adapter = findAppropriateAdapters(product.plan)?.[0];
32
+ if (!Adapter) {
33
+ throw new Error('No suitable enrollment plugin available for this item');
34
+ }
35
+
36
+ const enrollmentPlan = await Adapter.transformOrderItemToEnrollmentPlan(
37
+ orderPosition,
38
+ requestContext,
39
+ );
40
+
41
+ const enrollmentData: EnrollmentData = {
42
+ ...doc,
43
+ ...enrollmentPlan,
44
+ configuration: [],
45
+ };
46
+
47
+ return enrollmentData;
48
+ },
49
+
50
+ actions: async (enrollmentContext, requestContext) => {
51
+ const context = { ...enrollmentContext, ...requestContext };
52
+
53
+ // Resolve adapter
54
+ const product = await context.modules.products.findProduct({
55
+ productId: context.enrollment.productId,
56
+ });
57
+
58
+ const Adapter = findAppropriateAdapters(product?.plan)?.[0];
59
+
60
+ if (!Adapter) {
61
+ throw new Error('No suitable enrollment plugin available for this plan configuration');
62
+ }
63
+ const adapter = Adapter.actions(context);
64
+
65
+ return adapter;
66
+ },
67
+ };
@@ -0,0 +1,6 @@
1
+ export enum EnrollmentError {
2
+ ADAPTER_NOT_FOUND = 'ADAPTER_NOT_FOUND',
3
+ NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
4
+ INCOMPLETE_CONFIGURATION = 'INCOMPLETE_CONFIGURATION',
5
+ WRONG_CREDENTIALS = 'WRONG_CREDENTIALS',
6
+ }
@@ -0,0 +1,8 @@
1
+ export { configureEnrollmentsModule } from './module/configureEnrollmentsModule';
2
+
3
+ export { EnrollmentAdapter } from './director/EnrollmentAdapter';
4
+ export { EnrollmentDirector } from './director/EnrollmentDirector';
5
+
6
+ export { EnrollmentStatus } from './db/EnrollmentStatus';
7
+
8
+ export { enrollmentsSettings } from './enrollments-settings';
@@ -0,0 +1,22 @@
1
+ import later from 'later';
2
+ import { generateRandomHash } from '@unchainedshop/utils';
3
+
4
+ const everyHourSchedule = later.parse.text('every 59 minutes');
5
+ const emptyObject = () => {
6
+ return {};
7
+ };
8
+
9
+ export const enrollmentsSettings = {
10
+ autoSchedulingSchedule: null,
11
+ autoSchedulingInput: null,
12
+ enrollmentNumberHashFn: null,
13
+ configureSettings({
14
+ autoSchedulingSchedule = everyHourSchedule,
15
+ autoSchedulingInput = emptyObject,
16
+ enrollmentNumberHashFn = generateRandomHash,
17
+ } = {}) {
18
+ this.autoSchedulingSchedule = autoSchedulingSchedule;
19
+ this.autoSchedulingInput = autoSchedulingInput || emptyObject;
20
+ this.enrollmentNumberHashFn = enrollmentNumberHashFn;
21
+ },
22
+ };
@@ -0,0 +1,502 @@
1
+ import { Context, SortOption } from '@unchainedshop/types/api';
2
+ import { ModuleInput, ModuleMutations } from '@unchainedshop/types/common';
3
+ import {
4
+ Enrollment,
5
+ EnrollmentQuery,
6
+ EnrollmentsModule,
7
+ EnrollmentsSettingsOptions,
8
+ } from '@unchainedshop/types/enrollments';
9
+ import { Locale } from 'locale';
10
+ import { emit, registerEvents } from '@unchainedshop/events';
11
+ import { log } from '@unchainedshop/logger';
12
+ import { generateDbFilterById, generateDbMutations, buildSortOptions } from '@unchainedshop/utils';
13
+ import { EnrollmentsCollection } from '../db/EnrollmentsCollection';
14
+ import { EnrollmentsSchema } from '../db/EnrollmentsSchema';
15
+ import { EnrollmentStatus } from '../db/EnrollmentStatus';
16
+ import { EnrollmentDirector } from '../enrollments-index';
17
+ import { enrollmentsSettings } from '../enrollments-settings';
18
+
19
+ const ENROLLMENT_EVENTS: string[] = [
20
+ 'ENROLLMENT_ADD_PERIOD',
21
+ 'ENROLLMENT_CREATE',
22
+ 'ENROLLMENT_REMOVE',
23
+ 'ENROLLMENT_UPDATE',
24
+ ];
25
+
26
+ const buildFindSelector = ({ queryString, status, userId }: EnrollmentQuery) => {
27
+ const selector: {
28
+ deleted: Date;
29
+ status?: any;
30
+ $text?: { $search?: string };
31
+ userId?: string;
32
+ } = {
33
+ deleted: null,
34
+ };
35
+ if (status) selector.status = { $in: status };
36
+ if (userId) selector.userId = userId;
37
+
38
+ if (queryString) selector.$text = { $search: queryString };
39
+
40
+ return selector;
41
+ };
42
+
43
+ export const configureEnrollmentsModule = async ({
44
+ db,
45
+ options: enrollmentOptions = {},
46
+ }: ModuleInput<EnrollmentsSettingsOptions>): Promise<EnrollmentsModule> => {
47
+ registerEvents(ENROLLMENT_EVENTS);
48
+
49
+ enrollmentsSettings.configureSettings(enrollmentOptions);
50
+
51
+ const Enrollments = await EnrollmentsCollection(db);
52
+
53
+ const mutations = generateDbMutations<Enrollment>(
54
+ Enrollments,
55
+ EnrollmentsSchema,
56
+ ) as ModuleMutations<Enrollment>;
57
+
58
+ const findNewEnrollmentNumber = async (enrollment: Enrollment, index = 0): Promise<string> => {
59
+ const newHashID = enrollmentsSettings.enrollmentNumberHashFn(enrollment, index);
60
+ if ((await Enrollments.countDocuments({ enrollmentNumber: newHashID }, { limit: 1 })) === 0) {
61
+ return newHashID;
62
+ }
63
+ return findNewEnrollmentNumber(enrollment, index + 1);
64
+ };
65
+
66
+ const findNextStatus = async (
67
+ enrollment: Enrollment,
68
+ requestContext: Context,
69
+ ): Promise<EnrollmentStatus> => {
70
+ let status = enrollment.status as EnrollmentStatus;
71
+ const director = await EnrollmentDirector.actions({ enrollment }, requestContext);
72
+
73
+ if (status === EnrollmentStatus.INITIAL || status === EnrollmentStatus.PAUSED) {
74
+ if (await director.isValidForActivation()) {
75
+ status = EnrollmentStatus.ACTIVE;
76
+ }
77
+ } else if (status === EnrollmentStatus.ACTIVE) {
78
+ if (await director.isOverdue()) {
79
+ status = EnrollmentStatus.PAUSED;
80
+ }
81
+ } else if (requestContext.modules.enrollments.isExpired(enrollment, {})) {
82
+ status = EnrollmentStatus.TERMINATED;
83
+ }
84
+
85
+ return status;
86
+ };
87
+
88
+ const updateStatus: EnrollmentsModule['updateStatus'] = async (
89
+ enrollmentId,
90
+ { status, info = '' },
91
+ requestContext,
92
+ ) => {
93
+ const selector = generateDbFilterById(enrollmentId);
94
+ const enrollment = await Enrollments.findOne(selector, {});
95
+
96
+ if (enrollment.status === status) return enrollment;
97
+
98
+ const date = new Date();
99
+ const modifier: {
100
+ $set: Partial<Enrollment>;
101
+ $push: { log: Enrollment['log'][0] };
102
+ } = {
103
+ $set: { status, updated: new Date(), updatedBy: requestContext.userId },
104
+ $push: {
105
+ log: {
106
+ date,
107
+ status,
108
+ info,
109
+ },
110
+ },
111
+ };
112
+
113
+ switch (status) {
114
+ case EnrollmentStatus.ACTIVE:
115
+ modifier.$set.enrollmentNumber = await findNewEnrollmentNumber(enrollment);
116
+ break;
117
+ case EnrollmentStatus.TERMINATED:
118
+ modifier.$set.expires = enrollment.periods?.pop()?.end || new Date();
119
+ break;
120
+ default:
121
+ break;
122
+ }
123
+
124
+ log(`New Status: ${status}`, { enrollmentId });
125
+
126
+ await Enrollments.updateOne(selector, modifier);
127
+
128
+ const updatedEnrollment = await Enrollments.findOne(selector, {});
129
+
130
+ emit('ENROLLMENT_UPDATE', { enrollment, field: 'status' });
131
+
132
+ return updatedEnrollment;
133
+ };
134
+
135
+ const reactivateEnrollment = async (enrollment: Enrollment) => {
136
+ return enrollment;
137
+ };
138
+
139
+ const processEnrollment = async (
140
+ enrollment: Enrollment,
141
+ params: { enrollmentContext?: any; orderIdForFirstPeriod?: string },
142
+ requestContext: Context,
143
+ ) => {
144
+ let status = await findNextStatus(enrollment, requestContext);
145
+
146
+ if (status === EnrollmentStatus.ACTIVE) {
147
+ const nextEnrollment = await reactivateEnrollment(enrollment);
148
+ status = await findNextStatus(nextEnrollment, requestContext);
149
+ }
150
+
151
+ return updateStatus(enrollment._id, { status, info: 'enrollment processed' }, requestContext);
152
+ };
153
+
154
+ const initializeEnrollment = async (
155
+ enrollment: Enrollment,
156
+ params: { orderIdForFirstPeriod?: string; reason: string },
157
+ requestContext: Context,
158
+ ) => {
159
+ const { modules, userId } = requestContext;
160
+
161
+ // const user = await modules.users.findUserById(enrollment.userId);
162
+ // const locale = modules.users.userLocale(user, requestContext);
163
+ // const reason = "new_enrollment";
164
+
165
+ const director = await EnrollmentDirector.actions({ enrollment }, requestContext);
166
+ const period = await director.nextPeriod();
167
+
168
+ if (period && (params.orderIdForFirstPeriod || period.isTrial)) {
169
+ const intializedEnrollment = await modules.enrollments.addEnrollmentPeriod(
170
+ enrollment._id,
171
+ { ...period, orderId: params.orderIdForFirstPeriod },
172
+ userId,
173
+ );
174
+
175
+ return processEnrollment(intializedEnrollment, params, requestContext);
176
+ }
177
+
178
+ return processEnrollment(enrollment, params, requestContext);
179
+ };
180
+
181
+ const sendStatusToCustomer = async (
182
+ enrollment: Enrollment,
183
+ params: { locale?: Locale; reason?: string },
184
+ requestContext: Context,
185
+ ) => {
186
+ const { modules, userId } = requestContext;
187
+
188
+ let { locale } = params;
189
+ if (!locale) {
190
+ const user = await modules.users.findUserById(enrollment.userId);
191
+ locale = modules.users.userLocale(user, requestContext);
192
+ }
193
+
194
+ await modules.worker.addWork(
195
+ {
196
+ type: 'MESSAGE',
197
+ retries: 0,
198
+ input: {
199
+ reason: params.reason || 'status_change',
200
+ locale,
201
+ template: 'ENROLLMENT_STATUS',
202
+ enrollmentId: enrollment._id,
203
+ },
204
+ },
205
+ userId,
206
+ );
207
+
208
+ return enrollment;
209
+ };
210
+
211
+ const updateEnrollmentField =
212
+ (fieldKey: string) => async (enrollmentId: string, fieldValue: any, userId?: string) => {
213
+ log(`Update enrollment field ${fieldKey.toUpperCase()}`, {
214
+ enrollmentId,
215
+ userId,
216
+ });
217
+
218
+ await mutations.update(enrollmentId, { $set: { [fieldKey]: fieldValue } }, userId);
219
+
220
+ const enrollment = await Enrollments.findOne(generateDbFilterById(enrollmentId), {});
221
+
222
+ emit('ENROLLMENT_UPDATE', { enrollment, field: fieldKey });
223
+
224
+ return enrollment;
225
+ };
226
+
227
+ return {
228
+ // Queries
229
+ count: async (query: EnrollmentQuery) => {
230
+ const enrollmentCount = await Enrollments.countDocuments(buildFindSelector(query));
231
+ return enrollmentCount;
232
+ },
233
+
234
+ findEnrollment: async ({ enrollmentId, orderId }, options) => {
235
+ const selector = enrollmentId
236
+ ? generateDbFilterById(enrollmentId)
237
+ : { 'periods.orderId': orderId };
238
+
239
+ return Enrollments.findOne(selector, options);
240
+ },
241
+
242
+ findEnrollments: async ({
243
+ limit,
244
+ offset,
245
+ sort,
246
+ ...query
247
+ }: EnrollmentQuery & { limit?: number; offset?: number; sort?: Array<SortOption> }) => {
248
+ const enrollments = Enrollments.find(buildFindSelector(query), {
249
+ skip: offset,
250
+ limit,
251
+ sort: buildSortOptions(sort),
252
+ });
253
+
254
+ return enrollments.toArray();
255
+ },
256
+
257
+ // Transformations
258
+ normalizedStatus: (enrollment) => {
259
+ return enrollment.status === null
260
+ ? EnrollmentStatus.INITIAL
261
+ : (enrollment.status as EnrollmentStatus);
262
+ },
263
+
264
+ isExpired: (enrollment, { referenceDate }) => {
265
+ const relevantDate = referenceDate ? new Date(referenceDate) : new Date();
266
+ const expiryDate = new Date(enrollment.expires);
267
+ const isExpired = relevantDate.getTime() > expiryDate.getTime();
268
+ return isExpired;
269
+ },
270
+
271
+ // Processing
272
+ terminateEnrollment: async (enrollment, { enrollmentContext }, requestContext) => {
273
+ if (enrollment.status === EnrollmentStatus.TERMINATED) return enrollment;
274
+
275
+ let updatedEnrollment = await updateStatus(
276
+ enrollment._id,
277
+ {
278
+ status: EnrollmentStatus.TERMINATED,
279
+ info: 'terminated manually',
280
+ },
281
+ requestContext,
282
+ );
283
+
284
+ updatedEnrollment = await processEnrollment(
285
+ updatedEnrollment,
286
+ { enrollmentContext },
287
+ requestContext,
288
+ );
289
+
290
+ return sendStatusToCustomer(updatedEnrollment, {}, requestContext);
291
+ },
292
+
293
+ activateEnrollment: async (enrollment, { enrollmentContext }, requestContext) => {
294
+ if (enrollment.status === EnrollmentStatus.TERMINATED) return enrollment;
295
+
296
+ let updatedEnrollment = await updateStatus(
297
+ enrollment._id,
298
+ {
299
+ status: EnrollmentStatus.ACTIVE,
300
+ info: 'activated manually',
301
+ },
302
+ requestContext,
303
+ );
304
+
305
+ updatedEnrollment = await processEnrollment(
306
+ updatedEnrollment,
307
+ { enrollmentContext },
308
+ requestContext,
309
+ );
310
+
311
+ return sendStatusToCustomer(updatedEnrollment, {}, requestContext);
312
+ },
313
+
314
+ // Mutations
315
+ addEnrollmentPeriod: async (enrollmentId, period, userId) => {
316
+ const { start, end, orderId, isTrial } = period;
317
+ const selector = generateDbFilterById(enrollmentId);
318
+ await Enrollments.updateOne(selector, {
319
+ $push: {
320
+ periods: {
321
+ start,
322
+ end,
323
+ orderId,
324
+ isTrial,
325
+ },
326
+ },
327
+ $set: {
328
+ updated: new Date(),
329
+ updatedBy: userId,
330
+ },
331
+ });
332
+ const enrollment = await Enrollments.findOne(selector, {});
333
+
334
+ emit('ENROLLMENT_ADD_PERIOD', { enrollment });
335
+
336
+ return enrollment;
337
+ },
338
+
339
+ create: async (
340
+ { countryCode, currencyCode, orderIdForFirstPeriod, ...enrollmentData },
341
+ requestContext,
342
+ ) => {
343
+ const { userId, services } = requestContext;
344
+
345
+ log('Create Enrollment', { userId });
346
+
347
+ const currency =
348
+ currencyCode ||
349
+ (await services.countries.resolveDefaultCurrencyCode(
350
+ {
351
+ isoCode: countryCode,
352
+ },
353
+ requestContext,
354
+ ));
355
+
356
+ const enrollmentId = await mutations.create(
357
+ {
358
+ ...enrollmentData,
359
+ status: EnrollmentStatus.INITIAL,
360
+ periods: [],
361
+ currencyCode: currency,
362
+ countryCode,
363
+ configuration: enrollmentData.configuration || [],
364
+ log: [],
365
+ },
366
+ userId,
367
+ );
368
+
369
+ const newEnrollment = await Enrollments.findOne(generateDbFilterById(enrollmentId), {});
370
+
371
+ const reason = 'new_enrollment';
372
+
373
+ const initializedEnrollment = await initializeEnrollment(
374
+ newEnrollment,
375
+ {
376
+ orderIdForFirstPeriod,
377
+ reason,
378
+ },
379
+ requestContext,
380
+ );
381
+
382
+ const enrollment = await sendStatusToCustomer(
383
+ initializedEnrollment,
384
+ {
385
+ reason,
386
+ },
387
+ requestContext,
388
+ );
389
+
390
+ emit('ENROLLMENT_CREATE', { enrollment });
391
+
392
+ return enrollment;
393
+ },
394
+
395
+ createFromCheckout: async (order, { items, context }, requestContext) => {
396
+ const { modules } = requestContext;
397
+ const orderId = order._id;
398
+
399
+ const payment = await modules.orders.payments.findOrderPayment({
400
+ orderPaymentId: order.paymentId,
401
+ });
402
+ const delivery = await modules.orders.deliveries.findDelivery({
403
+ orderDeliveryId: order.deliveryId,
404
+ });
405
+
406
+ const template = {
407
+ billingAddress: order.billingAddress,
408
+ contact: order.contact,
409
+ countryCode: order.countryCode,
410
+ currencyCode: order.currency,
411
+ delivery: {
412
+ deliveryProviderId: delivery.deliveryProviderId,
413
+ context: delivery.context,
414
+ },
415
+ orderIdForFirstPeriod: orderId,
416
+ payment: {
417
+ paymentProviderId: payment.paymentProviderId,
418
+ context: payment.context,
419
+ },
420
+ userId: order.userId,
421
+
422
+ // TODO: Check with pascal
423
+ meta: order.context,
424
+
425
+ ...context,
426
+ };
427
+
428
+ const enrollments = await Promise.all(
429
+ items.map(async (item) => {
430
+ const enrollmentData = await EnrollmentDirector.transformOrderItemToEnrollment(
431
+ item,
432
+ template,
433
+ requestContext,
434
+ );
435
+
436
+ return modules.enrollments.create(enrollmentData, requestContext);
437
+ }),
438
+ );
439
+
440
+ return enrollments;
441
+ },
442
+
443
+ delete: async (enrollmentId, userId) => {
444
+ const deletedCount = await mutations.delete(enrollmentId, userId);
445
+ emit('ORDER_REMOVE', { enrollmentId });
446
+ return deletedCount;
447
+ },
448
+
449
+ removeEnrollmentPeriodByOrderId: async (enrollmentId, orderId, userId) => {
450
+ const selector = generateDbFilterById(enrollmentId);
451
+ await Enrollments.updateOne(selector, {
452
+ $set: {
453
+ updated: new Date(),
454
+ updatedBy: userId,
455
+ },
456
+
457
+ $pull: {
458
+ periods: { orderId: { $in: [orderId, undefined, null] } },
459
+ },
460
+ });
461
+
462
+ return Enrollments.findOne(selector, {});
463
+ },
464
+
465
+ updateBillingAddress: updateEnrollmentField('billingAddress'),
466
+ updateContact: updateEnrollmentField('contact'),
467
+ updateContext: updateEnrollmentField('context'),
468
+ updateDelivery: updateEnrollmentField('delivery'),
469
+ updatePayment: updateEnrollmentField('payment'),
470
+
471
+ updatePlan: async (enrollmentId, plan, requestContext) => {
472
+ const { userId } = requestContext;
473
+
474
+ log(`Update enrollment plan fields`, {
475
+ enrollmentId,
476
+ userId,
477
+ });
478
+
479
+ await mutations.update(
480
+ enrollmentId,
481
+ {
482
+ productId: plan.productId,
483
+ quantity: plan.quantity,
484
+ configuration: plan.configuration,
485
+ },
486
+ userId,
487
+ );
488
+
489
+ const selector = generateDbFilterById(enrollmentId);
490
+ const enrollment = await Enrollments.findOne(selector, {});
491
+
492
+ emit('ENROLLMENT_UPDATE', { enrollment, field: 'plan' });
493
+
494
+ const reason = 'updated_plan';
495
+ const initializedEnrollment = await initializeEnrollment(enrollment, { reason }, requestContext);
496
+
497
+ return sendStatusToCustomer(initializedEnrollment, { reason }, requestContext);
498
+ },
499
+
500
+ updateStatus,
501
+ };
502
+ };
@@ -0,0 +1,91 @@
1
+ import { assert } from 'chai';
2
+ import { initDb } from 'meteor/unchained:mongodb';
3
+ import { configureUsersModule } from 'meteor/unchained:core-users';
4
+ import { configureEnrollmentsModule } from 'meteor/unchained:core-enrollments';
5
+ import { EnrollmentsModule } from '@unchainedshop/types/enrollments';
6
+ import { Context } from '@unchainedshop/types/api';
7
+ import { User, UsersModule } from '@unchainedshop/types/user';
8
+
9
+ describe('Test exports', () => {
10
+ const context: {
11
+ modules: { enrollments: EnrollmentsModule; users: UsersModule };
12
+ services: { countries: { resolveDefaultCurrencyCode: () => string } };
13
+ } = {
14
+ modules: {
15
+ enrollments: null,
16
+ users: null,
17
+ },
18
+ services: {
19
+ countries: {
20
+ resolveDefaultCurrencyCode: () => 'CHF',
21
+ },
22
+ },
23
+ };
24
+ let user: User
25
+
26
+ before(async () => {
27
+ const db = await initDb();
28
+ const enrollmentsModule = await configureEnrollmentsModule({ db }).catch(
29
+ (error) => {
30
+ console.error(error);
31
+
32
+ throw error;
33
+ }
34
+ );
35
+
36
+ const usersModules = await configureUsersModule({ db }).catch((error) => {
37
+ console.error(error);
38
+
39
+ throw error;
40
+ });
41
+
42
+ context.modules.enrollments = enrollmentsModule;
43
+ context.modules.users = usersModules;
44
+ });
45
+
46
+ it('Insert enrollment', async () => {
47
+ let enrollment = await context.modules.enrollments.create(
48
+ {
49
+ billingAddress: {
50
+ lastName: 'Mustermann',
51
+ firstName: 'Max',
52
+ addressLine: 'Teststreet 11',
53
+ city: 'Zürich',
54
+ postalCode: '8009',
55
+ company: 'Unchained Commerce',
56
+ countryCode: 'CH',
57
+ },
58
+ countryCode: 'CH',
59
+ currencyCode: 'CHF',
60
+ contact: {},
61
+ productId: 'Product-123',
62
+ quantity: 2,
63
+ userId: 'Test-User-1',
64
+ payment: {
65
+ paymentProviderId: 'payment-provider-1',
66
+ context: {},
67
+ },
68
+ delivery: {
69
+ deliveryProviderId: 'delivery-provider-1',
70
+ context: {},
71
+ },
72
+ },
73
+ context as Context
74
+ );
75
+
76
+ assert.ok(enrollment);
77
+ const enrollmentId = enrollment._id;
78
+ enrollment = await context.modules.enrollments.findEnrollment({
79
+ enrollmentId,
80
+ });
81
+
82
+ assert.ok(enrollment);
83
+
84
+ const deletedCount = await context.modules.enrollments
85
+ .delete(enrollmentId, 'Test-User-1')
86
+ .catch((error) => {
87
+ return 0;
88
+ });
89
+ assert.equal(deletedCount, 1);
90
+ });
91
+ });