ztechno_core 0.0.105 → 0.0.108

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.
Files changed (63) hide show
  1. package/lib/core/crypto_service.d.ts +23 -0
  2. package/lib/core/crypto_service.js +100 -0
  3. package/lib/core/engine_base.d.ts +11 -0
  4. package/lib/core/engine_base.js +9 -0
  5. package/lib/core/index.d.ts +14 -0
  6. package/lib/core/index.js +101 -0
  7. package/lib/core/mail_service.d.ts +85 -0
  8. package/lib/core/mail_service.js +156 -0
  9. package/lib/core/orm/mail_blacklist_orm.d.ts +21 -0
  10. package/lib/core/orm/mail_blacklist_orm.js +79 -0
  11. package/lib/core/orm/orm.d.ts +27 -0
  12. package/lib/core/orm/orm.js +67 -0
  13. package/lib/core/sql_service.d.ts +195 -0
  14. package/lib/core/sql_service.js +333 -0
  15. package/lib/core/translate_service.d.ts +48 -0
  16. package/lib/core/translate_service.js +913 -0
  17. package/lib/core/types/crypto_types.d.ts +4 -0
  18. package/lib/core/types/crypto_types.js +2 -0
  19. package/lib/core/types/mail_types.d.ts +95 -0
  20. package/lib/core/types/mail_types.js +2 -0
  21. package/lib/core/types/site_config.d.ts +46 -0
  22. package/lib/core/types/site_config.js +2 -0
  23. package/lib/core/types/translate_types.d.ts +69 -0
  24. package/lib/core/types/translate_types.js +46 -0
  25. package/lib/core/types/user_types.d.ts +41 -0
  26. package/lib/core/types/user_types.js +2 -0
  27. package/lib/core/user_service.d.ts +87 -0
  28. package/lib/core/user_service.js +216 -0
  29. package/lib/express/index.d.ts +1 -0
  30. package/lib/express/index.js +18 -0
  31. package/lib/index.d.ts +17 -8
  32. package/lib/index.js +60 -16
  33. package/lib/mollie/index.d.ts +5 -0
  34. package/lib/mollie/index.js +62 -0
  35. package/lib/mollie/orm/customers_orm.d.ts +16 -0
  36. package/lib/mollie/orm/customers_orm.js +115 -0
  37. package/lib/mollie/orm/invoice_items_orm.d.ts +9 -0
  38. package/lib/mollie/orm/invoice_items_orm.js +71 -0
  39. package/lib/mollie/orm/invoice_payments_orm.d.ts +10 -0
  40. package/lib/mollie/orm/invoice_payments_orm.js +70 -0
  41. package/lib/mollie/orm/invoices_orm.d.ts +40 -0
  42. package/lib/mollie/orm/invoices_orm.js +172 -0
  43. package/lib/mollie/orm/subscription_items_orm.d.ts +9 -0
  44. package/lib/mollie/orm/subscription_items_orm.js +45 -0
  45. package/lib/mollie/orm/subscriptions_orm.d.ts +17 -0
  46. package/lib/mollie/orm/subscriptions_orm.js +122 -0
  47. package/lib/mollie/services/customer_service.d.ts +14 -0
  48. package/lib/mollie/services/customer_service.js +53 -0
  49. package/lib/mollie/services/invoice_service.d.ts +102 -0
  50. package/lib/mollie/services/invoice_service.js +866 -0
  51. package/lib/mollie/services/mollie_service.d.ts +42 -0
  52. package/lib/mollie/services/mollie_service.js +370 -0
  53. package/lib/mollie/services/subscription_service.d.ts +32 -0
  54. package/lib/mollie/services/subscription_service.js +134 -0
  55. package/lib/mollie/types/internal_types.d.ts +19 -0
  56. package/lib/mollie/types/internal_types.js +2 -0
  57. package/lib/mollie/types/mollie_types.d.ts +187 -0
  58. package/lib/mollie/types/mollie_types.js +3 -0
  59. package/lib/mollie/util/subscription_utils.d.ts +8 -0
  60. package/lib/mollie/util/subscription_utils.js +37 -0
  61. package/lib/schema/MySQLSchemaExtractor.d.ts +1 -1
  62. package/lib/schema/MySQLSchemaImporter.d.ts +1 -1
  63. package/package.json +3 -1
