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.
- package/lib/core/crypto_service.d.ts +23 -0
- package/lib/core/crypto_service.js +100 -0
- package/lib/core/engine_base.d.ts +11 -0
- package/lib/core/engine_base.js +9 -0
- package/lib/core/index.d.ts +14 -0
- package/lib/core/index.js +101 -0
- package/lib/core/mail_service.d.ts +85 -0
- package/lib/core/mail_service.js +156 -0
- package/lib/core/orm/mail_blacklist_orm.d.ts +21 -0
- package/lib/core/orm/mail_blacklist_orm.js +79 -0
- package/lib/core/orm/orm.d.ts +27 -0
- package/lib/core/orm/orm.js +67 -0
- package/lib/core/sql_service.d.ts +195 -0
- package/lib/core/sql_service.js +333 -0
- package/lib/core/translate_service.d.ts +48 -0
- package/lib/core/translate_service.js +913 -0
- package/lib/core/types/crypto_types.d.ts +4 -0
- package/lib/core/types/crypto_types.js +2 -0
- package/lib/core/types/mail_types.d.ts +95 -0
- package/lib/core/types/mail_types.js +2 -0
- package/lib/core/types/site_config.d.ts +46 -0
- package/lib/core/types/site_config.js +2 -0
- package/lib/core/types/translate_types.d.ts +69 -0
- package/lib/core/types/translate_types.js +46 -0
- package/lib/core/types/user_types.d.ts +41 -0
- package/lib/core/types/user_types.js +2 -0
- package/lib/core/user_service.d.ts +87 -0
- package/lib/core/user_service.js +216 -0
- package/lib/express/index.d.ts +1 -0
- package/lib/express/index.js +18 -0
- package/lib/index.d.ts +17 -8
- package/lib/index.js +60 -16
- package/lib/mollie/index.d.ts +5 -0
- package/lib/mollie/index.js +62 -0
- package/lib/mollie/orm/customers_orm.d.ts +16 -0
- package/lib/mollie/orm/customers_orm.js +115 -0
- package/lib/mollie/orm/invoice_items_orm.d.ts +9 -0
- package/lib/mollie/orm/invoice_items_orm.js +71 -0
- package/lib/mollie/orm/invoice_payments_orm.d.ts +10 -0
- package/lib/mollie/orm/invoice_payments_orm.js +70 -0
- package/lib/mollie/orm/invoices_orm.d.ts +40 -0
- package/lib/mollie/orm/invoices_orm.js +172 -0
- package/lib/mollie/orm/subscription_items_orm.d.ts +9 -0
- package/lib/mollie/orm/subscription_items_orm.js +45 -0
- package/lib/mollie/orm/subscriptions_orm.d.ts +17 -0
- package/lib/mollie/orm/subscriptions_orm.js +122 -0
- package/lib/mollie/services/customer_service.d.ts +14 -0
- package/lib/mollie/services/customer_service.js +53 -0
- package/lib/mollie/services/invoice_service.d.ts +102 -0
- package/lib/mollie/services/invoice_service.js +866 -0
- package/lib/mollie/services/mollie_service.d.ts +42 -0
- package/lib/mollie/services/mollie_service.js +370 -0
- package/lib/mollie/services/subscription_service.d.ts +32 -0
- package/lib/mollie/services/subscription_service.js +134 -0
- package/lib/mollie/types/internal_types.d.ts +19 -0
- package/lib/mollie/types/internal_types.js +2 -0
- package/lib/mollie/types/mollie_types.d.ts +187 -0
- package/lib/mollie/types/mollie_types.js +3 -0
- package/lib/mollie/util/subscription_utils.d.ts +8 -0
- package/lib/mollie/util/subscription_utils.js +37 -0
- package/lib/schema/MySQLSchemaExtractor.d.ts +1 -1
- package/lib/schema/MySQLSchemaImporter.d.ts +1 -1
- 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
|
+
};
|