@unchainedshop/core-enrollments 5.0.0-alpha.1 → 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 +9 -83
- package/lib/db/EnrollmentsCollection.d.ts +16 -1
- package/lib/db/EnrollmentsCollection.js +33 -26
- package/lib/enrollments-settings.d.ts +1 -0
- package/lib/enrollments-settings.js +3 -1
- package/lib/migrations/20260625120100-normalize-contact-phone.d.ts +2 -0
- package/lib/migrations/20260625120100-normalize-contact-phone.js +27 -0
- package/lib/module/configureEnrollmentsModule.d.ts +17 -3
- package/lib/module/configureEnrollmentsModule.js +76 -9
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -13,97 +13,23 @@ npm install @unchainedshop/core-enrollments
|
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
import { configureEnrollmentsModule, EnrollmentStatus } from '@unchainedshop/core-enrollments';
|
|
16
|
+
The platform initializes this module as `platform.unchainedAPI.modules.enrollments`.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
```typescript
|
|
19
|
+
import { EnrollmentStatus } from '@unchainedshop/core-enrollments';
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const
|
|
21
|
+
const { enrollments } = platform.unchainedAPI.modules;
|
|
22
|
+
const activeEnrollments = await enrollments.findEnrollments({
|
|
23
|
+
status: [EnrollmentStatus.ACTIVE],
|
|
23
24
|
userId: 'user-123',
|
|
24
|
-
productId: 'plan-product-456',
|
|
25
|
-
quantity: 1,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Activate enrollment
|
|
29
|
-
await enrollmentsModule.activate(enrollmentId);
|
|
30
|
-
|
|
31
|
-
// Find active enrollments
|
|
32
|
-
const enrollments = await enrollmentsModule.findEnrollments({
|
|
33
|
-
status: EnrollmentStatus.ACTIVE,
|
|
34
25
|
});
|
|
35
26
|
```
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
### Module Configuration
|
|
40
|
-
|
|
41
|
-
| Export | Description |
|
|
42
|
-
|--------|-------------|
|
|
43
|
-
| `configureEnrollmentsModule` | Configure and return the enrollments module |
|
|
44
|
-
|
|
45
|
-
### Queries
|
|
46
|
-
|
|
47
|
-
| Method | Description |
|
|
48
|
-
|--------|-------------|
|
|
49
|
-
| `findEnrollment` | Find enrollment by ID |
|
|
50
|
-
| `findEnrollments` | Find enrollments with filtering and pagination |
|
|
51
|
-
| `count` | Count enrollments matching query |
|
|
52
|
-
| `enrollmentExists` | Check if enrollment exists |
|
|
53
|
-
|
|
54
|
-
### Mutations
|
|
55
|
-
|
|
56
|
-
| Method | Description |
|
|
57
|
-
|--------|-------------|
|
|
58
|
-
| `create` | Create a new enrollment |
|
|
59
|
-
| `update` | Update enrollment data |
|
|
60
|
-
| `delete` | Delete an enrollment |
|
|
61
|
-
| `activate` | Activate an enrollment |
|
|
62
|
-
| `terminate` | Terminate an enrollment |
|
|
63
|
-
|
|
64
|
-
### Period Management
|
|
65
|
-
|
|
66
|
-
| Method | Description |
|
|
67
|
-
|--------|-------------|
|
|
68
|
-
| `addPeriod` | Add a billing period |
|
|
69
|
-
| `findPeriod` | Find a specific period |
|
|
70
|
-
| `isExpired` | Check if enrollment is expired |
|
|
71
|
-
|
|
72
|
-
### Utilities
|
|
73
|
-
|
|
74
|
-
| Export | Description |
|
|
75
|
-
|--------|-------------|
|
|
76
|
-
| `addToDate` | Add time interval to date |
|
|
77
|
-
|
|
78
|
-
### Constants
|
|
79
|
-
|
|
80
|
-
| Export | Description |
|
|
81
|
-
|--------|-------------|
|
|
82
|
-
| `EnrollmentStatus` | Status values (INITIAL, ACTIVE, PAUSED, TERMINATED) |
|
|
83
|
-
|
|
84
|
-
### Settings
|
|
85
|
-
|
|
86
|
-
| Export | Description |
|
|
87
|
-
|--------|-------------|
|
|
88
|
-
| `enrollmentsSettings` | Access enrollment module settings |
|
|
89
|
-
|
|
90
|
-
### Types
|
|
91
|
-
|
|
92
|
-
| Export | Description |
|
|
93
|
-
|--------|-------------|
|
|
94
|
-
| `Enrollment` | Enrollment document type |
|
|
95
|
-
| `EnrollmentPeriod` | Period document type |
|
|
96
|
-
| `EnrollmentsModule` | Module interface type |
|
|
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.
|
|
97
29
|
|
|
98
|
-
|
|
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`.
|
|
99
31
|
|
|
100
|
-
|
|
101
|
-
|-------|-------------|
|
|
102
|
-
| `ENROLLMENT_CREATE` | Enrollment created |
|
|
103
|
-
| `ENROLLMENT_UPDATE` | Enrollment updated |
|
|
104
|
-
| `ENROLLMENT_REMOVE` | Enrollment deleted |
|
|
105
|
-
| `ENROLLMENT_ACTIVATE` | Enrollment activated |
|
|
106
|
-
| `ENROLLMENT_TERMINATE` | Enrollment terminated |
|
|
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).
|
|
107
33
|
|
|
108
34
|
## License
|
|
109
35
|
|
|
@@ -5,9 +5,10 @@ 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
|
-
configuration
|
|
11
|
+
configuration?: {
|
|
11
12
|
key: string;
|
|
12
13
|
value: string;
|
|
13
14
|
}[] | null;
|
|
@@ -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;
|
|
@@ -1,41 +1,48 @@
|
|
|
1
|
-
import { mongodb, buildDbIndexes
|
|
1
|
+
import { mongodb, buildDbIndexes } from '@unchainedshop/mongodb';
|
|
2
2
|
import {} from '@unchainedshop/mongodb';
|
|
3
3
|
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
|
-
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
status: 1,
|
|
30
|
-
},
|
|
31
|
-
name: 'enrollment_fulltext_search',
|
|
19
|
+
await buildDbIndexes(Enrollments, [
|
|
20
|
+
{
|
|
21
|
+
index: {
|
|
22
|
+
_id: 'text',
|
|
23
|
+
userId: 'text',
|
|
24
|
+
enrollmentNumber: 'text',
|
|
25
|
+
status: 'text',
|
|
26
|
+
'contact.telNumber': 'text',
|
|
27
|
+
'contact.emailAddress': 'text',
|
|
28
|
+
},
|
|
29
|
+
options: {
|
|
30
|
+
weights: {
|
|
31
|
+
_id: 8,
|
|
32
|
+
userId: 3,
|
|
33
|
+
enrollmentNumber: 6,
|
|
34
|
+
'contact.telNumber': 5,
|
|
35
|
+
'contact.emailAddress': 4,
|
|
36
|
+
status: 1,
|
|
32
37
|
},
|
|
38
|
+
name: 'enrollment_fulltext_search',
|
|
33
39
|
},
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
36
42
|
await buildDbIndexes(Enrollments, [
|
|
37
|
-
{ index: {
|
|
38
|
-
{ index: {
|
|
43
|
+
{ index: { 'periods.orderId': 1 } },
|
|
44
|
+
{ index: { userId: 1, status: 1 } },
|
|
45
|
+
{ index: { productId: 1, status: 1 } },
|
|
39
46
|
{ index: { status: 1 } },
|
|
40
47
|
{ index: { enrollmentNumber: 1 } },
|
|
41
48
|
]);
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizePhoneNumber } from '@unchainedshop/utils';
|
|
2
|
+
import { EnrollmentsCollection } from "../db/EnrollmentsCollection.js";
|
|
3
|
+
export default function normalizeContactPhone(repository) {
|
|
4
|
+
repository?.register({
|
|
5
|
+
id: 20260625120100,
|
|
6
|
+
name: 'Normalize enrollment.contact.telNumber to E.164 format',
|
|
7
|
+
up: async ({ logger }) => {
|
|
8
|
+
const Enrollments = await EnrollmentsCollection(repository.db);
|
|
9
|
+
const enrollments = await Enrollments.find({ 'contact.telNumber': { $exists: true, $nin: [null, ''] } }, { projection: { _id: true, contact: true, billingAddress: true, countryCode: true } }).toArray();
|
|
10
|
+
let changed = 0;
|
|
11
|
+
let skipped = 0;
|
|
12
|
+
for (const enrollment of enrollments) {
|
|
13
|
+
const current = enrollment.contact?.telNumber;
|
|
14
|
+
const defaultCountry = enrollment.billingAddress?.countryCode || enrollment.countryCode;
|
|
15
|
+
const normalized = normalizePhoneNumber(current, defaultCountry);
|
|
16
|
+
if (!normalized || normalized === current) {
|
|
17
|
+
if (!normalized)
|
|
18
|
+
skipped += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
await Enrollments.updateOne({ _id: enrollment._id }, { $set: { 'contact.telNumber': normalized } });
|
|
22
|
+
changed += 1;
|
|
23
|
+
}
|
|
24
|
+
logger?.info(`Normalize enrollment.contact.telNumber: ${changed} updated, ${skipped} left unchanged (unparseable)`);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -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 {
|
|
@@ -8,7 +8,7 @@ export interface EnrollmentQuery {
|
|
|
8
8
|
queryString?: string;
|
|
9
9
|
}
|
|
10
10
|
export declare const buildFindSelector: ({ queryString, status, userId }: EnrollmentQuery) => mongodb.Filter<Enrollment>;
|
|
11
|
-
export declare const configureEnrollmentsModule: ({ db, options: enrollmentOptions, }: ModuleInput<EnrollmentsSettingsOptions>) => Promise<{
|
|
11
|
+
export declare const configureEnrollmentsModule: ({ db, migrationRepository, options: enrollmentOptions, }: ModuleInput<EnrollmentsSettingsOptions>) => Promise<{
|
|
12
12
|
count: (query: EnrollmentQuery) => Promise<number>;
|
|
13
13
|
openEnrollmentWithProduct: ({ productId }: {
|
|
14
14
|
productId: string;
|
|
@@ -18,6 +18,9 @@ export declare const configureEnrollmentsModule: ({ db, options: enrollmentOptio
|
|
|
18
18
|
} | {
|
|
19
19
|
orderId: string;
|
|
20
20
|
}, options?: mongodb.FindOptions) => Promise<Enrollment | null>;
|
|
21
|
+
findEnrollmentsByOrderIds: ({ orderIds }: {
|
|
22
|
+
orderIds: string[];
|
|
23
|
+
}, options?: mongodb.FindOptions) => Promise<Enrollment[]>;
|
|
21
24
|
findEnrollments: ({ limit, offset, sort, ...query }: EnrollmentQuery & {
|
|
22
25
|
limit?: number;
|
|
23
26
|
offset?: number;
|
|
@@ -28,11 +31,12 @@ export declare const configureEnrollmentsModule: ({ db, options: enrollmentOptio
|
|
|
28
31
|
referenceDate?: Date;
|
|
29
32
|
}) => boolean;
|
|
30
33
|
addEnrollmentPeriod: (enrollmentId: string, period: EnrollmentPeriod) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
34
|
+
markEnrollmentTrialEndingNotified: (enrollmentId: string, { start, end }: EnrollmentPeriod) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
31
35
|
create: ({ countryCode, currencyCode, ...enrollmentData }: Omit<Enrollment, "status" | "periods" | "log" | "_id" | "created"> & Pick<Partial<Enrollment>, "_id" | "created">) => Promise<Enrollment>;
|
|
32
36
|
delete: (enrollmentId: string) => Promise<number>;
|
|
33
37
|
removeEnrollmentPeriodByOrderId: (enrollmentId: string, orderId: string) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
34
38
|
updateBillingAddress: (enrollmentId: string, fieldValue: Address) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
35
|
-
updateContact: (enrollmentId: string,
|
|
39
|
+
updateContact: (enrollmentId: string, contact: Contact) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
36
40
|
updateContext: (enrollmentId: string, fieldValue: any) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
37
41
|
updateDelivery: (enrollmentId: string, fieldValue: {
|
|
38
42
|
deliveryProviderId?: string;
|
|
@@ -42,6 +46,16 @@ export declare const configureEnrollmentsModule: ({ db, options: enrollmentOptio
|
|
|
42
46
|
paymentProviderId: string;
|
|
43
47
|
context?: any;
|
|
44
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>;
|
|
45
59
|
updatePlan: (enrollmentId: string, plan: EnrollmentPlan) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
46
60
|
updateStatus: (enrollmentId: string, { status, info }: {
|
|
47
61
|
status: EnrollmentStatus;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
import { SortDirection } from '@unchainedshop/utils';
|
|
1
|
+
import { SortDirection, normalizePhoneNumber } from '@unchainedshop/utils';
|
|
2
2
|
import { EnrollmentStatus, } from "../db/EnrollmentsCollection.js";
|
|
3
3
|
import { emit, registerEvents } from '@unchainedshop/events';
|
|
4
|
-
import { generateDbFilterById, buildSortOptions, mongodb, generateDbObjectId,
|
|
4
|
+
import { generateDbFilterById, buildSortOptions, mongodb, generateDbObjectId, } from '@unchainedshop/mongodb';
|
|
5
5
|
import { EnrollmentsCollection } from "../db/EnrollmentsCollection.js";
|
|
6
6
|
import { enrollmentsSettings } from "../enrollments-settings.js";
|
|
7
|
+
import normalizeContactPhoneMigration from "../migrations/20260625120100-normalize-contact-phone.js";
|
|
7
8
|
const ENROLLMENT_EVENTS = [
|
|
8
9
|
'ENROLLMENT_ADD_PERIOD',
|
|
9
10
|
'ENROLLMENT_CREATE',
|
|
10
11
|
'ENROLLMENT_REMOVE',
|
|
11
12
|
'ENROLLMENT_UPDATE',
|
|
13
|
+
'ENROLLMENT_SUSPEND',
|
|
14
|
+
'ENROLLMENT_RESUME',
|
|
15
|
+
'ENROLLMENT_PLAN_CHANGE',
|
|
16
|
+
'ENROLLMENT_TRIAL_ENDING',
|
|
12
17
|
];
|
|
18
|
+
const normalizeContactPhone = (contact, defaultCountry) => {
|
|
19
|
+
if (!contact?.telNumber)
|
|
20
|
+
return contact;
|
|
21
|
+
return {
|
|
22
|
+
...contact,
|
|
23
|
+
telNumber: normalizePhoneNumber(contact.telNumber, defaultCountry) || contact.telNumber,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
13
26
|
export const buildFindSelector = ({ queryString, status, userId }) => {
|
|
14
27
|
const selector = {
|
|
15
28
|
deleted: null,
|
|
@@ -19,12 +32,12 @@ export const buildFindSelector = ({ queryString, status, userId }) => {
|
|
|
19
32
|
if (userId)
|
|
20
33
|
selector.userId = userId;
|
|
21
34
|
if (queryString) {
|
|
22
|
-
assertDocumentDBCompatMode();
|
|
23
35
|
selector.$text = { $search: queryString };
|
|
24
36
|
}
|
|
25
37
|
return selector;
|
|
26
38
|
};
|
|
27
|
-
export const configureEnrollmentsModule = async ({ db, options: enrollmentOptions = {}, }) => {
|
|
39
|
+
export const configureEnrollmentsModule = async ({ db, migrationRepository, options: enrollmentOptions = {}, }) => {
|
|
40
|
+
normalizeContactPhoneMigration(migrationRepository);
|
|
28
41
|
registerEvents(ENROLLMENT_EVENTS);
|
|
29
42
|
enrollmentsSettings.configureSettings(enrollmentOptions);
|
|
30
43
|
const Enrollments = await EnrollmentsCollection(db);
|
|
@@ -62,18 +75,30 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
62
75
|
};
|
|
63
76
|
switch (status) {
|
|
64
77
|
case EnrollmentStatus.ACTIVE:
|
|
65
|
-
modifier.$set.enrollmentNumber =
|
|
78
|
+
modifier.$set.enrollmentNumber =
|
|
79
|
+
enrollment.enrollmentNumber || (await findNewEnrollmentNumber(enrollment));
|
|
66
80
|
break;
|
|
67
|
-
case EnrollmentStatus.TERMINATED:
|
|
68
|
-
|
|
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);
|
|
69
85
|
break;
|
|
86
|
+
}
|
|
70
87
|
default:
|
|
71
88
|
break;
|
|
72
89
|
}
|
|
90
|
+
if (enrollment.status === EnrollmentStatus.SUSPENDED)
|
|
91
|
+
modifier.$unset = { resumeAt: 1 };
|
|
73
92
|
const updatedEnrollment = await Enrollments.findOneAndUpdate(selector, modifier, {
|
|
74
93
|
returnDocument: 'after',
|
|
75
94
|
});
|
|
76
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
|
+
}
|
|
77
102
|
return updatedEnrollment;
|
|
78
103
|
};
|
|
79
104
|
const updateEnrollmentField = (fieldKey) => async (enrollmentId, fieldValue) => {
|
|
@@ -93,7 +118,9 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
93
118
|
},
|
|
94
119
|
openEnrollmentWithProduct: async ({ productId }) => {
|
|
95
120
|
const selector = { productId };
|
|
96
|
-
selector.status = {
|
|
121
|
+
selector.status = {
|
|
122
|
+
$in: [EnrollmentStatus.ACTIVE, EnrollmentStatus.PAUSED, EnrollmentStatus.SUSPENDED],
|
|
123
|
+
};
|
|
97
124
|
return Enrollments.findOne(selector);
|
|
98
125
|
},
|
|
99
126
|
findEnrollment: async (params, options) => {
|
|
@@ -102,6 +129,11 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
102
129
|
}
|
|
103
130
|
return Enrollments.findOne({ 'periods.orderId': params.orderId, deleted: null }, options);
|
|
104
131
|
},
|
|
132
|
+
findEnrollmentsByOrderIds: async ({ orderIds }, options) => {
|
|
133
|
+
if (!orderIds?.length)
|
|
134
|
+
return [];
|
|
135
|
+
return Enrollments.find({ 'periods.orderId': { $in: orderIds }, deleted: null }, options).toArray();
|
|
136
|
+
},
|
|
105
137
|
findEnrollments: async ({ limit, offset, sort, ...query }) => {
|
|
106
138
|
const defaultSortOption = [{ key: 'created', value: SortDirection.ASC }];
|
|
107
139
|
const enrollments = Enrollments.find(buildFindSelector(query), {
|
|
@@ -140,6 +172,16 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
140
172
|
await emit('ENROLLMENT_ADD_PERIOD', { enrollment });
|
|
141
173
|
return enrollment;
|
|
142
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
|
+
},
|
|
143
185
|
create: async ({ countryCode, currencyCode, ...enrollmentData }) => {
|
|
144
186
|
const { insertedId: enrollmentId } = await Enrollments.insertOne({
|
|
145
187
|
_id: generateDbObjectId(),
|
|
@@ -149,6 +191,7 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
149
191
|
periods: [],
|
|
150
192
|
currencyCode,
|
|
151
193
|
countryCode,
|
|
194
|
+
contact: normalizeContactPhone(enrollmentData.contact, enrollmentData.billingAddress?.countryCode || countryCode),
|
|
152
195
|
configuration: enrollmentData.configuration || [],
|
|
153
196
|
log: [],
|
|
154
197
|
});
|
|
@@ -179,10 +222,34 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
179
222
|
}, { returnDocument: 'after' });
|
|
180
223
|
},
|
|
181
224
|
updateBillingAddress: updateEnrollmentField('billingAddress'),
|
|
182
|
-
updateContact:
|
|
225
|
+
updateContact: async (enrollmentId, contact) => {
|
|
226
|
+
const existing = await Enrollments.findOne(generateDbFilterById(enrollmentId), {
|
|
227
|
+
projection: { billingAddress: true, countryCode: true },
|
|
228
|
+
});
|
|
229
|
+
const defaultCountry = existing?.billingAddress?.countryCode || existing?.countryCode;
|
|
230
|
+
return updateEnrollmentField('contact')(enrollmentId, normalizeContactPhone(contact, defaultCountry));
|
|
231
|
+
},
|
|
183
232
|
updateContext: updateEnrollmentField('meta'),
|
|
184
233
|
updateDelivery: updateEnrollmentField('delivery'),
|
|
185
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
|
+
},
|
|
186
253
|
updatePlan: async (enrollmentId, plan) => {
|
|
187
254
|
const enrollment = await Enrollments.findOneAndUpdate(generateDbFilterById(enrollmentId), {
|
|
188
255
|
$set: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
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
|
+
"files": [
|
|
6
|
+
"lib"
|
|
7
|
+
],
|
|
5
8
|
"main": "lib/enrollments-index.js",
|
|
6
9
|
"types": "lib/enrollments-index.d.ts",
|
|
7
10
|
"type": "module",
|
|
@@ -35,12 +38,12 @@
|
|
|
35
38
|
},
|
|
36
39
|
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"@unchainedshop/events": "^5.0.0-alpha.
|
|
39
|
-
"@unchainedshop/
|
|
40
|
-
"@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"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"typescript": "^
|
|
46
|
+
"@types/node": "^26.2.0",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
45
48
|
}
|
|
46
49
|
}
|