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.
@@ -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') 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
- 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: {
@@ -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','archived') NOT NULL DEFAULT 'draft',
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 ensureArchivedStatusSchema;
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.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();
50
71
  await this.ensureSubsidyItemTypeSchema();
51
72
  await this.ensureSentCountSchema();
52
- await this.ensureArchivedStatusSchema();
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 ensureArchivedStatusSchema() {
145
+ async ensureArchivedAtSchema() {
125
146
  const table = this.invoicesOrm.alias;
126
147
  const rows = await this.opt.sqlService.exec({
127
- query: `SELECT COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=:schema AND TABLE_NAME=:tableName AND COLUMN_NAME='status' LIMIT 1`,
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
- const colType = rows?.[0]?.COLUMN_TYPE ?? '';
131
- if (colType && !colType.includes('archived')) {
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
- 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
- });
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.status === 'paid') {
464
- throw new Error(`Cannot archive a paid invoice (${invoice.invoice_number})`);
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.status === 'archived') {
505
+ if (!this.invoicesOrm.isArchived(invoice)) {
467
506
  return invoice;
468
507
  }
469
- await this.invoicesOrm.updateStatus(invoiceId, 'archived');
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
- 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
- });
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' | 'archived';
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ztechno_core",
3
- "version": "0.0.131",
3
+ "version": "0.0.133",
4
4
  "description": "Core files for ztechno framework",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",