ztechno_core 0.0.130 → 0.0.132
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 +19 -3
- package/lib/mollie/orm/invoices_orm.js +40 -1
- 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 +6 -0
- package/lib/mollie/services/invoice_service.js +83 -32
- package/lib/mollie/types/mollie_types.d.ts +41 -0
- 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','archived') NULL,
|
|
40
|
+
to_status ENUM('draft','pending','paid','failed','canceled','expired','refunded','archived') 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,7 +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>;
|
|
26
|
+
/** Transitions status only if the current DB status matches `fromStatus`. Safe against concurrent updates. */
|
|
27
|
+
updateStatusConditional(
|
|
28
|
+
id: number,
|
|
29
|
+
status: ZInvoiceStatus,
|
|
30
|
+
fromStatus: ZInvoiceStatus,
|
|
31
|
+
audit?: Omit<ZAuditContext, 'fromStatus'>,
|
|
32
|
+
): Promise<boolean>;
|
|
17
33
|
updatePaymentRef(
|
|
18
34
|
id: number,
|
|
19
35
|
payload: {
|
|
@@ -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,6 +92,41 @@ 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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Transitions status only if the current DB status matches `fromStatus`. Safe against concurrent updates. */
|
|
108
|
+
async updateStatusConditional(id, status, fromStatus, audit) {
|
|
109
|
+
const result = await this.sqlService.query(
|
|
110
|
+
/*SQL*/ `
|
|
111
|
+
UPDATE \`${this.alias}\`
|
|
112
|
+
SET status=:status, updated_at=NOW()
|
|
113
|
+
WHERE id=:id AND status=:fromStatus
|
|
114
|
+
`,
|
|
115
|
+
{ id, status, fromStatus },
|
|
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;
|
|
91
130
|
}
|
|
92
131
|
async updatePaymentRef(id, payload) {
|
|
93
132
|
await this.sqlService.query(
|
|
@@ -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,6 +43,8 @@ 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;
|
|
@@ -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,21 +32,39 @@ 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();
|
|
@@ -314,20 +335,23 @@ class InvoiceService {
|
|
|
314
335
|
sequenceType: opt?.sequenceType ?? 'oneoff',
|
|
315
336
|
mandateId: opt?.mandateId ?? undefined,
|
|
316
337
|
});
|
|
317
|
-
await this.paymentsOrm.upsert(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
338
|
+
await this.paymentsOrm.upsert(
|
|
339
|
+
{
|
|
340
|
+
invoice_id: invoice.id,
|
|
341
|
+
mollie_payment_id: payment.id,
|
|
342
|
+
status: payment.status,
|
|
343
|
+
sequence_type: payment.sequenceType ?? opt?.sequenceType ?? 'oneoff',
|
|
344
|
+
mollie_subscription_id: payment.subscriptionId ?? null,
|
|
345
|
+
method: payment.method ?? null,
|
|
346
|
+
amount: Number(payment.amount.value),
|
|
347
|
+
currency: payment.amount.currency,
|
|
348
|
+
checkout_url: payment?._links?.checkout?.href ?? null,
|
|
349
|
+
paid_at: (0, orm_1.toDatetime)(payment.paidAt ?? null),
|
|
350
|
+
expires_at: (0, orm_1.toDatetime)(payment.expiresAt),
|
|
351
|
+
mandate_id: payment.mandateId ?? opt?.mandateId ?? null,
|
|
352
|
+
},
|
|
353
|
+
{ actorType: 'system', note: 'Payment created via Mollie API' },
|
|
354
|
+
);
|
|
331
355
|
await this.invoicesOrm.updatePaymentRef(invoice.id, {
|
|
332
356
|
mollie_payment_id: payment.id,
|
|
333
357
|
checkout_url: payment?.getCheckoutUrl() ?? payment?._links?.checkout?.href ?? null,
|
|
@@ -466,7 +490,11 @@ class InvoiceService {
|
|
|
466
490
|
if (invoice.status === 'archived') {
|
|
467
491
|
return invoice;
|
|
468
492
|
}
|
|
469
|
-
await this.invoicesOrm.updateStatus(invoiceId, 'archived'
|
|
493
|
+
await this.invoicesOrm.updateStatus(invoiceId, 'archived', undefined, undefined, {
|
|
494
|
+
fromStatus: invoice.status,
|
|
495
|
+
actorType: 'admin',
|
|
496
|
+
note: 'Invoice archived',
|
|
497
|
+
});
|
|
470
498
|
return await this.invoicesOrm.findById(invoiceId);
|
|
471
499
|
}
|
|
472
500
|
// ==================== Duplicate ====================
|
|
@@ -566,6 +594,15 @@ class InvoiceService {
|
|
|
566
594
|
if (!savedInvoice) {
|
|
567
595
|
throw new Error('Failed to persist invoice');
|
|
568
596
|
}
|
|
597
|
+
// Audit log: record initial invoice creation status
|
|
598
|
+
await this.invoiceStatusLogOrm.insert({
|
|
599
|
+
invoice_id: savedInvoice.id,
|
|
600
|
+
from_status: null,
|
|
601
|
+
to_status: status,
|
|
602
|
+
actor_type: 'system',
|
|
603
|
+
mollie_payment_id: overrides?.mollie_payment_id ?? null,
|
|
604
|
+
note: 'Invoice created',
|
|
605
|
+
});
|
|
569
606
|
const itemsWithInvoice = items.map((it) => ({ ...it, invoice_id: savedInvoice.id }));
|
|
570
607
|
await this.itemsOrm.bulkInsert(itemsWithInvoice);
|
|
571
608
|
const pay = overrides?.issuePayToken === false ? null : await this.ensurePayLink(savedInvoice.id);
|
|
@@ -649,26 +686,34 @@ class InvoiceService {
|
|
|
649
686
|
if (!invoice) {
|
|
650
687
|
throw new Error(`Invoice for payment ${paymentId} not found`);
|
|
651
688
|
}
|
|
652
|
-
await this.paymentsOrm.upsert(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
689
|
+
await this.paymentsOrm.upsert(
|
|
690
|
+
{
|
|
691
|
+
invoice_id: invoice.id,
|
|
692
|
+
mollie_payment_id: payment.id,
|
|
693
|
+
status: payment.status,
|
|
694
|
+
sequence_type: payment.sequenceType ?? null,
|
|
695
|
+
mollie_subscription_id: payment.subscriptionId ?? null,
|
|
696
|
+
method: payment.method ?? null,
|
|
697
|
+
amount: Number(payment.amount.value),
|
|
698
|
+
currency: payment.amount.currency,
|
|
699
|
+
checkout_url: payment?._links?.checkout?.href ?? null,
|
|
700
|
+
paid_at: (0, orm_1.toDatetime)(payment.paidAt ?? null),
|
|
701
|
+
expires_at: (0, orm_1.toDatetime)(payment.expiresAt),
|
|
702
|
+
mandate_id: payment.mandateId ?? null,
|
|
703
|
+
},
|
|
704
|
+
{ actorType: 'webhook', note: 'Synced from Mollie webhook' },
|
|
705
|
+
);
|
|
666
706
|
await this.invoicesOrm.updatePaymentRef(invoice.id, {
|
|
667
707
|
mollie_payment_id: payment.id,
|
|
668
708
|
checkout_url: payment?._links?.checkout?.href ?? null,
|
|
669
709
|
});
|
|
670
710
|
const previousStatus = invoice.status;
|
|
671
|
-
await this.invoicesOrm.updateStatus(invoice.id, mappedStatus, paidAmount, paidAt
|
|
711
|
+
await this.invoicesOrm.updateStatus(invoice.id, mappedStatus, paidAmount, paidAt, {
|
|
712
|
+
fromStatus: previousStatus,
|
|
713
|
+
actorType: 'webhook',
|
|
714
|
+
molliePaymentId: payment.id,
|
|
715
|
+
note: `Synced from Mollie webhook (mollie status: ${payment.status})`,
|
|
716
|
+
});
|
|
672
717
|
if (mappedStatus === 'paid') {
|
|
673
718
|
await this.invoicesOrm.finalizePayToken(invoice.id);
|
|
674
719
|
}
|
|
@@ -1010,6 +1055,12 @@ class InvoiceService {
|
|
|
1010
1055
|
],
|
|
1011
1056
|
});
|
|
1012
1057
|
await this.invoicesOrm.incrementTimesSent(invoiceId);
|
|
1058
|
+
if (!isReceipt) {
|
|
1059
|
+
await this.invoicesOrm.updateStatusConditional(invoiceId, 'pending', 'draft', {
|
|
1060
|
+
actorType: 'system',
|
|
1061
|
+
note: 'Status set to pending after invoice email sent',
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1013
1064
|
return {
|
|
1014
1065
|
invoice_number: invoice.invoice_number,
|
|
1015
1066
|
recipient: to,
|
|
@@ -173,6 +173,47 @@ export type ZPayResolveResult =
|
|
|
173
173
|
action: 'paid';
|
|
174
174
|
invoice: ZInvoice;
|
|
175
175
|
};
|
|
176
|
+
export type ZAuditActorType = 'webhook' | 'system' | 'admin';
|
|
177
|
+
export type ZInvoiceStatusLogEntry = {
|
|
178
|
+
id?: number;
|
|
179
|
+
invoice_id: number;
|
|
180
|
+
from_status: ZInvoiceStatus | null;
|
|
181
|
+
to_status: ZInvoiceStatus;
|
|
182
|
+
actor_type: ZAuditActorType;
|
|
183
|
+
mollie_payment_id?: string | null;
|
|
184
|
+
note?: string | null;
|
|
185
|
+
metadata?: any;
|
|
186
|
+
created_at?: string | Date;
|
|
187
|
+
};
|
|
188
|
+
export type ZPaymentStatusLogEntry = {
|
|
189
|
+
id?: number;
|
|
190
|
+
payment_id: number;
|
|
191
|
+
invoice_id: number;
|
|
192
|
+
mollie_payment_id: string;
|
|
193
|
+
from_status: ZInvoicePaymentStatus | null;
|
|
194
|
+
to_status: ZInvoicePaymentStatus;
|
|
195
|
+
actor_type: ZAuditActorType;
|
|
196
|
+
note?: string | null;
|
|
197
|
+
metadata?: any;
|
|
198
|
+
created_at?: string | Date;
|
|
199
|
+
};
|
|
200
|
+
export type ZInvoiceTimelineEvent = {
|
|
201
|
+
event_type: 'invoice_status' | 'payment_status';
|
|
202
|
+
from_status: string | null;
|
|
203
|
+
to_status: string;
|
|
204
|
+
actor_type: ZAuditActorType;
|
|
205
|
+
mollie_payment_id?: string | null;
|
|
206
|
+
note?: string | null;
|
|
207
|
+
metadata?: any;
|
|
208
|
+
created_at: string | Date;
|
|
209
|
+
};
|
|
210
|
+
export type ZAuditContext = {
|
|
211
|
+
fromStatus?: string | null;
|
|
212
|
+
actorType: ZAuditActorType;
|
|
213
|
+
molliePaymentId?: string | null;
|
|
214
|
+
note?: string | null;
|
|
215
|
+
metadata?: any;
|
|
216
|
+
};
|
|
176
217
|
export type RecoveryStats = {
|
|
177
218
|
dryRun: boolean;
|
|
178
219
|
startedAt: string;
|