ztechno_core 0.0.131 → 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.
@@ -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';
@@ -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.SubscriptionService = exports.InvoiceService = exports.CustomerService = exports.MollieService = void 0;
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
- constructor(opt: { sqlService: ZSQLService; alias?: string });
6
- upsert(payment: Omit<ZInvoicePayment, 'id' | 'created_at' | 'updated_at'>): Promise<void>;
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
- async upsert(payment) {
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
- constructor(opt: { sqlService: ZSQLService; alias?: string });
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(id: number, status: ZInvoiceStatus, amount_paid?: number, paid_at?: string | null): Promise<void>;
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(id: number, status: ZInvoiceStatus, fromStatus: ZInvoiceStatus): Promise<void>;
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: {
@@ -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(
@@ -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.invoicesOrm = new invoices_orm_1.InvoicesOrm({ sqlService: opt.sqlService });
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({ sqlService: opt.sqlService });
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
- invoice_id: invoice.id,
319
- mollie_payment_id: payment.id,
320
- status: payment.status,
321
- sequence_type: payment.sequenceType ?? opt?.sequenceType ?? 'oneoff',
322
- mollie_subscription_id: payment.subscriptionId ?? null,
323
- method: payment.method ?? null,
324
- amount: Number(payment.amount.value),
325
- currency: payment.amount.currency,
326
- checkout_url: payment?._links?.checkout?.href ?? null,
327
- paid_at: (0, orm_1.toDatetime)(payment.paidAt ?? null),
328
- expires_at: (0, orm_1.toDatetime)(payment.expiresAt),
329
- mandate_id: payment.mandateId ?? opt?.mandateId ?? null,
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
- invoice_id: invoice.id,
654
- mollie_payment_id: payment.id,
655
- status: payment.status,
656
- sequence_type: payment.sequenceType ?? null,
657
- mollie_subscription_id: payment.subscriptionId ?? null,
658
- method: payment.method ?? null,
659
- amount: Number(payment.amount.value),
660
- currency: payment.amount.currency,
661
- checkout_url: payment?._links?.checkout?.href ?? null,
662
- paid_at: (0, orm_1.toDatetime)(payment.paidAt ?? null),
663
- expires_at: (0, orm_1.toDatetime)(payment.expiresAt),
664
- mandate_id: payment.mandateId ?? null,
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
  }
@@ -1011,7 +1056,10 @@ class InvoiceService {
1011
1056
  });
1012
1057
  await this.invoicesOrm.incrementTimesSent(invoiceId);
1013
1058
  if (!isReceipt) {
1014
- await this.invoicesOrm.updateStatusConditional(invoiceId, 'pending', 'draft');
1059
+ await this.invoicesOrm.updateStatusConditional(invoiceId, 'pending', 'draft', {
1060
+ actorType: 'system',
1061
+ note: 'Status set to pending after invoice email sent',
1062
+ });
1015
1063
  }
1016
1064
  return {
1017
1065
  invoice_number: invoice.invoice_number,
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ztechno_core",
3
- "version": "0.0.131",
3
+ "version": "0.0.132",
4
4
  "description": "Core files for ztechno framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",