@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.
- package/.npm/package/README +7 -0
- package/.npm/package/npm-shrinkwrap.json +205 -0
- package/.versions +55 -0
- package/README.md +5 -0
- package/lib/db/EnrollmentStatus.d.ts +6 -0
- package/lib/db/EnrollmentStatus.js +8 -0
- package/lib/db/EnrollmentStatus.js.map +1 -0
- package/lib/db/EnrollmentsCollection.d.ts +3 -0
- package/lib/db/EnrollmentsCollection.js +34 -0
- package/lib/db/EnrollmentsCollection.js.map +1 -0
- package/lib/db/EnrollmentsSchema.d.ts +2 -0
- package/lib/db/EnrollmentsSchema.js +46 -0
- package/lib/db/EnrollmentsSchema.js.map +1 -0
- package/lib/director/EnrollmentAdapter.d.ts +2 -0
- package/lib/director/EnrollmentAdapter.js +67 -0
- package/lib/director/EnrollmentAdapter.js.map +1 -0
- package/lib/director/EnrollmentDirector.d.ts +2 -0
- package/lib/director/EnrollmentDirector.js +46 -0
- package/lib/director/EnrollmentDirector.js.map +1 -0
- package/lib/director/EnrollmentError.d.ts +6 -0
- package/lib/director/EnrollmentError.js +8 -0
- package/lib/director/EnrollmentError.js.map +1 -0
- package/lib/enrollments-index.d.ts +5 -0
- package/lib/enrollments-index.js +6 -0
- package/lib/enrollments-index.js.map +1 -0
- package/lib/enrollments-settings.d.ts +16 -0
- package/lib/enrollments-settings.js +17 -0
- package/lib/enrollments-settings.js.map +1 -0
- package/lib/module/configureEnrollmentsModule.d.ts +3 -0
- package/lib/module/configureEnrollmentsModule.js +323 -0
- package/lib/module/configureEnrollmentsModule.js.map +1 -0
- package/package.js +37 -0
- package/package.json +47 -0
- package/plugins/licensed.ts +49 -0
- package/src/db/EnrollmentStatus.ts +6 -0
- package/src/db/EnrollmentsCollection.ts +38 -0
- package/src/db/EnrollmentsSchema.js +53 -0
- package/src/director/EnrollmentAdapter.ts +77 -0
- package/src/director/EnrollmentDirector.ts +67 -0
- package/src/director/EnrollmentError.ts +6 -0
- package/src/enrollments-index.ts +8 -0
- package/src/enrollments-settings.js +22 -0
- package/src/module/configureEnrollmentsModule.ts +502 -0
- package/tests/enrollments-index.test.ts +91 -0
- package/tsconfig.build.json +26 -0
- 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,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
|
+
});
|