@unchainedshop/core-enrollments 5.0.0-alpha.1 → 5.0.0-alpha.10
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 +8 -84
- package/lib/db/EnrollmentsCollection.d.ts +1 -1
- package/lib/db/EnrollmentsCollection.js +25 -26
- 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 +5 -2
- package/lib/module/configureEnrollmentsModule.js +26 -5
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -13,97 +13,21 @@ 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 |
|
|
97
|
-
|
|
98
|
-
## Events
|
|
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`).
|
|
99
29
|
|
|
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 |
|
|
30
|
+
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
31
|
|
|
108
32
|
## License
|
|
109
33
|
|
|
@@ -1,4 +1,4 @@
|
|
|
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',
|
|
@@ -8,34 +8,33 @@ export const EnrollmentStatus = {
|
|
|
8
8
|
};
|
|
9
9
|
export const EnrollmentsCollection = async (db) => {
|
|
10
10
|
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',
|
|
11
|
+
await buildDbIndexes(Enrollments, [
|
|
12
|
+
{
|
|
13
|
+
index: {
|
|
14
|
+
_id: 'text',
|
|
15
|
+
userId: 'text',
|
|
16
|
+
enrollmentNumber: 'text',
|
|
17
|
+
status: 'text',
|
|
18
|
+
'contact.telNumber': 'text',
|
|
19
|
+
'contact.emailAddress': 'text',
|
|
20
|
+
},
|
|
21
|
+
options: {
|
|
22
|
+
weights: {
|
|
23
|
+
_id: 8,
|
|
24
|
+
userId: 3,
|
|
25
|
+
enrollmentNumber: 6,
|
|
26
|
+
'contact.telNumber': 5,
|
|
27
|
+
'contact.emailAddress': 4,
|
|
28
|
+
status: 1,
|
|
32
29
|
},
|
|
30
|
+
name: 'enrollment_fulltext_search',
|
|
33
31
|
},
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
36
34
|
await buildDbIndexes(Enrollments, [
|
|
37
|
-
{ index: {
|
|
38
|
-
{ index: {
|
|
35
|
+
{ index: { 'periods.orderId': 1 } },
|
|
36
|
+
{ index: { userId: 1, status: 1 } },
|
|
37
|
+
{ index: { productId: 1, status: 1 } },
|
|
39
38
|
{ index: { status: 1 } },
|
|
40
39
|
{ index: { enrollmentNumber: 1 } },
|
|
41
40
|
]);
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -32,7 +35,7 @@ export declare const configureEnrollmentsModule: ({ db, options: enrollmentOptio
|
|
|
32
35
|
delete: (enrollmentId: string) => Promise<number>;
|
|
33
36
|
removeEnrollmentPeriodByOrderId: (enrollmentId: string, orderId: string) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
34
37
|
updateBillingAddress: (enrollmentId: string, fieldValue: Address) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
35
|
-
updateContact: (enrollmentId: string,
|
|
38
|
+
updateContact: (enrollmentId: string, contact: Contact) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
36
39
|
updateContext: (enrollmentId: string, fieldValue: any) => Promise<mongodb.WithId<Enrollment> | null>;
|
|
37
40
|
updateDelivery: (enrollmentId: string, fieldValue: {
|
|
38
41
|
deliveryProviderId?: string;
|
|
@@ -1,15 +1,24 @@
|
|
|
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',
|
|
12
13
|
];
|
|
14
|
+
const normalizeContactPhone = (contact, defaultCountry) => {
|
|
15
|
+
if (!contact?.telNumber)
|
|
16
|
+
return contact;
|
|
17
|
+
return {
|
|
18
|
+
...contact,
|
|
19
|
+
telNumber: normalizePhoneNumber(contact.telNumber, defaultCountry) || contact.telNumber,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
13
22
|
export const buildFindSelector = ({ queryString, status, userId }) => {
|
|
14
23
|
const selector = {
|
|
15
24
|
deleted: null,
|
|
@@ -19,12 +28,12 @@ export const buildFindSelector = ({ queryString, status, userId }) => {
|
|
|
19
28
|
if (userId)
|
|
20
29
|
selector.userId = userId;
|
|
21
30
|
if (queryString) {
|
|
22
|
-
assertDocumentDBCompatMode();
|
|
23
31
|
selector.$text = { $search: queryString };
|
|
24
32
|
}
|
|
25
33
|
return selector;
|
|
26
34
|
};
|
|
27
|
-
export const configureEnrollmentsModule = async ({ db, options: enrollmentOptions = {}, }) => {
|
|
35
|
+
export const configureEnrollmentsModule = async ({ db, migrationRepository, options: enrollmentOptions = {}, }) => {
|
|
36
|
+
normalizeContactPhoneMigration(migrationRepository);
|
|
28
37
|
registerEvents(ENROLLMENT_EVENTS);
|
|
29
38
|
enrollmentsSettings.configureSettings(enrollmentOptions);
|
|
30
39
|
const Enrollments = await EnrollmentsCollection(db);
|
|
@@ -102,6 +111,11 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
102
111
|
}
|
|
103
112
|
return Enrollments.findOne({ 'periods.orderId': params.orderId, deleted: null }, options);
|
|
104
113
|
},
|
|
114
|
+
findEnrollmentsByOrderIds: async ({ orderIds }, options) => {
|
|
115
|
+
if (!orderIds?.length)
|
|
116
|
+
return [];
|
|
117
|
+
return Enrollments.find({ 'periods.orderId': { $in: orderIds }, deleted: null }, options).toArray();
|
|
118
|
+
},
|
|
105
119
|
findEnrollments: async ({ limit, offset, sort, ...query }) => {
|
|
106
120
|
const defaultSortOption = [{ key: 'created', value: SortDirection.ASC }];
|
|
107
121
|
const enrollments = Enrollments.find(buildFindSelector(query), {
|
|
@@ -149,6 +163,7 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
149
163
|
periods: [],
|
|
150
164
|
currencyCode,
|
|
151
165
|
countryCode,
|
|
166
|
+
contact: normalizeContactPhone(enrollmentData.contact, enrollmentData.billingAddress?.countryCode || countryCode),
|
|
152
167
|
configuration: enrollmentData.configuration || [],
|
|
153
168
|
log: [],
|
|
154
169
|
});
|
|
@@ -179,7 +194,13 @@ export const configureEnrollmentsModule = async ({ db, options: enrollmentOption
|
|
|
179
194
|
}, { returnDocument: 'after' });
|
|
180
195
|
},
|
|
181
196
|
updateBillingAddress: updateEnrollmentField('billingAddress'),
|
|
182
|
-
updateContact:
|
|
197
|
+
updateContact: async (enrollmentId, contact) => {
|
|
198
|
+
const existing = await Enrollments.findOne(generateDbFilterById(enrollmentId), {
|
|
199
|
+
projection: { billingAddress: true, countryCode: true },
|
|
200
|
+
});
|
|
201
|
+
const defaultCountry = existing?.billingAddress?.countryCode || existing?.countryCode;
|
|
202
|
+
return updateEnrollmentField('contact')(enrollmentId, normalizeContactPhone(contact, defaultCountry));
|
|
203
|
+
},
|
|
183
204
|
updateContext: updateEnrollmentField('meta'),
|
|
184
205
|
updateDelivery: updateEnrollmentField('delivery'),
|
|
185
206
|
updatePayment: updateEnrollmentField('payment'),
|
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.10",
|
|
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.10",
|
|
42
|
+
"@unchainedshop/mongodb": "^5.0.0-alpha.10",
|
|
43
|
+
"@unchainedshop/utils": "^5.0.0-alpha.10"
|
|
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
|
}
|