@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 CHANGED
@@ -13,97 +13,21 @@ npm install @unchainedshop/core-enrollments
13
13
 
14
14
  ## Usage
15
15
 
16
- ```typescript
17
- import { configureEnrollmentsModule, EnrollmentStatus } from '@unchainedshop/core-enrollments';
16
+ The platform initializes this module as `platform.unchainedAPI.modules.enrollments`.
18
17
 
19
- const enrollmentsModule = await configureEnrollmentsModule({ db });
18
+ ```typescript
19
+ import { EnrollmentStatus } from '@unchainedshop/core-enrollments';
20
20
 
21
- // Create an enrollment
22
- const enrollmentId = await enrollmentsModule.create({
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
- ## API Overview
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
- | Event | Description |
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
 
@@ -7,7 +7,7 @@ export interface EnrollmentPeriod {
7
7
  isTrial?: boolean;
8
8
  }
9
9
  export interface EnrollmentPlan {
10
- configuration: {
10
+ configuration?: {
11
11
  key: string;
12
12
  value: string;
13
13
  }[] | null;
@@ -1,4 +1,4 @@
1
- import { mongodb, buildDbIndexes, isDocumentDBCompatModeEnabled } from '@unchainedshop/mongodb';
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
- if (!isDocumentDBCompatModeEnabled()) {
12
- await buildDbIndexes(Enrollments, [
13
- {
14
- index: {
15
- _id: 'text',
16
- userId: 'text',
17
- enrollmentNumber: 'text',
18
- status: 'text',
19
- 'contact.telNumber': 'text',
20
- 'contact.emailAddress': 'text',
21
- },
22
- options: {
23
- weights: {
24
- _id: 8,
25
- userId: 3,
26
- enrollmentNumber: 6,
27
- 'contact.telNumber': 5,
28
- 'contact.emailAddress': 4,
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: { userId: 1 } },
38
- { index: { productId: 1 } },
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,2 @@
1
+ import type { MigrationRepository } from '@unchainedshop/mongodb';
2
+ export default function normalizeContactPhone(repository: MigrationRepository): void;
@@ -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, fieldValue: Contact) => Promise<mongodb.WithId<Enrollment> | null>;
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, assertDocumentDBCompatMode, } from '@unchainedshop/mongodb';
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: updateEnrollmentField('contact'),
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.1",
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.1",
39
- "@unchainedshop/logger": "^5.0.0-alpha.1",
40
- "@unchainedshop/utils": "^5.0.0-alpha.1"
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": "^25.0.0",
44
- "typescript": "^5.8.3"
46
+ "@types/node": "^26.2.0",
47
+ "typescript": "^6.0.3"
45
48
  }
46
49
  }