ztechno_core 0.0.131 → 0.0.133
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/mollie/index.d.ts +3 -0
- package/lib/mollie/index.js +30 -1
- package/lib/mollie/orm/invoice_payments_orm.d.ts +12 -3
- package/lib/mollie/orm/invoice_payments_orm.js +31 -1
- package/lib/mollie/orm/invoice_status_log_orm.d.ts +9 -0
- package/lib/mollie/orm/invoice_status_log_orm.js +52 -0
- package/lib/mollie/orm/invoices_orm.d.ts +21 -4
- package/lib/mollie/orm/invoices_orm.js +56 -4
- package/lib/mollie/orm/payment_status_log_orm.d.ts +11 -0
- package/lib/mollie/orm/payment_status_log_orm.js +68 -0
- package/lib/mollie/services/invoice_audit_service.d.ts +38 -0
- package/lib/mollie/services/invoice_audit_service.js +178 -0
- package/lib/mollie/services/invoice_service.d.ts +8 -1
- package/lib/mollie/services/invoice_service.js +111 -44
- package/lib/mollie/types/mollie_types.d.ts +43 -1
- package/package.json +1 -1
package/lib/mollie/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { MollieService } from './services/mollie_service';
|
|
2
2
|
export { CustomerService } from './services/customer_service';
|
|
3
3
|
export { InvoiceService } from './services/invoice_service';
|
|
4
|
+
export { InvoiceAuditService } from './services/invoice_audit_service';
|
|
4
5
|
export { SubscriptionService } from './services/subscription_service';
|
|
6
|
+
export { InvoiceStatusLogOrm } from './orm/invoice_status_log_orm';
|
|
7
|
+
export { PaymentStatusLogOrm } from './orm/payment_status_log_orm';
|
|
5
8
|
export * from './types/mollie_types';
|
package/lib/mollie/index.js
CHANGED
|
@@ -26,7 +26,14 @@ var __exportStar =
|
|
|
26
26
|
if (p !== 'default' && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
29
|
-
exports.
|
|
29
|
+
exports.PaymentStatusLogOrm =
|
|
30
|
+
exports.InvoiceStatusLogOrm =
|
|
31
|
+
exports.SubscriptionService =
|
|
32
|
+
exports.InvoiceAuditService =
|
|
33
|
+
exports.InvoiceService =
|
|
34
|
+
exports.CustomerService =
|
|
35
|
+
exports.MollieService =
|
|
36
|
+
void 0;
|
|
30
37
|
// Services
|
|
31
38
|
var mollie_service_1 = require('./services/mollie_service');
|
|
32
39
|
Object.defineProperty(exports, 'MollieService', {
|
|
@@ -49,6 +56,13 @@ Object.defineProperty(exports, 'InvoiceService', {
|
|
|
49
56
|
return invoice_service_1.InvoiceService;
|
|
50
57
|
},
|
|
51
58
|
});
|
|
59
|
+
var invoice_audit_service_1 = require('./services/invoice_audit_service');
|
|
60
|
+
Object.defineProperty(exports, 'InvoiceAuditService', {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: function () {
|
|
63
|
+
return invoice_audit_service_1.InvoiceAuditService;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
52
66
|
var subscription_service_1 = require('./services/subscription_service');
|
|
53
67
|
Object.defineProperty(exports, 'SubscriptionService', {
|
|
54
68
|
enumerable: true,
|
|
@@ -56,6 +70,21 @@ Object.defineProperty(exports, 'SubscriptionService', {
|
|
|
56
70
|
return subscription_service_1.SubscriptionService;
|
|
57
71
|
},
|
|
58
72
|
});
|
|
73
|
+
// ORMs (audit log)
|
|
74
|
+
var invoice_status_log_orm_1 = require('./orm/invoice_status_log_orm');
|
|
75
|
+
Object.defineProperty(exports, 'InvoiceStatusLogOrm', {
|
|
76
|
+
enumerable: true,
|
|
77
|
+
get: function () {
|
|
78
|
+
return invoice_status_log_orm_1.InvoiceStatusLogOrm;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
var payment_status_log_orm_1 = require('./orm/payment_status_log_orm');
|
|
82
|
+
Object.defineProperty(exports, 'PaymentStatusLogOrm', {
|
|
83
|
+
enumerable: true,
|
|
84
|
+
get: function () {
|
|
85
|
+
return payment_status_log_orm_1.PaymentStatusLogOrm;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
59
88
|
// Public types (entities, inputs, outputs)
|
|
60
89
|
__exportStar(require('./types/mollie_types'), exports);
|
|
61
90
|
// internal_types.ts is intentionally NOT exported —
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { ZOrm } from '../../core/orm/orm';
|
|
2
2
|
import { ZSQLService } from '../../core/sql_service';
|
|
3
|
-
import { ZInvoicePayment } from '../types/mollie_types';
|
|
3
|
+
import { ZInvoicePayment, ZAuditActorType } from '../types/mollie_types';
|
|
4
|
+
import { PaymentStatusLogOrm } from './payment_status_log_orm';
|
|
4
5
|
export declare class InvoicePaymentsOrm extends ZOrm {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
private paymentLogOrm?;
|
|
7
|
+
constructor(opt: { sqlService: ZSQLService; alias?: string; paymentLogOrm?: PaymentStatusLogOrm });
|
|
8
|
+
setPaymentLogOrm(orm: PaymentStatusLogOrm): void;
|
|
9
|
+
upsert(
|
|
10
|
+
payment: Omit<ZInvoicePayment, 'id' | 'created_at' | 'updated_at'>,
|
|
11
|
+
audit?: {
|
|
12
|
+
actorType?: ZAuditActorType;
|
|
13
|
+
note?: string;
|
|
14
|
+
},
|
|
15
|
+
): Promise<void>;
|
|
7
16
|
findByPaymentId(mollie_payment_id: string): Promise<ZInvoicePayment>;
|
|
8
17
|
findByInvoice(invoice_id: number): Promise<ZInvoicePayment[]>;
|
|
9
18
|
createTable(): Promise<void>;
|
|
@@ -5,8 +5,19 @@ const orm_1 = require('../../core/orm/orm');
|
|
|
5
5
|
class InvoicePaymentsOrm extends orm_1.ZOrm {
|
|
6
6
|
constructor(opt) {
|
|
7
7
|
super({ sqlService: opt.sqlService, alias: opt.alias ?? 'mollie_invoice_payments' });
|
|
8
|
+
this.paymentLogOrm = opt.paymentLogOrm;
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
setPaymentLogOrm(orm) {
|
|
11
|
+
this.paymentLogOrm = orm;
|
|
12
|
+
}
|
|
13
|
+
async upsert(payment, audit) {
|
|
14
|
+
// Capture previous status for audit logging
|
|
15
|
+
let previousStatus = null;
|
|
16
|
+
let existingPayment;
|
|
17
|
+
if (this.paymentLogOrm) {
|
|
18
|
+
existingPayment = await this.findByPaymentId(payment.mollie_payment_id);
|
|
19
|
+
previousStatus = existingPayment?.status ?? null;
|
|
20
|
+
}
|
|
10
21
|
await this.sqlService.query(
|
|
11
22
|
/*SQL*/ `
|
|
12
23
|
INSERT INTO \`${this.alias}\`
|
|
@@ -28,6 +39,25 @@ class InvoicePaymentsOrm extends orm_1.ZOrm {
|
|
|
28
39
|
`,
|
|
29
40
|
payment,
|
|
30
41
|
);
|
|
42
|
+
// Audit log: record status change (or initial insert)
|
|
43
|
+
if (this.paymentLogOrm) {
|
|
44
|
+
const newStatus = payment.status;
|
|
45
|
+
if (previousStatus !== newStatus) {
|
|
46
|
+
// Fetch the persisted record to get its id
|
|
47
|
+
const persisted = existingPayment ?? (await this.findByPaymentId(payment.mollie_payment_id));
|
|
48
|
+
if (persisted) {
|
|
49
|
+
await this.paymentLogOrm.insert({
|
|
50
|
+
payment_id: persisted.id,
|
|
51
|
+
invoice_id: payment.invoice_id,
|
|
52
|
+
mollie_payment_id: payment.mollie_payment_id,
|
|
53
|
+
from_status: previousStatus,
|
|
54
|
+
to_status: newStatus,
|
|
55
|
+
actor_type: audit?.actorType ?? 'system',
|
|
56
|
+
note: audit?.note ?? null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
31
61
|
}
|
|
32
62
|
async findByPaymentId(mollie_payment_id) {
|
|
33
63
|
const res = await this.sqlService.exec({
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ZOrm } from '../../core/orm/orm';
|
|
2
|
+
import { ZSQLService } from '../../core/sql_service';
|
|
3
|
+
import { ZInvoiceStatusLogEntry } from '../types/mollie_types';
|
|
4
|
+
export declare class InvoiceStatusLogOrm extends ZOrm {
|
|
5
|
+
constructor(opt: { sqlService: ZSQLService; alias?: string });
|
|
6
|
+
insert(entry: Omit<ZInvoiceStatusLogEntry, 'id' | 'created_at'>): Promise<void>;
|
|
7
|
+
findByInvoiceId(invoiceId: number): Promise<ZInvoiceStatusLogEntry[]>;
|
|
8
|
+
createTable(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.InvoiceStatusLogOrm = void 0;
|
|
4
|
+
const orm_1 = require('../../core/orm/orm');
|
|
5
|
+
class InvoiceStatusLogOrm extends orm_1.ZOrm {
|
|
6
|
+
constructor(opt) {
|
|
7
|
+
super({ sqlService: opt.sqlService, alias: opt.alias ?? 'mollie_invoice_status_log' });
|
|
8
|
+
}
|
|
9
|
+
async insert(entry) {
|
|
10
|
+
await this.sqlService.query(
|
|
11
|
+
/*SQL*/ `
|
|
12
|
+
INSERT INTO \`${this.alias}\`
|
|
13
|
+
(invoice_id, from_status, to_status, actor_type, mollie_payment_id, note, metadata)
|
|
14
|
+
VALUES
|
|
15
|
+
(:invoice_id, :from_status, :to_status, :actor_type, :mollie_payment_id, :note, :metadata)
|
|
16
|
+
`,
|
|
17
|
+
{
|
|
18
|
+
invoice_id: entry.invoice_id,
|
|
19
|
+
from_status: entry.from_status ?? null,
|
|
20
|
+
to_status: entry.to_status,
|
|
21
|
+
actor_type: entry.actor_type,
|
|
22
|
+
mollie_payment_id: entry.mollie_payment_id ?? null,
|
|
23
|
+
note: entry.note ?? null,
|
|
24
|
+
metadata: entry.metadata != null ? JSON.stringify(entry.metadata) : null,
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
async findByInvoiceId(invoiceId) {
|
|
29
|
+
return await this.sqlService.exec({
|
|
30
|
+
query: `SELECT * FROM \`${this.alias}\` WHERE invoice_id=:invoiceId ORDER BY created_at ASC, id ASC`,
|
|
31
|
+
params: { invoiceId },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async createTable() {
|
|
35
|
+
await this.sqlService.query(/*SQL*/ `
|
|
36
|
+
CREATE TABLE IF NOT EXISTS \`${this.alias}\` (
|
|
37
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
38
|
+
invoice_id BIGINT UNSIGNED NOT NULL,
|
|
39
|
+
from_status ENUM('draft','pending','paid','failed','canceled','expired','refunded') NULL,
|
|
40
|
+
to_status ENUM('draft','pending','paid','failed','canceled','expired','refunded') NOT NULL,
|
|
41
|
+
actor_type ENUM('webhook','system','admin') NOT NULL,
|
|
42
|
+
mollie_payment_id VARCHAR(64) NULL,
|
|
43
|
+
note VARCHAR(512) NULL,
|
|
44
|
+
metadata JSON NULL,
|
|
45
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
KEY idx_inv_status_log_invoice (invoice_id),
|
|
47
|
+
KEY idx_inv_status_log_invoice_time (invoice_id, created_at)
|
|
48
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.InvoiceStatusLogOrm = InvoiceStatusLogOrm;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ZOrm } from '../../core/orm/orm';
|
|
2
2
|
import { ZSQLService } from '../../core/sql_service';
|
|
3
|
-
import { ZInvoice, ZInvoiceStatus } from '../types/mollie_types';
|
|
3
|
+
import { ZInvoice, ZInvoiceStatus, ZAuditContext } from '../types/mollie_types';
|
|
4
|
+
import { InvoiceStatusLogOrm } from './invoice_status_log_orm';
|
|
4
5
|
export declare class InvoicesOrm extends ZOrm {
|
|
5
|
-
|
|
6
|
+
private statusLogOrm?;
|
|
7
|
+
constructor(opt: { sqlService: ZSQLService; alias?: string; statusLogOrm?: InvoiceStatusLogOrm });
|
|
8
|
+
setStatusLogOrm(orm: InvoiceStatusLogOrm): void;
|
|
6
9
|
create(invoice: Omit<ZInvoice, 'id' | 'created_at' | 'updated_at'>): Promise<{
|
|
7
10
|
insertId: number;
|
|
8
11
|
affectedRows: number;
|
|
@@ -13,9 +16,20 @@ export declare class InvoicesOrm extends ZOrm {
|
|
|
13
16
|
findByPayTokenHash(pay_token_hash: string): Promise<ZInvoice>;
|
|
14
17
|
findAll(): Promise<ZInvoice[]>;
|
|
15
18
|
findLastByYear(year: number): Promise<ZInvoice | undefined>;
|
|
16
|
-
updateStatus(
|
|
19
|
+
updateStatus(
|
|
20
|
+
id: number,
|
|
21
|
+
status: ZInvoiceStatus,
|
|
22
|
+
amount_paid?: number,
|
|
23
|
+
paid_at?: string | null,
|
|
24
|
+
audit?: ZAuditContext,
|
|
25
|
+
): Promise<void>;
|
|
17
26
|
/** Transitions status only if the current DB status matches `fromStatus`. Safe against concurrent updates. */
|
|
18
|
-
updateStatusConditional(
|
|
27
|
+
updateStatusConditional(
|
|
28
|
+
id: number,
|
|
29
|
+
status: ZInvoiceStatus,
|
|
30
|
+
fromStatus: ZInvoiceStatus,
|
|
31
|
+
audit?: Omit<ZAuditContext, 'fromStatus'>,
|
|
32
|
+
): Promise<boolean>;
|
|
19
33
|
updatePaymentRef(
|
|
20
34
|
id: number,
|
|
21
35
|
payload: {
|
|
@@ -40,6 +54,9 @@ export declare class InvoicesOrm extends ZOrm {
|
|
|
40
54
|
): Promise<void>;
|
|
41
55
|
finalizePayToken(id: number): Promise<void>;
|
|
42
56
|
incrementTimesSent(id: number): Promise<void>;
|
|
57
|
+
setArchivedAt(id: number): Promise<void>;
|
|
58
|
+
clearArchivedAt(id: number): Promise<void>;
|
|
59
|
+
isArchived(invoice: ZInvoice): boolean;
|
|
43
60
|
updateMutableFields(
|
|
44
61
|
id: number,
|
|
45
62
|
fields: {
|
|
@@ -5,6 +5,10 @@ const orm_1 = require('../../core/orm/orm');
|
|
|
5
5
|
class InvoicesOrm extends orm_1.ZOrm {
|
|
6
6
|
constructor(opt) {
|
|
7
7
|
super({ sqlService: opt.sqlService, alias: opt.alias ?? 'mollie_invoices' });
|
|
8
|
+
this.statusLogOrm = opt.statusLogOrm;
|
|
9
|
+
}
|
|
10
|
+
setStatusLogOrm(orm) {
|
|
11
|
+
this.statusLogOrm = orm;
|
|
8
12
|
}
|
|
9
13
|
async create(invoice) {
|
|
10
14
|
const res = await this.sqlService.query(
|
|
@@ -79,7 +83,7 @@ class InvoicesOrm extends orm_1.ZOrm {
|
|
|
79
83
|
});
|
|
80
84
|
return res[0];
|
|
81
85
|
}
|
|
82
|
-
async updateStatus(id, status, amount_paid, paid_at) {
|
|
86
|
+
async updateStatus(id, status, amount_paid, paid_at, audit) {
|
|
83
87
|
await this.sqlService.query(
|
|
84
88
|
/*SQL*/ `
|
|
85
89
|
UPDATE \`${this.alias}\`
|
|
@@ -88,10 +92,21 @@ class InvoicesOrm extends orm_1.ZOrm {
|
|
|
88
92
|
`,
|
|
89
93
|
{ id, status, amount_paid: amount_paid ?? null, paid_at: paid_at ?? null },
|
|
90
94
|
);
|
|
95
|
+
if (this.statusLogOrm && audit) {
|
|
96
|
+
await this.statusLogOrm.insert({
|
|
97
|
+
invoice_id: id,
|
|
98
|
+
from_status: audit.fromStatus ?? null,
|
|
99
|
+
to_status: status,
|
|
100
|
+
actor_type: audit.actorType,
|
|
101
|
+
mollie_payment_id: audit.molliePaymentId ?? null,
|
|
102
|
+
note: audit.note ?? null,
|
|
103
|
+
metadata: audit.metadata ?? null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
91
106
|
}
|
|
92
107
|
/** Transitions status only if the current DB status matches `fromStatus`. Safe against concurrent updates. */
|
|
93
|
-
async updateStatusConditional(id, status, fromStatus) {
|
|
94
|
-
await this.sqlService.query(
|
|
108
|
+
async updateStatusConditional(id, status, fromStatus, audit) {
|
|
109
|
+
const result = await this.sqlService.query(
|
|
95
110
|
/*SQL*/ `
|
|
96
111
|
UPDATE \`${this.alias}\`
|
|
97
112
|
SET status=:status, updated_at=NOW()
|
|
@@ -99,6 +114,19 @@ class InvoicesOrm extends orm_1.ZOrm {
|
|
|
99
114
|
`,
|
|
100
115
|
{ id, status, fromStatus },
|
|
101
116
|
);
|
|
117
|
+
const changed = result?.affectedRows > 0 || result?.changedRows > 0;
|
|
118
|
+
if (this.statusLogOrm && changed) {
|
|
119
|
+
await this.statusLogOrm.insert({
|
|
120
|
+
invoice_id: id,
|
|
121
|
+
from_status: fromStatus,
|
|
122
|
+
to_status: status,
|
|
123
|
+
actor_type: audit?.actorType ?? 'system',
|
|
124
|
+
mollie_payment_id: audit?.molliePaymentId ?? null,
|
|
125
|
+
note: audit?.note ?? null,
|
|
126
|
+
metadata: audit?.metadata ?? null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return changed;
|
|
102
130
|
}
|
|
103
131
|
async updatePaymentRef(id, payload) {
|
|
104
132
|
await this.sqlService.query(
|
|
@@ -162,6 +190,29 @@ class InvoicesOrm extends orm_1.ZOrm {
|
|
|
162
190
|
{ id },
|
|
163
191
|
);
|
|
164
192
|
}
|
|
193
|
+
async setArchivedAt(id) {
|
|
194
|
+
await this.sqlService.query(
|
|
195
|
+
/*SQL*/ `
|
|
196
|
+
UPDATE \`${this.alias}\`
|
|
197
|
+
SET archived_at=NOW(), updated_at=NOW()
|
|
198
|
+
WHERE id=:id
|
|
199
|
+
`,
|
|
200
|
+
{ id },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
async clearArchivedAt(id) {
|
|
204
|
+
await this.sqlService.query(
|
|
205
|
+
/*SQL*/ `
|
|
206
|
+
UPDATE \`${this.alias}\`
|
|
207
|
+
SET archived_at=NULL, updated_at=NOW()
|
|
208
|
+
WHERE id=:id
|
|
209
|
+
`,
|
|
210
|
+
{ id },
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
isArchived(invoice) {
|
|
214
|
+
return invoice.archived_at != null;
|
|
215
|
+
}
|
|
165
216
|
async updateMutableFields(id, fields) {
|
|
166
217
|
const sets = [];
|
|
167
218
|
const params = { id };
|
|
@@ -207,7 +258,7 @@ class InvoicesOrm extends orm_1.ZOrm {
|
|
|
207
258
|
pay_token_hash CHAR(64),
|
|
208
259
|
pay_token_expires_at DATETIME,
|
|
209
260
|
pay_token_finalized_at DATETIME,
|
|
210
|
-
status ENUM('draft','pending','paid','failed','canceled','expired','refunded'
|
|
261
|
+
status ENUM('draft','pending','paid','failed','canceled','expired','refunded') NOT NULL DEFAULT 'draft',
|
|
211
262
|
amount_due DECIMAL(12,2) NOT NULL,
|
|
212
263
|
amount_paid DECIMAL(12,2) NOT NULL DEFAULT 0,
|
|
213
264
|
currency CHAR(3) NOT NULL DEFAULT 'EUR',
|
|
@@ -218,6 +269,7 @@ class InvoicesOrm extends orm_1.ZOrm {
|
|
|
218
269
|
paid_at DATETIME,
|
|
219
270
|
checkout_url VARCHAR(512),
|
|
220
271
|
times_sent INT NOT NULL DEFAULT 0,
|
|
272
|
+
archived_at DATETIME NULL,
|
|
221
273
|
metadata JSON NULL,
|
|
222
274
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
223
275
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ZOrm } from '../../core/orm/orm';
|
|
2
|
+
import { ZSQLService } from '../../core/sql_service';
|
|
3
|
+
import { ZPaymentStatusLogEntry } from '../types/mollie_types';
|
|
4
|
+
export declare class PaymentStatusLogOrm extends ZOrm {
|
|
5
|
+
constructor(opt: { sqlService: ZSQLService; alias?: string });
|
|
6
|
+
insert(entry: Omit<ZPaymentStatusLogEntry, 'id' | 'created_at'>): Promise<void>;
|
|
7
|
+
findByInvoiceId(invoiceId: number): Promise<ZPaymentStatusLogEntry[]>;
|
|
8
|
+
findByPaymentId(paymentId: number): Promise<ZPaymentStatusLogEntry[]>;
|
|
9
|
+
findByMolliePaymentId(molliePaymentId: string): Promise<ZPaymentStatusLogEntry[]>;
|
|
10
|
+
createTable(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.PaymentStatusLogOrm = void 0;
|
|
4
|
+
const orm_1 = require('../../core/orm/orm');
|
|
5
|
+
class PaymentStatusLogOrm extends orm_1.ZOrm {
|
|
6
|
+
constructor(opt) {
|
|
7
|
+
super({ sqlService: opt.sqlService, alias: opt.alias ?? 'mollie_payment_status_log' });
|
|
8
|
+
}
|
|
9
|
+
async insert(entry) {
|
|
10
|
+
await this.sqlService.query(
|
|
11
|
+
/*SQL*/ `
|
|
12
|
+
INSERT INTO \`${this.alias}\`
|
|
13
|
+
(payment_id, invoice_id, mollie_payment_id, from_status, to_status, actor_type, note, metadata)
|
|
14
|
+
VALUES
|
|
15
|
+
(:payment_id, :invoice_id, :mollie_payment_id, :from_status, :to_status, :actor_type, :note, :metadata)
|
|
16
|
+
`,
|
|
17
|
+
{
|
|
18
|
+
payment_id: entry.payment_id,
|
|
19
|
+
invoice_id: entry.invoice_id,
|
|
20
|
+
mollie_payment_id: entry.mollie_payment_id,
|
|
21
|
+
from_status: entry.from_status ?? null,
|
|
22
|
+
to_status: entry.to_status,
|
|
23
|
+
actor_type: entry.actor_type,
|
|
24
|
+
note: entry.note ?? null,
|
|
25
|
+
metadata: entry.metadata != null ? JSON.stringify(entry.metadata) : null,
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
async findByInvoiceId(invoiceId) {
|
|
30
|
+
return await this.sqlService.exec({
|
|
31
|
+
query: `SELECT * FROM \`${this.alias}\` WHERE invoice_id=:invoiceId ORDER BY created_at ASC, id ASC`,
|
|
32
|
+
params: { invoiceId },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async findByPaymentId(paymentId) {
|
|
36
|
+
return await this.sqlService.exec({
|
|
37
|
+
query: `SELECT * FROM \`${this.alias}\` WHERE payment_id=:paymentId ORDER BY created_at ASC, id ASC`,
|
|
38
|
+
params: { paymentId },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async findByMolliePaymentId(molliePaymentId) {
|
|
42
|
+
return await this.sqlService.exec({
|
|
43
|
+
query: `SELECT * FROM \`${this.alias}\` WHERE mollie_payment_id=:molliePaymentId ORDER BY created_at ASC, id ASC`,
|
|
44
|
+
params: { molliePaymentId },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async createTable() {
|
|
48
|
+
await this.sqlService.query(/*SQL*/ `
|
|
49
|
+
CREATE TABLE IF NOT EXISTS \`${this.alias}\` (
|
|
50
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
51
|
+
payment_id BIGINT UNSIGNED NOT NULL,
|
|
52
|
+
invoice_id BIGINT UNSIGNED NOT NULL,
|
|
53
|
+
mollie_payment_id VARCHAR(64) NOT NULL,
|
|
54
|
+
from_status ENUM('open','pending','authorized','paid','canceled','expired','failed','refunded') NULL,
|
|
55
|
+
to_status ENUM('open','pending','authorized','paid','canceled','expired','failed','refunded') NOT NULL,
|
|
56
|
+
actor_type ENUM('webhook','system','admin') NOT NULL,
|
|
57
|
+
note VARCHAR(512) NULL,
|
|
58
|
+
metadata JSON NULL,
|
|
59
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
60
|
+
KEY idx_pay_status_log_payment (payment_id),
|
|
61
|
+
KEY idx_pay_status_log_invoice (invoice_id),
|
|
62
|
+
KEY idx_pay_status_log_invoice_time (invoice_id, created_at),
|
|
63
|
+
KEY idx_pay_status_log_mollie (mollie_payment_id)
|
|
64
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.PaymentStatusLogOrm = PaymentStatusLogOrm;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ZSQLService } from '../../core/sql_service';
|
|
2
|
+
import { InvoiceStatusLogOrm } from '../orm/invoice_status_log_orm';
|
|
3
|
+
import { PaymentStatusLogOrm } from '../orm/payment_status_log_orm';
|
|
4
|
+
import { ZInvoiceStatusLogEntry, ZPaymentStatusLogEntry, ZInvoiceTimelineEvent } from '../types/mollie_types';
|
|
5
|
+
export declare class InvoiceAuditService {
|
|
6
|
+
private invoiceStatusLogOrm;
|
|
7
|
+
private paymentStatusLogOrm;
|
|
8
|
+
private sqlService;
|
|
9
|
+
constructor(opt: {
|
|
10
|
+
sqlService: ZSQLService;
|
|
11
|
+
invoiceStatusLogOrm?: InvoiceStatusLogOrm;
|
|
12
|
+
paymentStatusLogOrm?: PaymentStatusLogOrm;
|
|
13
|
+
});
|
|
14
|
+
/** Ensure audit log tables exist */
|
|
15
|
+
autoInit(): Promise<void>;
|
|
16
|
+
get statusLogOrm(): InvoiceStatusLogOrm;
|
|
17
|
+
get paymentLogOrm(): PaymentStatusLogOrm;
|
|
18
|
+
/** Returns all invoice-level status transitions for a given invoice */
|
|
19
|
+
getInvoiceStatusHistory(invoiceId: number): Promise<ZInvoiceStatusLogEntry[]>;
|
|
20
|
+
/** Returns all payment-level status transitions for a given invoice */
|
|
21
|
+
getPaymentStatusHistory(invoiceId: number): Promise<ZPaymentStatusLogEntry[]>;
|
|
22
|
+
/** Returns all status transitions for a single Mollie payment */
|
|
23
|
+
getPaymentHistory(molliePaymentId: string): Promise<ZPaymentStatusLogEntry[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns a unified, chronologically ordered timeline of all status changes
|
|
26
|
+
* (both invoice-level and payment-level) for a given invoice.
|
|
27
|
+
*/
|
|
28
|
+
getInvoiceTimeline(invoiceId: number): Promise<ZInvoiceTimelineEvent[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Backfills audit log entries from existing invoices and payments.
|
|
31
|
+
* Uses `created_at` for initial status and `paid_at` for paid transitions.
|
|
32
|
+
* Idempotent — skips entities that already have log entries.
|
|
33
|
+
*/
|
|
34
|
+
backfillAuditLog(opt?: { invoicesTable?: string; paymentsTable?: string }): Promise<{
|
|
35
|
+
invoicesCreated: number;
|
|
36
|
+
paymentsCreated: number;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
3
|
+
exports.InvoiceAuditService = void 0;
|
|
4
|
+
const invoice_status_log_orm_1 = require('../orm/invoice_status_log_orm');
|
|
5
|
+
const payment_status_log_orm_1 = require('../orm/payment_status_log_orm');
|
|
6
|
+
class InvoiceAuditService {
|
|
7
|
+
constructor(opt) {
|
|
8
|
+
this.sqlService = opt.sqlService;
|
|
9
|
+
this.invoiceStatusLogOrm =
|
|
10
|
+
opt.invoiceStatusLogOrm ?? new invoice_status_log_orm_1.InvoiceStatusLogOrm({ sqlService: opt.sqlService });
|
|
11
|
+
this.paymentStatusLogOrm =
|
|
12
|
+
opt.paymentStatusLogOrm ?? new payment_status_log_orm_1.PaymentStatusLogOrm({ sqlService: opt.sqlService });
|
|
13
|
+
}
|
|
14
|
+
/** Ensure audit log tables exist */
|
|
15
|
+
async autoInit() {
|
|
16
|
+
await this.invoiceStatusLogOrm.ensureTableExists();
|
|
17
|
+
await this.paymentStatusLogOrm.ensureTableExists();
|
|
18
|
+
}
|
|
19
|
+
get statusLogOrm() {
|
|
20
|
+
return this.invoiceStatusLogOrm;
|
|
21
|
+
}
|
|
22
|
+
get paymentLogOrm() {
|
|
23
|
+
return this.paymentStatusLogOrm;
|
|
24
|
+
}
|
|
25
|
+
// ==================== Query API ====================
|
|
26
|
+
/** Returns all invoice-level status transitions for a given invoice */
|
|
27
|
+
async getInvoiceStatusHistory(invoiceId) {
|
|
28
|
+
return await this.invoiceStatusLogOrm.findByInvoiceId(invoiceId);
|
|
29
|
+
}
|
|
30
|
+
/** Returns all payment-level status transitions for a given invoice */
|
|
31
|
+
async getPaymentStatusHistory(invoiceId) {
|
|
32
|
+
return await this.paymentStatusLogOrm.findByInvoiceId(invoiceId);
|
|
33
|
+
}
|
|
34
|
+
/** Returns all status transitions for a single Mollie payment */
|
|
35
|
+
async getPaymentHistory(molliePaymentId) {
|
|
36
|
+
return await this.paymentStatusLogOrm.findByMolliePaymentId(molliePaymentId);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns a unified, chronologically ordered timeline of all status changes
|
|
40
|
+
* (both invoice-level and payment-level) for a given invoice.
|
|
41
|
+
*/
|
|
42
|
+
async getInvoiceTimeline(invoiceId) {
|
|
43
|
+
const invLog = this.invoiceStatusLogOrm.alias;
|
|
44
|
+
const payLog = this.paymentStatusLogOrm.alias;
|
|
45
|
+
const rows = await this.sqlService.exec({
|
|
46
|
+
query: /*SQL*/ `
|
|
47
|
+
SELECT
|
|
48
|
+
'invoice_status' AS event_type,
|
|
49
|
+
from_status,
|
|
50
|
+
to_status,
|
|
51
|
+
actor_type,
|
|
52
|
+
mollie_payment_id,
|
|
53
|
+
note,
|
|
54
|
+
metadata,
|
|
55
|
+
created_at,
|
|
56
|
+
id AS sort_id
|
|
57
|
+
FROM \`${invLog}\`
|
|
58
|
+
WHERE invoice_id = :invoiceId
|
|
59
|
+
|
|
60
|
+
UNION ALL
|
|
61
|
+
|
|
62
|
+
SELECT
|
|
63
|
+
'payment_status' AS event_type,
|
|
64
|
+
from_status,
|
|
65
|
+
to_status,
|
|
66
|
+
actor_type,
|
|
67
|
+
mollie_payment_id,
|
|
68
|
+
note,
|
|
69
|
+
metadata,
|
|
70
|
+
created_at,
|
|
71
|
+
id AS sort_id
|
|
72
|
+
FROM \`${payLog}\`
|
|
73
|
+
WHERE invoice_id = :invoiceId
|
|
74
|
+
|
|
75
|
+
ORDER BY created_at ASC, sort_id ASC
|
|
76
|
+
`,
|
|
77
|
+
params: { invoiceId },
|
|
78
|
+
});
|
|
79
|
+
return rows.map((r) => ({
|
|
80
|
+
event_type: r.event_type,
|
|
81
|
+
from_status: r.from_status,
|
|
82
|
+
to_status: r.to_status,
|
|
83
|
+
actor_type: r.actor_type,
|
|
84
|
+
mollie_payment_id: r.mollie_payment_id,
|
|
85
|
+
note: r.note,
|
|
86
|
+
metadata: typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata,
|
|
87
|
+
created_at: r.created_at,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
// ==================== Backfill ====================
|
|
91
|
+
/**
|
|
92
|
+
* Backfills audit log entries from existing invoices and payments.
|
|
93
|
+
* Uses `created_at` for initial status and `paid_at` for paid transitions.
|
|
94
|
+
* Idempotent — skips entities that already have log entries.
|
|
95
|
+
*/
|
|
96
|
+
async backfillAuditLog(opt) {
|
|
97
|
+
const invTable = opt?.invoicesTable ?? 'mollie_invoices';
|
|
98
|
+
const payTable = opt?.paymentsTable ?? 'mollie_invoice_payments';
|
|
99
|
+
const invLog = this.invoiceStatusLogOrm.alias;
|
|
100
|
+
const payLog = this.paymentStatusLogOrm.alias;
|
|
101
|
+
let invoicesCreated = 0;
|
|
102
|
+
let paymentsCreated = 0;
|
|
103
|
+
// --- Invoice status backfill ---
|
|
104
|
+
const invoices = await this.sqlService.exec({
|
|
105
|
+
query: `SELECT id, status, paid_at, created_at FROM \`${invTable}\` ORDER BY id ASC`,
|
|
106
|
+
});
|
|
107
|
+
for (const inv of invoices) {
|
|
108
|
+
// Check if already backfilled
|
|
109
|
+
const existing = await this.sqlService.exec({
|
|
110
|
+
query: `SELECT COUNT(*) AS cnt FROM \`${invLog}\` WHERE invoice_id = :id`,
|
|
111
|
+
params: { id: inv.id },
|
|
112
|
+
});
|
|
113
|
+
if (existing[0]?.cnt > 0) continue;
|
|
114
|
+
// Insert creation entry
|
|
115
|
+
await this.sqlService.query(
|
|
116
|
+
/*SQL*/ `
|
|
117
|
+
INSERT INTO \`${invLog}\` (invoice_id, from_status, to_status, actor_type, note, created_at)
|
|
118
|
+
VALUES (:id, NULL, :status, 'system', 'Backfilled from existing data', :created_at)
|
|
119
|
+
`,
|
|
120
|
+
{ id: inv.id, status: inv.status, created_at: inv.created_at },
|
|
121
|
+
);
|
|
122
|
+
invoicesCreated++;
|
|
123
|
+
// If paid and has a paid_at timestamp different from created_at, add a paid transition
|
|
124
|
+
if (inv.status === 'paid' && inv.paid_at && inv.paid_at !== inv.created_at) {
|
|
125
|
+
await this.sqlService.query(
|
|
126
|
+
/*SQL*/ `
|
|
127
|
+
INSERT INTO \`${invLog}\` (invoice_id, from_status, to_status, actor_type, note, created_at)
|
|
128
|
+
VALUES (:id, 'pending', 'paid', 'system', 'Backfilled from paid_at', :paid_at)
|
|
129
|
+
`,
|
|
130
|
+
{ id: inv.id, paid_at: inv.paid_at },
|
|
131
|
+
);
|
|
132
|
+
invoicesCreated++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// --- Payment status backfill ---
|
|
136
|
+
const payments = await this.sqlService.exec({
|
|
137
|
+
query: `SELECT id, invoice_id, mollie_payment_id, status, paid_at, created_at FROM \`${payTable}\` ORDER BY id ASC`,
|
|
138
|
+
});
|
|
139
|
+
for (const pay of payments) {
|
|
140
|
+
const existing = await this.sqlService.exec({
|
|
141
|
+
query: `SELECT COUNT(*) AS cnt FROM \`${payLog}\` WHERE payment_id = :id`,
|
|
142
|
+
params: { id: pay.id },
|
|
143
|
+
});
|
|
144
|
+
if (existing[0]?.cnt > 0) continue;
|
|
145
|
+
await this.sqlService.query(
|
|
146
|
+
/*SQL*/ `
|
|
147
|
+
INSERT INTO \`${payLog}\` (payment_id, invoice_id, mollie_payment_id, from_status, to_status, actor_type, note, created_at)
|
|
148
|
+
VALUES (:id, :invoice_id, :mollie_payment_id, NULL, :status, 'system', 'Backfilled from existing data', :created_at)
|
|
149
|
+
`,
|
|
150
|
+
{
|
|
151
|
+
id: pay.id,
|
|
152
|
+
invoice_id: pay.invoice_id,
|
|
153
|
+
mollie_payment_id: pay.mollie_payment_id,
|
|
154
|
+
status: pay.status,
|
|
155
|
+
created_at: pay.created_at,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
paymentsCreated++;
|
|
159
|
+
if (pay.status === 'paid' && pay.paid_at && pay.paid_at !== pay.created_at) {
|
|
160
|
+
await this.sqlService.query(
|
|
161
|
+
/*SQL*/ `
|
|
162
|
+
INSERT INTO \`${payLog}\` (payment_id, invoice_id, mollie_payment_id, from_status, to_status, actor_type, note, created_at)
|
|
163
|
+
VALUES (:id, :invoice_id, :mollie_payment_id, 'open', 'paid', 'system', 'Backfilled from paid_at', :paid_at)
|
|
164
|
+
`,
|
|
165
|
+
{
|
|
166
|
+
id: pay.id,
|
|
167
|
+
invoice_id: pay.invoice_id,
|
|
168
|
+
mollie_payment_id: pay.mollie_payment_id,
|
|
169
|
+
paid_at: pay.paid_at,
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
paymentsCreated++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { invoicesCreated, paymentsCreated };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.InvoiceAuditService = InvoiceAuditService;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { CustomerService } from './customer_service';
|
|
3
|
+
import { InvoiceAuditService } from './invoice_audit_service';
|
|
3
4
|
import { RenderData } from '../../core/types/site_config';
|
|
4
5
|
import { ZMailService } from '../../core/mail_service';
|
|
5
6
|
import { MollieService } from './mollie_service';
|
|
@@ -22,6 +23,9 @@ export declare class InvoiceService {
|
|
|
22
23
|
private templateOrm;
|
|
23
24
|
private subscriptionsOrm;
|
|
24
25
|
private subscriptionItemsOrm;
|
|
26
|
+
private invoiceStatusLogOrm;
|
|
27
|
+
private paymentStatusLogOrm;
|
|
28
|
+
private auditService;
|
|
25
29
|
private payTokenSecret;
|
|
26
30
|
private payTokenLifetimeMs;
|
|
27
31
|
private mailService;
|
|
@@ -39,12 +43,14 @@ export declare class InvoiceService {
|
|
|
39
43
|
invoiceNumberMode?: 'sequence' | 'id';
|
|
40
44
|
invoiceNumberFormat?: (id: number) => string;
|
|
41
45
|
});
|
|
46
|
+
/** Returns the audit service for querying audit logs and timeline */
|
|
47
|
+
getAuditService(): InvoiceAuditService;
|
|
42
48
|
autoInit(): Promise<void>;
|
|
43
49
|
private ensurePayTokenSchema;
|
|
44
50
|
private ensureSubscriptionInvoiceSchema;
|
|
45
51
|
private ensureSubsidyItemTypeSchema;
|
|
46
52
|
private ensureSentCountSchema;
|
|
47
|
-
private
|
|
53
|
+
private ensureArchivedAtSchema;
|
|
48
54
|
private ensureInvoicePaymentSchema;
|
|
49
55
|
private generateInvoiceNumber;
|
|
50
56
|
private getWebhookUrl;
|
|
@@ -82,6 +88,7 @@ export declare class InvoiceService {
|
|
|
82
88
|
},
|
|
83
89
|
): Promise<ZInvoice>;
|
|
84
90
|
archiveInvoice(invoiceId: number): Promise<ZInvoice>;
|
|
91
|
+
unarchiveInvoice(invoiceId: number): Promise<ZInvoice>;
|
|
85
92
|
duplicateInvoice(
|
|
86
93
|
sourceInvoiceId: number,
|
|
87
94
|
customerId: number,
|
|
@@ -16,6 +16,9 @@ const invoice_payments_orm_1 = require('../orm/invoice_payments_orm');
|
|
|
16
16
|
const invoices_orm_1 = require('../orm/invoices_orm');
|
|
17
17
|
const subscription_items_orm_1 = require('../orm/subscription_items_orm');
|
|
18
18
|
const subscriptions_orm_1 = require('../orm/subscriptions_orm');
|
|
19
|
+
const invoice_status_log_orm_1 = require('../orm/invoice_status_log_orm');
|
|
20
|
+
const payment_status_log_orm_1 = require('../orm/payment_status_log_orm');
|
|
21
|
+
const invoice_audit_service_1 = require('./invoice_audit_service');
|
|
19
22
|
const subscription_utils_1 = require('../util/subscription_utils');
|
|
20
23
|
const orm_1 = require('../../core/orm/orm');
|
|
21
24
|
class InvoiceService {
|
|
@@ -29,27 +32,45 @@ class InvoiceService {
|
|
|
29
32
|
this.opt = opt;
|
|
30
33
|
this.payTokenSecret = this.opt.payTokenSecret || '';
|
|
31
34
|
this.payTokenLifetimeMs = 60 * 24 * 60 * 60 * 1000;
|
|
32
|
-
this.
|
|
35
|
+
this.invoiceStatusLogOrm = new invoice_status_log_orm_1.InvoiceStatusLogOrm({ sqlService: opt.sqlService });
|
|
36
|
+
this.paymentStatusLogOrm = new payment_status_log_orm_1.PaymentStatusLogOrm({ sqlService: opt.sqlService });
|
|
37
|
+
this.invoicesOrm = new invoices_orm_1.InvoicesOrm({
|
|
38
|
+
sqlService: opt.sqlService,
|
|
39
|
+
statusLogOrm: this.invoiceStatusLogOrm,
|
|
40
|
+
});
|
|
33
41
|
this.itemsOrm = new invoice_items_orm_1.InvoiceItemsOrm({ sqlService: opt.sqlService });
|
|
34
|
-
this.paymentsOrm = new invoice_payments_orm_1.InvoicePaymentsOrm({
|
|
42
|
+
this.paymentsOrm = new invoice_payments_orm_1.InvoicePaymentsOrm({
|
|
43
|
+
sqlService: opt.sqlService,
|
|
44
|
+
paymentLogOrm: this.paymentStatusLogOrm,
|
|
45
|
+
});
|
|
35
46
|
this.templateOrm = new invoice_item_templates_orm_1.InvoiceItemTemplatesOrm({ sqlService: opt.sqlService });
|
|
36
47
|
this.subscriptionsOrm = new subscriptions_orm_1.SubscriptionsOrm({ sqlService: opt.sqlService });
|
|
37
48
|
this.subscriptionItemsOrm = new subscription_items_orm_1.SubscriptionItemsOrm({ sqlService: opt.sqlService });
|
|
49
|
+
this.auditService = new invoice_audit_service_1.InvoiceAuditService({
|
|
50
|
+
sqlService: opt.sqlService,
|
|
51
|
+
invoiceStatusLogOrm: this.invoiceStatusLogOrm,
|
|
52
|
+
paymentStatusLogOrm: this.paymentStatusLogOrm,
|
|
53
|
+
});
|
|
38
54
|
this.mailService = opt.mailService;
|
|
39
55
|
this.invoiceNumberMode = opt.invoiceNumberMode ?? 'sequence';
|
|
40
56
|
this.invoiceNumberFormat = opt.invoiceNumberFormat ?? ((id) => `INV-${id.toString().padStart(6, '0')}`);
|
|
41
57
|
}
|
|
58
|
+
/** Returns the audit service for querying audit logs and timeline */
|
|
59
|
+
getAuditService() {
|
|
60
|
+
return this.auditService;
|
|
61
|
+
}
|
|
42
62
|
async autoInit() {
|
|
43
63
|
await this.invoicesOrm.ensureTableExists();
|
|
44
64
|
await this.itemsOrm.ensureTableExists();
|
|
45
65
|
await this.paymentsOrm.ensureTableExists();
|
|
46
66
|
await this.templateOrm.ensureTableExists();
|
|
67
|
+
await this.auditService.autoInit();
|
|
47
68
|
await this.ensurePayTokenSchema();
|
|
48
69
|
await this.ensureSubscriptionInvoiceSchema();
|
|
49
70
|
await this.ensureInvoicePaymentSchema();
|
|
50
71
|
await this.ensureSubsidyItemTypeSchema();
|
|
51
72
|
await this.ensureSentCountSchema();
|
|
52
|
-
await this.
|
|
73
|
+
await this.ensureArchivedAtSchema();
|
|
53
74
|
}
|
|
54
75
|
async ensurePayTokenSchema() {
|
|
55
76
|
const table = this.invoicesOrm.alias;
|
|
@@ -121,17 +142,14 @@ class InvoiceService {
|
|
|
121
142
|
);
|
|
122
143
|
}
|
|
123
144
|
}
|
|
124
|
-
async
|
|
145
|
+
async ensureArchivedAtSchema() {
|
|
125
146
|
const table = this.invoicesOrm.alias;
|
|
126
147
|
const rows = await this.opt.sqlService.exec({
|
|
127
|
-
query: `SELECT
|
|
148
|
+
query: `SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=:schema AND TABLE_NAME=:tableName AND COLUMN_NAME='archived_at' LIMIT 1`,
|
|
128
149
|
params: { schema: this.opt.sqlService.database, tableName: table },
|
|
129
150
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await this.opt.sqlService.query(
|
|
133
|
-
`ALTER TABLE \`${table}\` MODIFY COLUMN status ENUM('draft','pending','paid','failed','canceled','expired','refunded','archived') NOT NULL DEFAULT 'draft'`,
|
|
134
|
-
);
|
|
151
|
+
if (!rows?.[0]) {
|
|
152
|
+
await this.opt.sqlService.query(`ALTER TABLE \`${table}\` ADD COLUMN archived_at DATETIME NULL AFTER times_sent`);
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
async ensureInvoicePaymentSchema() {
|
|
@@ -314,20 +332,23 @@ class InvoiceService {
|
|
|
314
332
|
sequenceType: opt?.sequenceType ?? 'oneoff',
|
|
315
333
|
mandateId: opt?.mandateId ?? undefined,
|
|
316
334
|
});
|
|
317
|
-
await this.paymentsOrm.upsert(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
335
|
+
await this.paymentsOrm.upsert(
|
|
336
|
+
{
|
|
337
|
+
invoice_id: invoice.id,
|
|
338
|
+
mollie_payment_id: payment.id,
|
|
339
|
+
status: payment.status,
|
|
340
|
+
sequence_type: payment.sequenceType ?? opt?.sequenceType ?? 'oneoff',
|
|
341
|
+
mollie_subscription_id: payment.subscriptionId ?? null,
|
|
342
|
+
method: payment.method ?? null,
|
|
343
|
+
amount: Number(payment.amount.value),
|
|
344
|
+
currency: payment.amount.currency,
|
|
345
|
+
checkout_url: payment?._links?.checkout?.href ?? null,
|
|
346
|
+
paid_at: (0, orm_1.toDatetime)(payment.paidAt ?? null),
|
|
347
|
+
expires_at: (0, orm_1.toDatetime)(payment.expiresAt),
|
|
348
|
+
mandate_id: payment.mandateId ?? opt?.mandateId ?? null,
|
|
349
|
+
},
|
|
350
|
+
{ actorType: 'system', note: 'Payment created via Mollie API' },
|
|
351
|
+
);
|
|
331
352
|
await this.invoicesOrm.updatePaymentRef(invoice.id, {
|
|
332
353
|
mollie_payment_id: payment.id,
|
|
333
354
|
checkout_url: payment?.getCheckoutUrl() ?? payment?._links?.checkout?.href ?? null,
|
|
@@ -427,6 +448,8 @@ class InvoiceService {
|
|
|
427
448
|
if (!invoice) throw new Error(`Invoice ${invoiceId} not found`);
|
|
428
449
|
if (invoice.status !== 'draft')
|
|
429
450
|
throw new Error(`Invoice ${invoice.invoice_number} cannot be edited (status: ${invoice.status})`);
|
|
451
|
+
if (this.invoicesOrm.isArchived(invoice))
|
|
452
|
+
throw new Error(`Invoice ${invoice.invoice_number} cannot be edited (archived)`);
|
|
430
453
|
if ((invoice.times_sent ?? 0) > 0)
|
|
431
454
|
throw new Error(
|
|
432
455
|
`Invoice ${invoice.invoice_number} cannot be edited (already sent ${invoice.times_sent} time(s))`,
|
|
@@ -460,13 +483,37 @@ class InvoiceService {
|
|
|
460
483
|
if (!invoice) {
|
|
461
484
|
throw new Error(`Invoice ${invoiceId} not found`);
|
|
462
485
|
}
|
|
463
|
-
if (invoice
|
|
464
|
-
|
|
486
|
+
if (this.invoicesOrm.isArchived(invoice)) {
|
|
487
|
+
return invoice;
|
|
488
|
+
}
|
|
489
|
+
await this.invoicesOrm.setArchivedAt(invoiceId);
|
|
490
|
+
// Audit log: record archive event
|
|
491
|
+
await this.invoiceStatusLogOrm.insert({
|
|
492
|
+
invoice_id: invoiceId,
|
|
493
|
+
from_status: invoice.status,
|
|
494
|
+
to_status: invoice.status,
|
|
495
|
+
actor_type: 'admin',
|
|
496
|
+
note: 'Invoice archived (archived_at set)',
|
|
497
|
+
});
|
|
498
|
+
return await this.invoicesOrm.findById(invoiceId);
|
|
499
|
+
}
|
|
500
|
+
async unarchiveInvoice(invoiceId) {
|
|
501
|
+
const invoice = await this.invoicesOrm.findById(invoiceId);
|
|
502
|
+
if (!invoice) {
|
|
503
|
+
throw new Error(`Invoice ${invoiceId} not found`);
|
|
465
504
|
}
|
|
466
|
-
if (invoice
|
|
505
|
+
if (!this.invoicesOrm.isArchived(invoice)) {
|
|
467
506
|
return invoice;
|
|
468
507
|
}
|
|
469
|
-
await this.invoicesOrm.
|
|
508
|
+
await this.invoicesOrm.clearArchivedAt(invoiceId);
|
|
509
|
+
// Audit log: record unarchive event
|
|
510
|
+
await this.invoiceStatusLogOrm.insert({
|
|
511
|
+
invoice_id: invoiceId,
|
|
512
|
+
from_status: invoice.status,
|
|
513
|
+
to_status: invoice.status,
|
|
514
|
+
actor_type: 'admin',
|
|
515
|
+
note: 'Invoice unarchived (archived_at cleared)',
|
|
516
|
+
});
|
|
470
517
|
return await this.invoicesOrm.findById(invoiceId);
|
|
471
518
|
}
|
|
472
519
|
// ==================== Duplicate ====================
|
|
@@ -566,6 +613,15 @@ class InvoiceService {
|
|
|
566
613
|
if (!savedInvoice) {
|
|
567
614
|
throw new Error('Failed to persist invoice');
|
|
568
615
|
}
|
|
616
|
+
// Audit log: record initial invoice creation status
|
|
617
|
+
await this.invoiceStatusLogOrm.insert({
|
|
618
|
+
invoice_id: savedInvoice.id,
|
|
619
|
+
from_status: null,
|
|
620
|
+
to_status: status,
|
|
621
|
+
actor_type: 'system',
|
|
622
|
+
mollie_payment_id: overrides?.mollie_payment_id ?? null,
|
|
623
|
+
note: 'Invoice created',
|
|
624
|
+
});
|
|
569
625
|
const itemsWithInvoice = items.map((it) => ({ ...it, invoice_id: savedInvoice.id }));
|
|
570
626
|
await this.itemsOrm.bulkInsert(itemsWithInvoice);
|
|
571
627
|
const pay = overrides?.issuePayToken === false ? null : await this.ensurePayLink(savedInvoice.id);
|
|
@@ -649,26 +705,34 @@ class InvoiceService {
|
|
|
649
705
|
if (!invoice) {
|
|
650
706
|
throw new Error(`Invoice for payment ${paymentId} not found`);
|
|
651
707
|
}
|
|
652
|
-
await this.paymentsOrm.upsert(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
708
|
+
await this.paymentsOrm.upsert(
|
|
709
|
+
{
|
|
710
|
+
invoice_id: invoice.id,
|
|
711
|
+
mollie_payment_id: payment.id,
|
|
712
|
+
status: payment.status,
|
|
713
|
+
sequence_type: payment.sequenceType ?? null,
|
|
714
|
+
mollie_subscription_id: payment.subscriptionId ?? null,
|
|
715
|
+
method: payment.method ?? null,
|
|
716
|
+
amount: Number(payment.amount.value),
|
|
717
|
+
currency: payment.amount.currency,
|
|
718
|
+
checkout_url: payment?._links?.checkout?.href ?? null,
|
|
719
|
+
paid_at: (0, orm_1.toDatetime)(payment.paidAt ?? null),
|
|
720
|
+
expires_at: (0, orm_1.toDatetime)(payment.expiresAt),
|
|
721
|
+
mandate_id: payment.mandateId ?? null,
|
|
722
|
+
},
|
|
723
|
+
{ actorType: 'webhook', note: 'Synced from Mollie webhook' },
|
|
724
|
+
);
|
|
666
725
|
await this.invoicesOrm.updatePaymentRef(invoice.id, {
|
|
667
726
|
mollie_payment_id: payment.id,
|
|
668
727
|
checkout_url: payment?._links?.checkout?.href ?? null,
|
|
669
728
|
});
|
|
670
729
|
const previousStatus = invoice.status;
|
|
671
|
-
await this.invoicesOrm.updateStatus(invoice.id, mappedStatus, paidAmount, paidAt
|
|
730
|
+
await this.invoicesOrm.updateStatus(invoice.id, mappedStatus, paidAmount, paidAt, {
|
|
731
|
+
fromStatus: previousStatus,
|
|
732
|
+
actorType: 'webhook',
|
|
733
|
+
molliePaymentId: payment.id,
|
|
734
|
+
note: `Synced from Mollie webhook (mollie status: ${payment.status})`,
|
|
735
|
+
});
|
|
672
736
|
if (mappedStatus === 'paid') {
|
|
673
737
|
await this.invoicesOrm.finalizePayToken(invoice.id);
|
|
674
738
|
}
|
|
@@ -1011,7 +1075,10 @@ class InvoiceService {
|
|
|
1011
1075
|
});
|
|
1012
1076
|
await this.invoicesOrm.incrementTimesSent(invoiceId);
|
|
1013
1077
|
if (!isReceipt) {
|
|
1014
|
-
await this.invoicesOrm.updateStatusConditional(invoiceId, 'pending', 'draft'
|
|
1078
|
+
await this.invoicesOrm.updateStatusConditional(invoiceId, 'pending', 'draft', {
|
|
1079
|
+
actorType: 'system',
|
|
1080
|
+
note: 'Status set to pending after invoice email sent',
|
|
1081
|
+
});
|
|
1015
1082
|
}
|
|
1016
1083
|
return {
|
|
1017
1084
|
invoice_number: invoice.invoice_number,
|
|
@@ -18,7 +18,7 @@ export type ZCustomer = {
|
|
|
18
18
|
updated_at?: string | Date;
|
|
19
19
|
};
|
|
20
20
|
export type ZInvoiceItemType = 'service' | 'subsidy';
|
|
21
|
-
export type ZInvoiceStatus = 'draft' | 'pending' | 'paid' | 'failed' | 'canceled' | 'expired' | 'refunded'
|
|
21
|
+
export type ZInvoiceStatus = 'draft' | 'pending' | 'paid' | 'failed' | 'canceled' | 'expired' | 'refunded';
|
|
22
22
|
export type ZInvoiceItem = {
|
|
23
23
|
id?: number;
|
|
24
24
|
invoice_id: number;
|
|
@@ -77,6 +77,7 @@ export type ZInvoice = {
|
|
|
77
77
|
paid_at?: string | null;
|
|
78
78
|
checkout_url?: string | null;
|
|
79
79
|
times_sent?: number;
|
|
80
|
+
archived_at?: string | null;
|
|
80
81
|
metadata?: any;
|
|
81
82
|
created_at?: string | Date;
|
|
82
83
|
updated_at?: string | Date;
|
|
@@ -173,6 +174,47 @@ export type ZPayResolveResult =
|
|
|
173
174
|
action: 'paid';
|
|
174
175
|
invoice: ZInvoice;
|
|
175
176
|
};
|
|
177
|
+
export type ZAuditActorType = 'webhook' | 'system' | 'admin';
|
|
178
|
+
export type ZInvoiceStatusLogEntry = {
|
|
179
|
+
id?: number;
|
|
180
|
+
invoice_id: number;
|
|
181
|
+
from_status: ZInvoiceStatus | null;
|
|
182
|
+
to_status: ZInvoiceStatus;
|
|
183
|
+
actor_type: ZAuditActorType;
|
|
184
|
+
mollie_payment_id?: string | null;
|
|
185
|
+
note?: string | null;
|
|
186
|
+
metadata?: any;
|
|
187
|
+
created_at?: string | Date;
|
|
188
|
+
};
|
|
189
|
+
export type ZPaymentStatusLogEntry = {
|
|
190
|
+
id?: number;
|
|
191
|
+
payment_id: number;
|
|
192
|
+
invoice_id: number;
|
|
193
|
+
mollie_payment_id: string;
|
|
194
|
+
from_status: ZInvoicePaymentStatus | null;
|
|
195
|
+
to_status: ZInvoicePaymentStatus;
|
|
196
|
+
actor_type: ZAuditActorType;
|
|
197
|
+
note?: string | null;
|
|
198
|
+
metadata?: any;
|
|
199
|
+
created_at?: string | Date;
|
|
200
|
+
};
|
|
201
|
+
export type ZInvoiceTimelineEvent = {
|
|
202
|
+
event_type: 'invoice_status' | 'payment_status';
|
|
203
|
+
from_status: string | null;
|
|
204
|
+
to_status: string;
|
|
205
|
+
actor_type: ZAuditActorType;
|
|
206
|
+
mollie_payment_id?: string | null;
|
|
207
|
+
note?: string | null;
|
|
208
|
+
metadata?: any;
|
|
209
|
+
created_at: string | Date;
|
|
210
|
+
};
|
|
211
|
+
export type ZAuditContext = {
|
|
212
|
+
fromStatus?: string | null;
|
|
213
|
+
actorType: ZAuditActorType;
|
|
214
|
+
molliePaymentId?: string | null;
|
|
215
|
+
note?: string | null;
|
|
216
|
+
metadata?: any;
|
|
217
|
+
};
|
|
176
218
|
export type RecoveryStats = {
|
|
177
219
|
dryRun: boolean;
|
|
178
220
|
startedAt: string;
|