@@ -0,0 +1,42 @@
1
+ import { Payment, PaymentStatus, Locale, Customer, Subscription, SubscriptionCreateParams } from '@mollie/api-client';
2
+ import { ZSQLService } from '../../core';
3
+ import { RecoveryStats } from '../types/mollie_types';
4
+ export declare class MollieService {
5
+ private opt;
6
+ protected key: string;
7
+ readonly webhookUrl: string;
8
+ constructor(opt: {
9
+ sqlService: ZSQLService;
10
+ env: 'PROD' | 'DEV';
11
+ apiKeyLive?: string;
12
+ apiKeyTest?: string;
13
+ webhookUrl: string;
14
+ });
15
+ private get client();
16
+ resetDatabase(options?: { dryRun?: boolean }): Promise<{
17
+ dryRun: boolean;
18
+ deleted: Record<string, number>;
19
+ }>;
20
+ recoverFromMollie(options?: { dryRun?: boolean }): Promise<RecoveryStats>;
21
+ createCustomer(payload: {
22
+ name: string;
23
+ email: string;
24
+ locale?: Locale;
25
+ metadata?: Record<string, any>;
26
+ }): Promise<Customer>;
27
+ getCustomer(customerId: string): Promise<Customer>;
28
+ createSubscription(
29
+ input: SubscriptionCreateParams & {
30
+ customerId: string;
31
+ },
32
+ ): Promise<Subscription>;
33
+ getSubscription(customerId: string, subscriptionId: string): Promise<Subscription>;
34
+ cancelSubscription(customerId: string, subscriptionId: string): Promise<Subscription>;
35
+ getPayment(id: string): Promise<Payment>;
36
+ listPayments(opt?: {
37
+ limit?: number;
38
+ profileId?: string;
39
+ customerId?: string;
40
+ }): Promise<import('@mollie/api-client').Page<Payment>>;
41
+ mapPaymentStatus(status: PaymentStatus): 'pending' | 'paid' | 'failed' | 'canceled' | 'expired' | 'refunded';
42
+ }
@@ -0,0 +1,370 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.MollieService = void 0;
4
+ const api_client_1 = require('@mollie/api-client');
5
+ const customers_orm_1 = require('../orm/customers_orm');
6
+ const invoice_payments_orm_1 = require('../orm/invoice_payments_orm');
7
+ const invoices_orm_1 = require('../orm/invoices_orm');
8
+ const subscriptions_orm_1 = require('../orm/subscriptions_orm');
9
+ const orm_1 = require('../../core/orm/orm');
10
+ const invoice_items_orm_1 = require('../orm/invoice_items_orm');
11
+ const subscription_items_orm_1 = require('../orm/subscription_items_orm');
12
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
13
+ class MollieService {
14
+ constructor(opt) {
15
+ this.opt = opt;
16
+ this.webhookUrl = opt.webhookUrl;
17
+ switch (opt.env) {
18
+ case 'PROD':
19
+ if (!opt.apiKeyLive) {
20
+ throw new Error('MollieService is missing MOLLIE_LIVE_KEY');
21
+ }
22
+ this.key = opt.apiKeyLive;
23
+ break;
24
+ case 'DEV':
25
+ if (!opt.apiKeyTest) {
26
+ throw new Error('MollieService is missing MOLLIE_TEST_KEY');
27
+ }
28
+ this.key = opt.apiKeyTest;
29
+ break;
30
+ }
31
+ }
32
+ get client() {
33
+ const apiKey = this.key;
34
+ if (!apiKey || apiKey === '') {
35
+ throw new Error('MOLLIE_LIVE_KEY or MOLLIE_TEST_KEY is missing');
36
+ }
37
+ return (0, api_client_1.createMollieClient)({ apiKey });
38
+ }
39
+ async resetDatabase(options) {
40
+ const customersOrm = new customers_orm_1.CustomersOrm({ sqlService: this.opt.sqlService });
41
+ const subscriptionsOrm = new subscriptions_orm_1.SubscriptionsOrm({ sqlService: this.opt.sqlService });
42
+ const subscriptionItemsOrm = new subscription_items_orm_1.SubscriptionItemsOrm({ sqlService: this.opt.sqlService });
43
+ const invoicesOrm = new invoices_orm_1.InvoicesOrm({ sqlService: this.opt.sqlService });
44
+ const invoiceItemsOrm = new invoice_items_orm_1.InvoiceItemsOrm({ sqlService: this.opt.sqlService });
45
+ const paymentsOrm = new invoice_payments_orm_1.InvoicePaymentsOrm({ sqlService: this.opt.sqlService });
46
+ // Child tables first, then parent tables (respect FK ordering)
47
+ const orms = [invoiceItemsOrm, paymentsOrm, invoicesOrm, subscriptionItemsOrm, subscriptionsOrm, customersOrm];
48
+ if (options?.dryRun) {
49
+ const counts = {};
50
+ for (const orm of orms) {
51
+ const rows = await this.opt.sqlService.exec({
52
+ query: `SELECT COUNT(*) AS cnt FROM \`${orm.alias}\``,
53
+ });
54
+ counts[orm.alias] = rows[0]?.cnt ?? 0;
55
+ }
56
+ return { dryRun: true, deleted: counts };
57
+ }
58
+ return await this.opt.sqlService.transaction(async (trx) => {
59
+ const deleted = {};
60
+ trx.query('SET FOREIGN_KEY_CHECKS = 0');
61
+ for (const orm of orms) {
62
+ const res = await trx.query(`DELETE FROM \`${orm.alias}\``);
63
+ deleted[orm.alias] = res?.affectedRows ?? 0;
64
+ }
65
+ await trx.query('SET FOREIGN_KEY_CHECKS = 1');
66
+ return { dryRun: false, deleted };
67
+ });
68
+ }
69
+ async recoverFromMollie(options) {
70
+ const dryRun = options?.dryRun ?? false;
71
+ const mollie = this.client;
72
+ const startedAt = new Date();
73
+ const customersOrm = new customers_orm_1.CustomersOrm({ sqlService: this.opt.sqlService });
74
+ const subscriptionsOrm = new subscriptions_orm_1.SubscriptionsOrm({ sqlService: this.opt.sqlService });
75
+ const subscriptionItemsOrm = new subscription_items_orm_1.SubscriptionItemsOrm({ sqlService: this.opt.sqlService });
76
+ const invoicesOrm = new invoices_orm_1.InvoicesOrm({ sqlService: this.opt.sqlService });
77
+ const invoiceItemsOrm = new invoice_items_orm_1.InvoiceItemsOrm({ sqlService: this.opt.sqlService });
78
+ const paymentsOrm = new invoice_payments_orm_1.InvoicePaymentsOrm({ sqlService: this.opt.sqlService });
79
+ const stats = {
80
+ dryRun,
81
+ startedAt: startedAt.toISOString(),
82
+ durationMs: 0,
83
+ customers: { recovered: 0, skipped: 0 },
84
+ subscriptions: { recovered: 0 },
85
+ invoices: { recovered: 0, skippedExisting: 0 },
86
+ payments: { recovered: 0, skippedNoCustomer: 0 },
87
+ errors: [],
88
+ };
89
+ // 1. Recover customers
90
+ const allCustomers = [];
91
+ let customerPage = await mollie.customers.page();
92
+ while (customerPage.length > 0) {
93
+ for (const mc of customerPage) {
94
+ try {
95
+ // Re-run safety: check if local record has richer data
96
+ const existing = await customersOrm.findByMollieCustomerId(mc.id);
97
+ if (existing) {
98
+ const hasLocalData = existing.company || existing.phone || existing.address_line1 || existing.btw_nummer;
99
+ if (hasLocalData) {
100
+ // Only update Mollie-exclusive fields, don't blank out local data
101
+ if (!dryRun) {
102
+ await customersOrm.update(existing.id, {
103
+ name: mc.name ?? existing.name,
104
+ email: mc.email ?? existing.email,
105
+ locale: mc.locale ?? existing.locale,
106
+ metadata: mc.metadata ?? existing.metadata,
107
+ });
108
+ }
109
+ allCustomers.push({ mollieId: mc.id, localId: existing.id });
110
+ stats.customers.skipped++;
111
+ continue;
112
+ }
113
+ }
114
+ if (!dryRun) {
115
+ // Extract customer details from Mollie metadata
116
+ const meta = mc.metadata ?? {};
117
+ await customersOrm.create({
118
+ mollie_customer_id: mc.id,
119
+ email: mc.email ?? '',
120
+ name: mc.name ?? '',
121
+ company: meta.company ?? null,
122
+ phone: meta.phone ?? null,
123
+ btw_nummer: meta.btw_nummer ?? null,
124
+ address_line1: meta.address_line1 ?? null,
125
+ address_line2: meta.address_line2 ?? null,
126
+ postal_code: meta.postal_code ?? null,
127
+ city: meta.city ?? null,
128
+ country: meta.country ?? null,
129
+ locale: mc.locale ?? null,
130
+ metadata: mc.metadata ?? null,
131
+ });
132
+ }
133
+ const local = dryRun ? existing : await customersOrm.findByEmail(mc.email ?? '');
134
+ if (local) {
135
+ allCustomers.push({ mollieId: mc.id, localId: local.id });
136
+ }
137
+ stats.customers.recovered++;
138
+ } catch (err) {
139
+ stats.errors.push({ entity: 'customer', mollieId: mc.id, error: err?.message ?? String(err) });
140
+ }
141
+ }
142
+ if (customerPage.nextPageCursor) {
143
+ await sleep(100);
144
+ customerPage = await mollie.customers.page({ from: customerPage.nextPageCursor });
145
+ } else break;
146
+ }
147
+ // 2. Recover subscriptions per customer
148
+ for (const { mollieId, localId } of allCustomers) {
149
+ let subPage = await mollie.customerSubscriptions.page({ customerId: mollieId });
150
+ while (subPage.length > 0) {
151
+ for (const ms of subPage) {
152
+ try {
153
+ if (!dryRun) {
154
+ await subscriptionsOrm.create({
155
+ customer_id: localId,
156
+ mollie_customer_id: mollieId,
157
+ mollie_subscription_id: ms.id,
158
+ status: ms.status,
159
+ interval: ms.interval,
160
+ description: ms.description ?? null,
161
+ amount: Number(ms.amount.value),
162
+ currency: ms.amount.currency,
163
+ mandate_id: ms.mandateId ?? null,
164
+ next_payment_date: (0, orm_1.toDatetime)(ms.nextPaymentDate) ?? null,
165
+ canceled_at: (0, orm_1.toDatetime)(ms.canceledAt) ?? null,
166
+ metadata: ms.metadata ?? null,
167
+ });
168
+ // Recover subscription items from metadata
169
+ const subMeta = ms.metadata ?? {};
170
+ if (Array.isArray(subMeta.items) && subMeta.items.length > 0) {
171
+ const localSub = await subscriptionsOrm.findByMollieSubscriptionId(ms.id);
172
+ if (localSub) {
173
+ const existingItems = await subscriptionItemsOrm.findBySubscription(localSub.id);
174
+ if (existingItems.length === 0) {
175
+ await subscriptionItemsOrm.bulkInsert(
176
+ subMeta.items.map((it, idx) => {
177
+ const unitPrice = Number(it.u);
178
+ const quantity = Number(it.q);
179
+ const vatRate = Number(it.v ?? 0);
180
+ const totalExVat = Number((unitPrice * quantity).toFixed(2));
181
+ const totalIncVat = Number((totalExVat * (1 + vatRate / 100)).toFixed(2));
182
+ return {
183
+ subscription_id: localSub.id,
184
+ description: it.d,
185
+ quantity,
186
+ unit_price: unitPrice,
187
+ vat_rate: vatRate,
188
+ total_ex_vat: totalExVat,
189
+ total_inc_vat: totalIncVat,
190
+ sort_order: idx,
191
+ };
192
+ }),
193
+ );
194
+ }
195
+ }
196
+ }
197
+ }
198
+ stats.subscriptions.recovered++;
199
+ } catch (err) {
200
+ stats.errors.push({ entity: 'subscription', mollieId: ms.id, error: err?.message ?? String(err) });
201
+ }
202
+ }
203
+ if (subPage.nextPageCursor) {
204
+ await sleep(100);
205
+ subPage = await mollie.customerSubscriptions.page({ customerId: mollieId, from: subPage.nextPageCursor });
206
+ } else break;
207
+ }
208
+ }
209
+ // 3. Recover payments → create skeleton invoices
210
+ let payPage = await mollie.payments.page({ limit: 250 });
211
+ while (payPage.length > 0) {
212
+ for (const mp of payPage) {
213
+ try {
214
+ const meta = mp.metadata ?? {};
215
+ const customerId = allCustomers.find((c) => c.mollieId === mp.customerId)?.localId;
216
+ if (!customerId) {
217
+ stats.payments.skippedNoCustomer++;
218
+ continue;
219
+ }
220
+ // Improved invoice numbering: use metadata or REC-YYYY-paymentId
221
+ const createdYear = mp.createdAt ? new Date(mp.createdAt).getFullYear() : new Date().getFullYear();
222
+ const invoiceNumber = meta.invoice_number ?? `REC-${createdYear}-${mp.id}`;
223
+ // Group payments: check if invoice already exists before creating
224
+ let invoice = await invoicesOrm.findByInvoiceNumber(invoiceNumber);
225
+ if (invoice) {
226
+ stats.invoices.skippedExisting++;
227
+ } else {
228
+ // Use mapPaymentStatus for correct status mapping
229
+ const mappedStatus = this.mapPaymentStatus(mp.status);
230
+ // Link to subscription if available
231
+ let subscriptionId = null;
232
+ const mollieSubId = mp.subscriptionId ?? null;
233
+ if (mollieSubId) {
234
+ const localSub = await subscriptionsOrm.findByMollieSubscriptionId(mollieSubId);
235
+ if (localSub) subscriptionId = localSub.id;
236
+ }
237
+ if (!dryRun) {
238
+ await invoicesOrm.create({
239
+ invoice_number: invoiceNumber,
240
+ customer_id: customerId,
241
+ subscription_id: subscriptionId,
242
+ subscription_period_start: null,
243
+ subscription_period_end: null,
244
+ mollie_customer_id: mp.customerId ?? null,
245
+ mollie_payment_id: mp.id,
246
+ pay_token_hash: null,
247
+ pay_token_expires_at: null,
248
+ pay_token_finalized_at: null,
249
+ status: mappedStatus,
250
+ amount_due: Number(mp.amount.value),
251
+ amount_paid: mappedStatus === 'paid' ? Number(mp.amount.value) : 0,
252
+ currency: mp.amount.currency,
253
+ description: mp.description ?? null,
254
+ payment_terms: meta.payment_terms ?? null,
255
+ due_date: meta.due_date ?? null,
256
+ issued_at: (0, orm_1.toDatetime)(mp.createdAt) ?? null,
257
+ paid_at: (0, orm_1.toDatetime)(mp.paidAt) ?? null,
258
+ checkout_url: mp._links?.checkout?.href ?? null,
259
+ metadata: mp.metadata ?? null,
260
+ });
261
+ invoice = await invoicesOrm.findByInvoiceNumber(invoiceNumber);
262
+ // Recover invoice line items from payment metadata
263
+ if (invoice && Array.isArray(meta.items) && meta.items.length > 0) {
264
+ await invoiceItemsOrm.bulkInsert(
265
+ meta.items.map((it, idx) => {
266
+ const unitPrice = Number(it.u);
267
+ const quantity = Number(it.q);
268
+ const vatRate = Number(it.v ?? 0);
269
+ const totalExVat = Number((unitPrice * quantity).toFixed(2));
270
+ const totalIncVat = Number((totalExVat * (1 + vatRate / 100)).toFixed(2));
271
+ return {
272
+ invoice_id: invoice.id,
273
+ item_type: it.t ?? 'service',
274
+ description: it.d,
275
+ quantity,
276
+ unit_price: unitPrice,
277
+ vat_rate: vatRate,
278
+ total_ex_vat: totalExVat,
279
+ total_inc_vat: totalIncVat,
280
+ sort_order: idx,
281
+ };
282
+ }),
283
+ );
284
+ }
285
+ }
286
+ stats.invoices.recovered++;
287
+ }
288
+ if (invoice && !dryRun) {
289
+ await paymentsOrm.upsert({
290
+ invoice_id: invoice.id,
291
+ mollie_payment_id: mp.id,
292
+ status: mp.status,
293
+ sequence_type: mp.sequenceType ?? null,
294
+ mollie_subscription_id: mp.subscriptionId ?? null,
295
+ method: mp.method ?? null,
296
+ amount: Number(mp.amount.value),
297
+ currency: mp.amount.currency,
298
+ checkout_url: mp._links?.checkout?.href ?? null,
299
+ paid_at: (0, orm_1.toDatetime)(mp.paidAt) ?? null,
300
+ expires_at: (0, orm_1.toDatetime)(mp.expiresAt) ?? null,
301
+ mandate_id: mp.mandateId ?? null,
302
+ });
303
+ }
304
+ stats.payments.recovered++;
305
+ } catch (err) {
306
+ stats.errors.push({ entity: 'payment', mollieId: mp.id, error: err?.message ?? String(err) });
307
+ }
308
+ }
309
+ if (payPage.nextPageCursor) {
310
+ await sleep(100);
311
+ payPage = await mollie.payments.page({ from: payPage.nextPageCursor });
312
+ } else break;
313
+ }
314
+ stats.durationMs = Date.now() - startedAt.getTime();
315
+ console.log(
316
+ `[Recovery] ${dryRun ? 'DRY RUN' : 'COMPLETE'} — ` +
317
+ `customers: ${stats.customers.recovered} recovered / ${stats.customers.skipped} skipped, ` +
318
+ `subscriptions: ${stats.subscriptions.recovered}, ` +
319
+ `invoices: ${stats.invoices.recovered} recovered / ${stats.invoices.skippedExisting} existing, ` +
320
+ `payments: ${stats.payments.recovered} recovered / ${stats.payments.skippedNoCustomer} skipped (no customer), ` +
321
+ `errors: ${stats.errors.length}, ` +
322
+ `duration: ${stats.durationMs}ms`,
323
+ );
324
+ return stats;
325
+ }
326
+ async createCustomer(payload) {
327
+ return await this.client.customers.create(payload);
328
+ }
329
+ async getCustomer(customerId) {
330
+ return await this.client.customers.get(customerId);
331
+ }
332
+ /** @internal */
333
+ async createPayment(input) {
334
+ return await this.client.payments.create({
335
+ ...input,
336
+ sequenceType: input.sequenceType ?? 'oneoff',
337
+ mandateId: input.mandateId ?? undefined,
338
+ });
339
+ }
340
+ async createSubscription(input) {
341
+ return await this.client.customerSubscriptions.create(input);
342
+ }
343
+ async getSubscription(customerId, subscriptionId) {
344
+ return await this.client.customerSubscriptions.get(subscriptionId, { customerId });
345
+ }
346
+ async cancelSubscription(customerId, subscriptionId) {
347
+ return await this.client.customerSubscriptions.cancel(subscriptionId, { customerId });
348
+ }
349
+ async getPayment(id) {
350
+ return await this.client.payments.get(id);
351
+ }
352
+ async listPayments(opt) {
353
+ return await this.client.payments.page({ limit: opt?.limit ?? 250 });
354
+ }
355
+ mapPaymentStatus(status) {
356
+ switch (status) {
357
+ case 'paid':
358
+ return 'paid';
359
+ case 'failed':
360
+ return 'failed';
361
+ case 'canceled':
362
+ return 'canceled';
363
+ case 'expired':
364
+ return 'expired';
365
+ default:
366
+ return 'pending';
367
+ }
368
+ }
369
+ }
370
+ exports.MollieService = MollieService;
@@ -0,0 +1,32 @@
1
+ import { ZSQLService } from '../../core';
2
+ import { ZSubscription, CreateSubscriptionInput, ZSubscriptionItem } from '../types/mollie_types';
3
+ import { CustomerService } from './customer_service';
4
+ import { InvoiceService } from './invoice_service';
5
+ import { MollieService } from './mollie_service';
6
+ export declare class SubscriptionService {
7
+ private opt;
8
+ private subscriptionsOrm;
9
+ private itemsOrm;
10
+ constructor(opt: {
11
+ sqlService: ZSQLService;
12
+ mollieService: MollieService;
13
+ customerService: CustomerService;
14
+ invoiceService: InvoiceService;
15
+ });
16
+ autoInit(): Promise<void>;
17
+ private calcTotals;
18
+ list(): Promise<ZSubscription[]>;
19
+ get(id: number): Promise<{
20
+ subscription: ZSubscription;
21
+ items: ZSubscriptionItem[];
22
+ }>;
23
+ createSubscription(input: CreateSubscriptionInput): Promise<{
24
+ subscription: ZSubscription;
25
+ invoice: import('../types/mollie_types').ZInvoice;
26
+ payUrl: string;
27
+ payTokenExpiresAt: string;
28
+ checkoutUrl: string;
29
+ }>;
30
+ cancelSubscription(id: number): Promise<ZSubscription>;
31
+ getNextStartDate(interval: string, baseDate?: Date): string;
32
+ }
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });
3
+ exports.SubscriptionService = void 0;
4
+ const orm_1 = require('../../core/orm/orm');
5
+ const subscription_items_orm_1 = require('../orm/subscription_items_orm');
6
+ const subscriptions_orm_1 = require('../orm/subscriptions_orm');
7
+ const subscription_utils_1 = require('../util/subscription_utils');
8
+ class SubscriptionService {
9
+ constructor(opt) {
10
+ this.opt = opt;
11
+ this.subscriptionsOrm = new subscriptions_orm_1.SubscriptionsOrm({ sqlService: this.opt.sqlService });
12
+ this.itemsOrm = new subscription_items_orm_1.SubscriptionItemsOrm({ sqlService: this.opt.sqlService });
13
+ }
14
+ async autoInit() {
15
+ await this.subscriptionsOrm.ensureTableExists();
16
+ await this.itemsOrm.ensureTableExists();
17
+ }
18
+ calcTotals(items) {
19
+ let amount_due = 0;
20
+ const mapped = items.map((item, idx) => {
21
+ const total_ex_vat = Number(item.quantity) * Number(item.unit_price);
22
+ const total_inc_vat = total_ex_vat * (1 + Number(item.vat_rate || 0) / 100);
23
+ amount_due += total_inc_vat;
24
+ return {
25
+ subscription_id: 0,
26
+ description: item.description,
27
+ quantity: Number(item.quantity),
28
+ unit_price: Number(item.unit_price),
29
+ vat_rate: Number(item.vat_rate || 0),
30
+ total_ex_vat,
31
+ total_inc_vat,
32
+ sort_order: item.sort_order ?? idx,
33
+ };
34
+ });
35
+ return { amount_due: Number(amount_due.toFixed(2)), items: mapped };
36
+ }
37
+ async list() {
38
+ return await this.subscriptionsOrm.findAll();
39
+ }
40
+ async get(id) {
41
+ const subscription = await this.subscriptionsOrm.findById(id);
42
+ if (!subscription) {
43
+ throw new Error(`Subscription ${id} not found`);
44
+ }
45
+ const items = await this.itemsOrm.findBySubscription(subscription.id);
46
+ return { subscription, items };
47
+ }
48
+ async createSubscription(input) {
49
+ if (!Array.isArray(input.items) || input.items.length === 0) {
50
+ throw new Error('Subscription items are required');
51
+ }
52
+ const customer = await this.opt.customerService.findById(input.customer_id);
53
+ if (!customer) {
54
+ throw new Error(`Customer ${input.customer_id} not found`);
55
+ }
56
+ if (!customer.mollie_customer_id) {
57
+ throw new Error(`Customer ${customer.id} missing mollie_customer_id`);
58
+ }
59
+ (0, subscription_utils_1.parseSubscriptionInterval)(input.interval);
60
+ const { items, amount_due } = this.calcTotals(input.items);
61
+ const payload = {
62
+ customer_id: customer.id,
63
+ mollie_customer_id: customer.mollie_customer_id,
64
+ mollie_subscription_id: null,
65
+ status: 'setup_pending',
66
+ interval: input.interval.trim(),
67
+ description: input.description ?? null,
68
+ amount: amount_due,
69
+ currency: input.currency || 'EUR',
70
+ mandate_id: null,
71
+ next_payment_date: null,
72
+ canceled_at: null,
73
+ metadata: input.metadata ?? null,
74
+ };
75
+ const res = await this.subscriptionsOrm.create(payload);
76
+ const subscriptionId = res?.insertId;
77
+ const subscription = subscriptionId
78
+ ? await this.subscriptionsOrm.findById(subscriptionId)
79
+ : await this.subscriptionsOrm.findLatestByCustomer(customer.id);
80
+ if (!subscription) {
81
+ throw new Error('Failed to persist subscription');
82
+ }
83
+ const itemsWithSubscription = items.map((it) => ({ ...it, subscription_id: subscription.id }));
84
+ await this.itemsOrm.bulkInsert(itemsWithSubscription);
85
+ const invoiceResult = await this.opt.invoiceService.createInvoiceFromItems(
86
+ {
87
+ customer_id: customer.id,
88
+ description: input.description,
89
+ currency: input.currency || 'EUR',
90
+ items: input.items,
91
+ metadata: input.metadata ?? null,
92
+ },
93
+ {
94
+ subscription_id: subscription.id,
95
+ issuePayToken: true,
96
+ },
97
+ );
98
+ const payment = await this.opt.invoiceService.createMolliePaymentForInvoice(invoiceResult.invoice, {
99
+ sequenceType: 'first',
100
+ });
101
+ const pay = invoiceResult.pay ?? (await this.opt.invoiceService.ensurePayLink(invoiceResult.invoice.id));
102
+ const updatedInvoice = await this.opt.invoiceService.getInvoiceById(invoiceResult.invoice.id);
103
+ return {
104
+ subscription,
105
+ invoice: updatedInvoice || invoiceResult.invoice,
106
+ payUrl: pay.payUrl,
107
+ payTokenExpiresAt: pay.expiresAt,
108
+ checkoutUrl: payment?.getCheckoutUrl?.() ?? payment?._links?.checkout?.href ?? null,
109
+ };
110
+ }
111
+ async cancelSubscription(id) {
112
+ const subscription = await this.subscriptionsOrm.findById(id);
113
+ if (!subscription) {
114
+ throw new Error(`Subscription ${id} not found`);
115
+ }
116
+ if (subscription.mollie_subscription_id && subscription.mollie_customer_id) {
117
+ await this.opt.mollieService.cancelSubscription(
118
+ subscription.mollie_customer_id,
119
+ subscription.mollie_subscription_id,
120
+ );
121
+ }
122
+ await this.subscriptionsOrm.update(id, {
123
+ status: 'canceled',
124
+ canceled_at: (0, orm_1.formatDatetime)(new Date()),
125
+ });
126
+ return await this.subscriptionsOrm.findById(id);
127
+ }
128
+ getNextStartDate(interval, baseDate) {
129
+ const parsed = (0, subscription_utils_1.parseSubscriptionInterval)(interval);
130
+ const start = (0, subscription_utils_1.addSubscriptionInterval)(baseDate ?? new Date(), parsed);
131
+ return (0, subscription_utils_1.formatDateOnly)(start);
132
+ }
133
+ }
134
+ exports.SubscriptionService = SubscriptionService;
@@ -0,0 +1,19 @@
1
+ import { Locale, SequenceType } from '@mollie/api-client';
2
+ /**
3
+ * Internal type for building Mollie API payment requests.
4
+ * Consumers should use InvoiceService/SubscriptionService instead of calling MollieService.createPayment() directly.
5
+ */
6
+ export type ZCreatePaymentInput = {
7
+ amount: {
8
+ currency: string;
9
+ value: string;
10
+ };
11
+ description: string;
12
+ redirectUrl: string;
13
+ webhookUrl: string;
14
+ customerId?: string;
15
+ metadata?: Record<string, any>;
16
+ locale?: Locale;
17
+ sequenceType?: SequenceType;
18
+ mandateId?: string;
19
+ };
@@ -0,0 +1,2 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, '__esModule', { value: true });