@unchainedshop/core-enrollments 5.0.0-alpha.10 → 5.0.0-alpha.11
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/README.md +3 -1
- package/lib/db/EnrollmentsCollection.d.ts +15 -0
- package/lib/db/EnrollmentsCollection.js +8 -0
- package/lib/enrollments-settings.d.ts +1 -0
- package/lib/enrollments-settings.js +3 -1
- package/lib/module/configureEnrollmentsModule.d.ts +12 -1
- package/lib/module/configureEnrollmentsModule.js +50 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -25,7 +25,9 @@ const activeEnrollments = await enrollments.findEnrollments({
|
|
|
25
25
|
});
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Status filters accept arrays. Enrollment lifecycle operations and recurring order generation are coordinated by `unchainedAPI.services.enrollments` and the registered enrollment and worker plugins. The module stores enrollments and their periods (`addEnrollmentPeriod`).
|
|
28
|
+
Status filters accept arrays. Enrollment lifecycle operations and recurring order generation are coordinated by `unchainedAPI.services.enrollments` and the registered enrollment and worker plugins. The module stores enrollments and their periods (`addEnrollmentPeriod`) together with the lifecycle fields the services maintain: `expires` (when the enrollment ends), `resumeAt`, `contractStartDate`, `minimumCommitmentEnd` and the cancellation reason.
|
|
29
|
+
|
|
30
|
+
`EnrollmentStatus` covers `INITIAL`, `ACTIVE`, `PAUSED`, `SUSPENDED` and `TERMINATED`; `EnrollmentTerminationReason` lists the cancellation reasons. Emitted events: `ENROLLMENT_CREATE`, `ENROLLMENT_UPDATE`, `ENROLLMENT_REMOVE`, `ENROLLMENT_ADD_PERIOD`, `ENROLLMENT_SUSPEND`, `ENROLLMENT_RESUME`, `ENROLLMENT_PLAN_CHANGE` and `ENROLLMENT_TRIAL_ENDING`.
|
|
29
31
|
|
|
30
32
|
See the [module guide](https://docs.unchained.shop/platform-configuration/modules/enrollments), [public exports](src/enrollments-index.ts), and [module implementation](src/module/configureEnrollmentsModule.ts).
|
|
31
33
|
|
|
@@ -5,6 +5,7 @@ export interface EnrollmentPeriod {
|
|
|
5
5
|
end: Date;
|
|
6
6
|
orderId?: string;
|
|
7
7
|
isTrial?: boolean;
|
|
8
|
+
trialEndingNotifiedAt?: Date;
|
|
8
9
|
}
|
|
9
10
|
export interface EnrollmentPlan {
|
|
10
11
|
configuration?: {
|
|
@@ -18,8 +19,17 @@ export declare const EnrollmentStatus: {
|
|
|
18
19
|
readonly INITIAL: "INITIAL";
|
|
19
20
|
readonly ACTIVE: "ACTIVE";
|
|
20
21
|
readonly PAUSED: "PAUSED";
|
|
22
|
+
readonly SUSPENDED: "SUSPENDED";
|
|
21
23
|
readonly TERMINATED: "TERMINATED";
|
|
22
24
|
};
|
|
25
|
+
export declare const EnrollmentTerminationReason: {
|
|
26
|
+
readonly USER_REQUESTED: "USER_REQUESTED";
|
|
27
|
+
readonly PAYMENT_FAILED: "PAYMENT_FAILED";
|
|
28
|
+
readonly EXPIRED: "EXPIRED";
|
|
29
|
+
readonly ADMIN_ACTION: "ADMIN_ACTION";
|
|
30
|
+
readonly OTHER: "OTHER";
|
|
31
|
+
};
|
|
32
|
+
export type EnrollmentTerminationReason = (typeof EnrollmentTerminationReason)[keyof typeof EnrollmentTerminationReason];
|
|
23
33
|
export type EnrollmentStatus = (typeof EnrollmentStatus)[keyof typeof EnrollmentStatus];
|
|
24
34
|
export interface EnrollmentOrderPositionTemplate {
|
|
25
35
|
context?: any;
|
|
@@ -50,6 +60,11 @@ export type Enrollment = {
|
|
|
50
60
|
enrollmentNumber?: string;
|
|
51
61
|
orderIdForFirstPeriod?: string;
|
|
52
62
|
expires?: Date;
|
|
63
|
+
resumeAt?: Date;
|
|
64
|
+
contractStartDate?: Date;
|
|
65
|
+
minimumCommitmentEnd?: Date;
|
|
66
|
+
cancellationReason?: EnrollmentTerminationReason;
|
|
67
|
+
cancellationComment?: string;
|
|
53
68
|
meta?: any;
|
|
54
69
|
payment?: {
|
|
55
70
|
paymentProviderId: string;
|
|
@@ -4,8 +4,16 @@ export const EnrollmentStatus = {
|
|
|
4
4
|
INITIAL: 'INITIAL',
|
|
5
5
|
ACTIVE: 'ACTIVE',
|
|
6
6
|
PAUSED: 'PAUSED',
|
|
7
|
+
SUSPENDED: 'SUSPENDED',
|
|
7
8
|
TERMINATED: 'TERMINATED',
|
|
8
9
|
};
|
|
10
|
+
export const EnrollmentTerminationReason = {
|
|
11
|
+
USER_REQUESTED: 'USER_REQUESTED',
|
|
12
|
+
PAYMENT_FAILED: 'PAYMENT_FAILED',
|
|
13
|
+
EXPIRED: 'EXPIRED',
|
|
14
|
+
ADMIN_ACTION: 'ADMIN_ACTION',
|
|
15
|
+
OTHER: 'OTHER',
|
|
16
|
+
};
|
|
9
17
|
export const EnrollmentsCollection = async (db) => {
|
|
10
18
|
const Enrollments = db.collection('enrollments');
|
|
11
19
|
await buildDbIndexes(Enrollments, [
|
|
@@ -12,6 +12,7 @@ export interface ScheduleData {
|
|
|
12
12
|
export interface EnrollmentSettings {
|
|
13
13
|
autoSchedulingSchedule: ScheduleData;
|
|
14
14
|
enrollmentNumberHashFn: (enrollment: Enrollment, index: number) => string;
|
|
15
|
+
trialEndingNoticeDays: number;
|
|
15
16
|
configureSettings: (options?: EnrollmentsSettingsOptions) => void;
|
|
16
17
|
}
|
|
17
18
|
export type EnrollmentsSettingsOptions = Omit<Partial<EnrollmentSettings>, 'configureSettings'>;
|
|
@@ -5,8 +5,10 @@ const everyHourSchedule = {
|
|
|
5
5
|
export const enrollmentsSettings = {
|
|
6
6
|
autoSchedulingSchedule: everyHourSchedule,
|
|
7
7
|
enrollmentNumberHashFn: generateRandomHash,
|
|
8
|
-
|
|
8
|
+
trialEndingNoticeDays: 3,
|
|
9
|
+
configureSettings({ autoSchedulingSchedule, enrollmentNumberHashFn, trialEndingNoticeDays } = {}) {
|
|
9
10
|
this.autoSchedulingSchedule = autoSchedulingSchedule || everyHourSchedule;
|
|
10
11
|
this.enrollmentNumberHashFn = enrollmentNumberHashFn || generateRandomHash;
|
|
12
|
+
this.trialEndingNoticeDays = trialEndingNoticeDays ?? 3;
|
|
11
13
|
},
|
|
12
14
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SortOption } from '@unchainedshop/utils';
|
|
2
|
-
import { type Enrollment, type EnrollmentPeriod, type EnrollmentPlan, EnrollmentStatus } from '../db/EnrollmentsCollection.ts';
|
|
2
|
+
import { type Enrollment, type EnrollmentPeriod, type EnrollmentPlan, type EnrollmentTerminationReason, EnrollmentStatus } from '../db/EnrollmentsCollection.ts';
|
|
3
3
|
import { mongodb, type Address, type Contact, type ModuleInput } from '@unchainedshop/mongodb';
|
|
4
4
|
import { type EnrollmentsSettingsOptions } from '../enrollments-settings.ts';
|
|
5
5
|
export interface EnrollmentQuery {
|
|
@@ -31,6 +31,7 @@ export declare const configureEnrollmentsModule: ({ db, migrationRepository, opt
|
|
|
31
31
|
referenceDate?: Date;
|
|
32
32
|
}) => boolean;
|
|
33
33
|
addEnrollmentPeriod: (enrollmentId: string, period: EnrollmentPeriod) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
34
|
+
markEnrollmentTrialEndingNotified: (enrollmentId: string, { start, end }: EnrollmentPeriod) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
34
35
|
create: ({ countryCode, currencyCode, ...enrollmentData }: Omit<Enrollment, "status" | "periods" | "log" | "_id" | "created"> & Pick<Partial<Enrollment>, "_id" | "created">) => Promise<Enrollment>;
|
|
35
36
|
delete: (enrollmentId: string) => Promise<number>;
|
|
36
37
|
removeEnrollmentPeriodByOrderId: (enrollmentId: string, orderId: string) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
@@ -45,6 +46,16 @@ export declare const configureEnrollmentsModule: ({ db, migrationRepository, opt
|
|
|
45
46
|
paymentProviderId: string;
|
|
46
47
|
context?: any;
|
|
47
48
|
} | undefined) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
49
|
+
updateExpiry: (enrollmentId: string, fieldValue: Date | null) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
50
|
+
updateResumeAt: (enrollmentId: string, fieldValue: Date) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
51
|
+
updateCommitment: (enrollmentId: string, { contractStartDate, minimumCommitmentEnd, }: {
|
|
52
|
+
contractStartDate: Date;
|
|
53
|
+
minimumCommitmentEnd: Date;
|
|
54
|
+
}) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
55
|
+
updateCancellation: (enrollmentId: string, params: {
|
|
56
|
+
reason?: EnrollmentTerminationReason;
|
|
57
|
+
comment?: string;
|
|
58
|
+
}) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
48
59
|
updatePlan: (enrollmentId: string, plan: EnrollmentPlan) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
49
60
|
updateStatus: (enrollmentId: string, { status, info }: {
|
|
50
61
|
status: EnrollmentStatus;
|
|
@@ -10,6 +10,10 @@ const ENROLLMENT_EVENTS = [
|
|
|
10
10
|
'ENROLLMENT_CREATE',
|
|
11
11
|
'ENROLLMENT_REMOVE',
|
|
12
12
|
'ENROLLMENT_UPDATE',
|
|
13
|
+
'ENROLLMENT_SUSPEND',
|
|
14
|
+
'ENROLLMENT_RESUME',
|
|
15
|
+
'ENROLLMENT_PLAN_CHANGE',
|
|
16
|
+
'ENROLLMENT_TRIAL_ENDING',
|
|
13
17
|
];
|
|
14
18
|
const normalizeContactPhone = (contact, defaultCountry) => {
|
|
15
19
|
if (!contact?.telNumber)
|
|
@@ -71,18 +75,30 @@ export const configureEnrollmentsModule = async ({ db, migrationRepository, opti
|
|
|
71
75
|
};
|
|
72
76
|
switch (status) {
|
|
73
77
|
case EnrollmentStatus.ACTIVE:
|
|
74
|
-
modifier.$set.enrollmentNumber =
|
|
78
|
+
modifier.$set.enrollmentNumber =
|
|
79
|
+
enrollment.enrollmentNumber || (await findNewEnrollmentNumber(enrollment));
|
|
75
80
|
break;
|
|
76
|
-
case EnrollmentStatus.TERMINATED:
|
|
77
|
-
|
|
81
|
+
case EnrollmentStatus.TERMINATED: {
|
|
82
|
+
const periodEnds = (enrollment.periods || []).map(({ end }) => new Date(end).getTime());
|
|
83
|
+
modifier.$set.expires =
|
|
84
|
+
enrollment.expires || (periodEnds.length ? new Date(Math.max(...periodEnds)) : date);
|
|
78
85
|
break;
|
|
86
|
+
}
|
|
79
87
|
default:
|
|
80
88
|
break;
|
|
81
89
|
}
|
|
90
|
+
if (enrollment.status === EnrollmentStatus.SUSPENDED)
|
|
91
|
+
modifier.$unset = { resumeAt: 1 };
|
|
82
92
|
const updatedEnrollment = await Enrollments.findOneAndUpdate(selector, modifier, {
|
|
83
93
|
returnDocument: 'after',
|
|
84
94
|
});
|
|
85
95
|
await emit('ENROLLMENT_UPDATE', { enrollment, field: 'status' });
|
|
96
|
+
if (status === EnrollmentStatus.SUSPENDED) {
|
|
97
|
+
await emit('ENROLLMENT_SUSPEND', { enrollment: updatedEnrollment });
|
|
98
|
+
}
|
|
99
|
+
else if (enrollment.status === EnrollmentStatus.SUSPENDED && status === EnrollmentStatus.ACTIVE) {
|
|
100
|
+
await emit('ENROLLMENT_RESUME', { enrollment: updatedEnrollment });
|
|
101
|
+
}
|
|
86
102
|
return updatedEnrollment;
|
|
87
103
|
};
|
|
88
104
|
const updateEnrollmentField = (fieldKey) => async (enrollmentId, fieldValue) => {
|
|
@@ -102,7 +118,9 @@ export const configureEnrollmentsModule = async ({ db, migrationRepository, opti
|
|
|
102
118
|
},
|
|
103
119
|
openEnrollmentWithProduct: async ({ productId }) => {
|
|
104
120
|
const selector = { productId };
|
|
105
|
-
selector.status = {
|
|
121
|
+
selector.status = {
|
|
122
|
+
$in: [EnrollmentStatus.ACTIVE, EnrollmentStatus.PAUSED, EnrollmentStatus.SUSPENDED],
|
|
123
|
+
};
|
|
106
124
|
return Enrollments.findOne(selector);
|
|
107
125
|
},
|
|
108
126
|
findEnrollment: async (params, options) => {
|
|
@@ -154,6 +172,16 @@ export const configureEnrollmentsModule = async ({ db, migrationRepository, opti
|
|
|
154
172
|
await emit('ENROLLMENT_ADD_PERIOD', { enrollment });
|
|
155
173
|
return enrollment;
|
|
156
174
|
},
|
|
175
|
+
markEnrollmentTrialEndingNotified: async (enrollmentId, { start, end }) => {
|
|
176
|
+
const enrollment = await Enrollments.findOneAndUpdate({
|
|
177
|
+
...generateDbFilterById(enrollmentId),
|
|
178
|
+
periods: { $elemMatch: { start, end, trialEndingNotifiedAt: null } },
|
|
179
|
+
}, { $set: { 'periods.$.trialEndingNotifiedAt': new Date(), updated: new Date() } }, { returnDocument: 'after' });
|
|
180
|
+
if (!enrollment)
|
|
181
|
+
return null;
|
|
182
|
+
await emit('ENROLLMENT_UPDATE', { enrollment, field: 'periods' });
|
|
183
|
+
return enrollment;
|
|
184
|
+
},
|
|
157
185
|
create: async ({ countryCode, currencyCode, ...enrollmentData }) => {
|
|
158
186
|
const { insertedId: enrollmentId } = await Enrollments.insertOne({
|
|
159
187
|
_id: generateDbObjectId(),
|
|
@@ -204,6 +232,24 @@ export const configureEnrollmentsModule = async ({ db, migrationRepository, opti
|
|
|
204
232
|
updateContext: updateEnrollmentField('meta'),
|
|
205
233
|
updateDelivery: updateEnrollmentField('delivery'),
|
|
206
234
|
updatePayment: updateEnrollmentField('payment'),
|
|
235
|
+
updateExpiry: updateEnrollmentField('expires'),
|
|
236
|
+
updateResumeAt: updateEnrollmentField('resumeAt'),
|
|
237
|
+
updateCommitment: async (enrollmentId, { contractStartDate, minimumCommitmentEnd, }) => {
|
|
238
|
+
const enrollment = await Enrollments.findOneAndUpdate(generateDbFilterById(enrollmentId), { $set: { updated: new Date(), contractStartDate, minimumCommitmentEnd } }, { returnDocument: 'after' });
|
|
239
|
+
await emit('ENROLLMENT_UPDATE', { enrollment, field: 'commitment' });
|
|
240
|
+
return enrollment;
|
|
241
|
+
},
|
|
242
|
+
updateCancellation: async (enrollmentId, params) => {
|
|
243
|
+
const enrollment = await Enrollments.findOneAndUpdate(generateDbFilterById(enrollmentId), {
|
|
244
|
+
$set: {
|
|
245
|
+
updated: new Date(),
|
|
246
|
+
...(params.reason && { cancellationReason: params.reason }),
|
|
247
|
+
...(params.comment !== undefined && { cancellationComment: params.comment }),
|
|
248
|
+
},
|
|
249
|
+
}, { returnDocument: 'after' });
|
|
250
|
+
await emit('ENROLLMENT_UPDATE', { enrollment, field: 'cancellation' });
|
|
251
|
+
return enrollment;
|
|
252
|
+
},
|
|
207
253
|
updatePlan: async (enrollmentId, plan) => {
|
|
208
254
|
const enrollment = await Enrollments.findOneAndUpdate(generateDbFilterById(enrollmentId), {
|
|
209
255
|
$set: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/core-enrollments",
|
|
3
3
|
"description": "Subscription and recurring billing module for the Unchained Engine",
|
|
4
|
-
"version": "5.0.0-alpha.
|
|
4
|
+
"version": "5.0.0-alpha.11",
|
|
5
5
|
"files": [
|
|
6
6
|
"lib"
|
|
7
7
|
],
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@unchainedshop/events": "^5.0.0-alpha.
|
|
42
|
-
"@unchainedshop/mongodb": "^5.0.0-alpha.
|
|
43
|
-
"@unchainedshop/utils": "^5.0.0-alpha.
|
|
41
|
+
"@unchainedshop/events": "^5.0.0-alpha.11",
|
|
42
|
+
"@unchainedshop/mongodb": "^5.0.0-alpha.11",
|
|
43
|
+
"@unchainedshop/utils": "^5.0.0-alpha.11"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^26.2.0",
|