vr-models 1.0.56 → 1.0.57

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.
@@ -2,6 +2,7 @@ import { Model, InferAttributes, InferCreationAttributes, CreationOptional, NonA
2
2
  import type { Device } from "./device.models";
3
3
  import type { User } from "./user.models";
4
4
  import type { Installment } from "./installment.models";
5
+ import { Payment } from "./payment.models";
5
6
  export type PaymentPlanStatus = "ACTIVE" | "COMPLETED" | "DEFAULTED" | "CANCELLED";
6
7
  export declare const PAYMENT_PLAN_STATUS: readonly ["ACTIVE", "COMPLETED", "DEFAULTED", "CANCELLED"];
7
8
  export interface DevicePaymentPlanAttributes {
@@ -19,6 +20,7 @@ export interface DevicePaymentPlanAttributes {
19
20
  devices?: Device[];
20
21
  user: User;
21
22
  installments?: Installment[];
23
+ payment?: Payment;
22
24
  }
23
25
  export interface DevicePaymentPlanCreationAttributes extends Omit<DevicePaymentPlanAttributes, "id" | "createdAt" | "updatedAt" | "paidAmount" | "outstandingAmount" | "lastPaymentAt" | "nextInstallmentDueAt" | "status" | "completedAt" | "devices" | "installments"> {
24
26
  id?: string;
@@ -41,6 +43,7 @@ export declare class DevicePaymentPlan extends Model<InferAttributes<DevicePayme
41
43
  devices?: NonAttribute<Device[]>;
42
44
  user: NonAttribute<User>;
43
45
  installments?: NonAttribute<Installment[]>;
46
+ payment?: NonAttribute<Payment>;
44
47
  static initialize(sequelize: any): void;
45
48
  static associate(models: Record<string, ModelStatic<Model>>): void;
46
49
  updateProgress(): Promise<void>;
@@ -91,6 +91,13 @@ class DevicePaymentPlan extends vr_migrations_1.Model {
91
91
  onDelete: "SET NULL", // ← This is SET NULL, not CASCADE
92
92
  onUpdate: "CASCADE",
93
93
  });
94
+ // One-to-One with Payment (down payment/full payment upfront)
95
+ this.hasOne(models.Payment, {
96
+ foreignKey: "devicePaymentPlanId",
97
+ as: "downPayment", // DevicePaymentPlan.getDownPayment()
98
+ onDelete: "SET NULL",
99
+ onUpdate: "CASCADE",
100
+ });
94
101
  }
95
102
  // Custom instance methods
96
103
  async updateProgress() {
@@ -7,10 +7,10 @@ export * from "./payment.models";
7
7
  export * from "./pricing.models";
8
8
  export * from "./product.models";
9
9
  export * from "./securityClearance.models";
10
- export * from "./transaction.models";
10
+ export * from "./paymentEventLog.models";
11
11
  export * from "./installment.models";
12
12
  export * from "./ban.models";
13
13
  export * from "./suspension.models";
14
14
  export * from "./appSpecs.models";
15
15
  export * from "./phoneContact.models";
16
- export type { UserModel, TransactionModel, SecurityClearanceModel, ProductModel, PricingModel, PaymentModel, IdempotencyRecordModel, EventLogModel, DevicePaymentPlanModel, DeviceModel, InstallmentModel, BanModel, SuspensionModel, AppSpecsModel, PhoneContactModel, } from "./types";
16
+ export type { UserModel, SecurityClearanceModel, ProductModel, PricingModel, PaymentModel, PaymentEventLogModel, IdempotencyRecordModel, EventLogModel, DevicePaymentPlanModel, DeviceModel, InstallmentModel, BanModel, SuspensionModel, AppSpecsModel, PhoneContactModel, } from "./types";
@@ -23,7 +23,7 @@ __exportStar(require("./payment.models"), exports);
23
23
  __exportStar(require("./pricing.models"), exports);
24
24
  __exportStar(require("./product.models"), exports);
25
25
  __exportStar(require("./securityClearance.models"), exports);
26
- __exportStar(require("./transaction.models"), exports);
26
+ __exportStar(require("./paymentEventLog.models"), exports);
27
27
  __exportStar(require("./installment.models"), exports);
28
28
  __exportStar(require("./ban.models"), exports);
29
29
  __exportStar(require("./suspension.models"), exports);
@@ -1,65 +1,84 @@
1
1
  import { Model, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ModelStatic } from "vr-migrations";
2
2
  import type { User } from "./user.models";
3
3
  import type { DevicePaymentPlan } from "./devicePaymentPlan.models";
4
- import type { Transaction } from "./transaction.models";
4
+ import type { Installment } from "./installment.models";
5
5
  import type { IdempotencyRecord } from "./idempotencyRecord.models";
6
- export type PaymentProvider = "mtn_momo" | "airtel_money";
7
- export type PaymentStatus = "PENDING" | "SUCCESSFUL" | "FAILED";
8
- export declare const PAYMENT_PROVIDER: readonly ["mtn_momo", "airtel_money"];
9
- export declare const PAYMENT_STATUS: readonly ["PENDING", "SUCCESSFUL", "FAILED"];
6
+ import { PaymentEventLog } from "./paymentEventLog.models";
7
+ export type PaymentProvider = "MTN_MOMO" | "AIRTEL_MONEY" | "DPO" | "FLUTTERWAVE";
8
+ export type PaymentStatus = "PENDING" | "SUCCESSFUL" | "FAILED" | "REFUNDED";
9
+ export declare const PAYMENT_PROVIDER: readonly ["MTN_MOMO", "AIRTEL_MONEY", "DPO", "FLUTTERWAVE"];
10
+ export declare const PAYMENT_STATUS: readonly ["PENDING", "SUCCESSFUL", "FAILED", "REFUNDED"];
10
11
  export interface PaymentAttributes {
11
12
  id: string;
12
13
  userId: string;
13
- devicePaymentPlanId: string;
14
- transactionId: string | null;
14
+ devicePaymentPlanId: string | null;
15
+ installmentId: string | null;
15
16
  idempotencyKeyId: string | null;
16
17
  amount: number;
18
+ currency: string;
17
19
  provider: PaymentProvider;
18
20
  providerReference: string | null;
19
21
  status: PaymentStatus;
22
+ customerPhone: string;
23
+ customerName: string | null;
24
+ failureReason: string | null;
20
25
  metadata: Record<string, any>;
26
+ settledAt: Date | null;
21
27
  createdAt: Date;
22
28
  updatedAt: Date;
23
29
  user?: User;
24
30
  devicePaymentPlan?: DevicePaymentPlan;
25
- transaction?: Transaction;
31
+ installment?: Installment;
26
32
  idempotencyKey?: IdempotencyRecord;
33
+ paymentEventLogs?: PaymentEventLog[];
27
34
  }
28
- export interface PaymentCreationAttributes extends Omit<PaymentAttributes, "id" | "createdAt" | "updatedAt" | "status" | "providerReference" | "metadata" | "transactionId" | "idempotencyKeyId"> {
35
+ export interface PaymentCreationAttributes extends Omit<PaymentAttributes, "id" | "createdAt" | "updatedAt" | "status" | "providerReference" | "failureReason" | "settledAt" | "metadata"> {
29
36
  id?: string;
30
37
  status?: PaymentStatus;
31
38
  providerReference?: string | null;
39
+ failureReason?: string | null;
40
+ settledAt?: Date | null;
32
41
  metadata?: Record<string, any>;
33
- transactionId?: string | null;
34
- idempotencyKeyId?: string | null;
35
42
  }
36
43
  export declare class Payment extends Model<InferAttributes<Payment>, InferCreationAttributes<Payment>> implements PaymentAttributes {
37
44
  id: CreationOptional<string>;
38
45
  userId: string;
39
- devicePaymentPlanId: string;
40
- transactionId: CreationOptional<string | null>;
46
+ devicePaymentPlanId: CreationOptional<string | null>;
47
+ installmentId: CreationOptional<string | null>;
41
48
  idempotencyKeyId: CreationOptional<string | null>;
42
49
  amount: number;
50
+ currency: string;
43
51
  provider: PaymentProvider;
44
52
  providerReference: CreationOptional<string | null>;
45
53
  status: PaymentStatus;
54
+ customerPhone: string;
55
+ customerName: CreationOptional<string | null>;
56
+ failureReason: CreationOptional<string | null>;
46
57
  metadata: CreationOptional<Record<string, any>>;
58
+ settledAt: CreationOptional<Date | null>;
47
59
  readonly createdAt: CreationOptional<Date>;
48
60
  readonly updatedAt: CreationOptional<Date>;
49
61
  user?: NonAttribute<User>;
50
62
  devicePaymentPlan?: NonAttribute<DevicePaymentPlan>;
51
- transaction?: NonAttribute<Transaction>;
63
+ installment?: NonAttribute<Installment>;
52
64
  idempotencyKey?: NonAttribute<IdempotencyRecord>;
65
+ paymentEventLogs?: NonAttribute<PaymentEventLog[]>;
53
66
  static initialize(sequelize: any): void;
54
67
  static associate(models: Record<string, ModelStatic<Model>>): void;
55
- markAsSucceeded(providerReference: string, metadata?: Record<string, any>): Promise<void>;
68
+ markAsSuccessful(providerReference: string, metadata?: Record<string, any>): Promise<void>;
56
69
  markAsFailed(reason: string, metadata?: Record<string, any>): Promise<void>;
57
- process(transactionId: string, idempotencyKeyId?: string): Promise<void>;
70
+ markAsRefunded(metadata?: Record<string, any>): Promise<void>;
71
+ markAsSettled(settledAt?: Date): Promise<void>;
58
72
  isSuccessful(): boolean;
59
73
  isPending(): boolean;
60
74
  isFailed(): boolean;
61
- getProviderDisplayName(): string;
75
+ isRefunded(): boolean;
76
+ getFormattedAmount(): string;
77
+ getTargetType(): "payment_plan" | "installment";
78
+ getTargetId(): string;
62
79
  static getUserPayments(userId: string, limit?: number): Promise<Payment[]>;
63
- static getPaymentPlanPayments(devicePaymentPlanId: string): Promise<Payment[]>;
80
+ static getPlanPayments(planId: string): Promise<Payment[]>;
81
+ static getInstallmentPayments(installmentId: string): Promise<Payment[]>;
82
+ static getPendingPayments(): Promise<Payment[]>;
64
83
  }
65
84
  export type PaymentModel = typeof Payment;
@@ -1,10 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Payment = exports.PAYMENT_STATUS = exports.PAYMENT_PROVIDER = void 0;
4
+ // src/models/payment.models.ts
4
5
  const vr_migrations_1 = require("vr-migrations");
5
- // TYPES AS CONSTANTS
6
- exports.PAYMENT_PROVIDER = ["mtn_momo", "airtel_money"];
7
- exports.PAYMENT_STATUS = ["PENDING", "SUCCESSFUL", "FAILED"];
6
+ exports.PAYMENT_PROVIDER = [
7
+ "MTN_MOMO",
8
+ "AIRTEL_MONEY",
9
+ "DPO",
10
+ "FLUTTERWAVE",
11
+ ];
12
+ exports.PAYMENT_STATUS = [
13
+ "PENDING",
14
+ "SUCCESSFUL",
15
+ "FAILED",
16
+ "REFUNDED",
17
+ ];
8
18
  class Payment extends vr_migrations_1.Model {
9
19
  // Static initialization method
10
20
  static initialize(sequelize) {
@@ -14,43 +24,115 @@ class Payment extends vr_migrations_1.Model {
14
24
  defaultValue: vr_migrations_1.DataTypes.UUIDV4,
15
25
  primaryKey: true,
16
26
  },
17
- userId: { type: vr_migrations_1.DataTypes.UUID, allowNull: false },
18
- devicePaymentPlanId: { type: vr_migrations_1.DataTypes.UUID, allowNull: false },
19
- transactionId: { type: vr_migrations_1.DataTypes.UUID, allowNull: true },
20
- idempotencyKeyId: { type: vr_migrations_1.DataTypes.UUID, allowNull: true },
27
+ userId: {
28
+ type: vr_migrations_1.DataTypes.UUID,
29
+ allowNull: false,
30
+ },
31
+ devicePaymentPlanId: {
32
+ type: vr_migrations_1.DataTypes.UUID,
33
+ allowNull: true,
34
+ },
35
+ installmentId: {
36
+ type: vr_migrations_1.DataTypes.UUID,
37
+ allowNull: true,
38
+ },
39
+ idempotencyKeyId: {
40
+ type: vr_migrations_1.DataTypes.UUID,
41
+ allowNull: true,
42
+ },
21
43
  amount: {
22
- type: vr_migrations_1.DataTypes.FLOAT,
44
+ type: vr_migrations_1.DataTypes.DECIMAL(10, 2),
23
45
  allowNull: false,
24
46
  validate: { min: 1 },
25
47
  },
48
+ currency: {
49
+ type: vr_migrations_1.DataTypes.STRING(3),
50
+ allowNull: false,
51
+ defaultValue: "RWF",
52
+ },
26
53
  provider: {
27
- type: vr_migrations_1.DataTypes.ENUM("mtn_momo", "airtel_money"),
54
+ type: vr_migrations_1.DataTypes.ENUM("MTN_MOMO", "AIRTEL_MONEY", "DPO", "FLUTTERWAVE"),
28
55
  allowNull: false,
29
56
  },
30
- providerReference: { type: vr_migrations_1.DataTypes.STRING, allowNull: true },
57
+ providerReference: {
58
+ type: vr_migrations_1.DataTypes.STRING(255),
59
+ allowNull: true,
60
+ },
31
61
  status: {
32
- type: vr_migrations_1.DataTypes.ENUM("PENDING", "SUCCESSFUL", "FAILED"),
62
+ type: vr_migrations_1.DataTypes.ENUM("PENDING", "SUCCESSFUL", "FAILED", "REFUNDED"),
33
63
  allowNull: false,
34
64
  defaultValue: "PENDING",
35
65
  },
66
+ customerPhone: {
67
+ type: vr_migrations_1.DataTypes.STRING(20),
68
+ allowNull: false,
69
+ },
70
+ customerName: {
71
+ type: vr_migrations_1.DataTypes.STRING(100),
72
+ allowNull: true,
73
+ },
74
+ failureReason: {
75
+ type: vr_migrations_1.DataTypes.TEXT,
76
+ allowNull: true,
77
+ },
36
78
  metadata: {
37
79
  type: vr_migrations_1.DataTypes.JSONB,
38
80
  allowNull: false,
39
81
  defaultValue: {},
40
82
  },
41
- createdAt: { type: vr_migrations_1.DataTypes.DATE, defaultValue: vr_migrations_1.DataTypes.NOW },
42
- updatedAt: { type: vr_migrations_1.DataTypes.DATE, defaultValue: vr_migrations_1.DataTypes.NOW },
83
+ settledAt: {
84
+ type: vr_migrations_1.DataTypes.DATE,
85
+ allowNull: true,
86
+ },
87
+ createdAt: {
88
+ type: vr_migrations_1.DataTypes.DATE,
89
+ defaultValue: vr_migrations_1.DataTypes.NOW,
90
+ },
91
+ updatedAt: {
92
+ type: vr_migrations_1.DataTypes.DATE,
93
+ defaultValue: vr_migrations_1.DataTypes.NOW,
94
+ },
43
95
  }, {
44
96
  sequelize,
45
- tableName: "payments",
46
97
  modelName: "Payment",
47
- hooks: {
48
- beforeUpdate: (payment) => {
49
- if (payment.status === "SUCCESSFUL" && !payment.providerReference) {
50
- throw new Error("providerReference is required for successful payments");
98
+ tableName: "payments",
99
+ timestamps: true,
100
+ freezeTableName: true,
101
+ validate: {
102
+ exactlyOneTarget() {
103
+ const hasPlan = !!this.devicePaymentPlanId;
104
+ const hasInstallment = !!this.installmentId;
105
+ if ((hasPlan && hasInstallment) || (!hasPlan && !hasInstallment)) {
106
+ throw new Error("Payment must belong to exactly one: devicePaymentPlanId OR installmentId");
51
107
  }
52
108
  },
53
109
  },
110
+ indexes: [
111
+ {
112
+ fields: ["userId"],
113
+ name: "payment_user_idx",
114
+ },
115
+ {
116
+ fields: ["devicePaymentPlanId"],
117
+ name: "payment_plan_idx",
118
+ },
119
+ {
120
+ fields: ["installmentId"],
121
+ name: "payment_installment_idx",
122
+ },
123
+ {
124
+ fields: ["status"],
125
+ name: "payment_status_idx",
126
+ },
127
+ {
128
+ fields: ["providerReference"],
129
+ name: "payment_provider_ref_idx",
130
+ },
131
+ {
132
+ fields: ["createdAt"],
133
+ name: "payment_created_at_idx",
134
+ },
135
+ ],
54
136
  });
55
137
  }
56
138
  // Static association method
@@ -64,12 +146,12 @@ class Payment extends vr_migrations_1.Model {
64
146
  this.belongsTo(models.DevicePaymentPlan, {
65
147
  foreignKey: "devicePaymentPlanId",
66
148
  as: "devicePaymentPlan",
67
- onDelete: "RESTRICT",
149
+ onDelete: "SET NULL",
68
150
  onUpdate: "CASCADE",
69
151
  });
70
- this.belongsTo(models.Transaction, {
71
- foreignKey: "transactionId",
72
- as: "transaction",
152
+ this.belongsTo(models.Installment, {
153
+ foreignKey: "installmentId",
154
+ as: "installment",
73
155
  onDelete: "SET NULL",
74
156
  onUpdate: "CASCADE",
75
157
  });
@@ -79,33 +161,49 @@ class Payment extends vr_migrations_1.Model {
79
161
  onDelete: "SET NULL",
80
162
  onUpdate: "CASCADE",
81
163
  });
164
+ this.hasMany(models.PaymentEventLog, {
165
+ foreignKey: "paymentId",
166
+ as: "eventLogs", // Payment.getEventLogs()
167
+ onDelete: "SET NULL",
168
+ });
82
169
  }
83
170
  // Custom instance methods
84
- async markAsSucceeded(providerReference, metadata) {
171
+ async markAsSuccessful(providerReference, metadata) {
85
172
  this.status = "SUCCESSFUL";
86
173
  this.providerReference = providerReference;
87
174
  if (metadata) {
88
175
  this.metadata = { ...this.metadata, ...metadata };
89
176
  }
90
177
  await this.save();
178
+ // If this payment is for an installment, mark it as paid
179
+ if (this.installmentId) {
180
+ const Installment = this.constructor.sequelize.models
181
+ .Installment;
182
+ const installment = await Installment.findByPk(this.installmentId);
183
+ if (installment && installment.status !== "PAID") {
184
+ await installment.markAsPaid(this.id);
185
+ }
186
+ }
91
187
  }
92
188
  async markAsFailed(reason, metadata) {
93
189
  this.status = "FAILED";
94
- const failureMetadata = {
95
- failureReason: reason,
96
- failedAt: new Date().toISOString(),
97
- ...metadata,
98
- };
99
- this.metadata = { ...this.metadata, ...failureMetadata };
190
+ this.failureReason = reason;
191
+ if (metadata) {
192
+ this.metadata = { ...this.metadata, ...metadata };
193
+ }
100
194
  await this.save();
101
195
  }
102
- async process(transactionId, idempotencyKeyId) {
103
- this.transactionId = transactionId;
104
- if (idempotencyKeyId) {
105
- this.idempotencyKeyId = idempotencyKeyId;
196
+ async markAsRefunded(metadata) {
197
+ this.status = "REFUNDED";
198
+ if (metadata) {
199
+ this.metadata = { ...this.metadata, ...metadata };
106
200
  }
107
201
  await this.save();
108
202
  }
203
+ async markAsSettled(settledAt = new Date()) {
204
+ this.settledAt = settledAt;
205
+ await this.save();
206
+ }
109
207
  isSuccessful() {
110
208
  return this.status === "SUCCESSFUL";
111
209
  }
@@ -115,26 +213,42 @@ class Payment extends vr_migrations_1.Model {
115
213
  isFailed() {
116
214
  return this.status === "FAILED";
117
215
  }
118
- getProviderDisplayName() {
119
- switch (this.provider) {
120
- case "mtn_momo":
121
- return "MTN Mobile Money";
122
- case "airtel_money":
123
- return "Airtel Money";
124
- default:
125
- return this.provider;
126
- }
216
+ isRefunded() {
217
+ return this.status === "REFUNDED";
218
+ }
219
+ getFormattedAmount() {
220
+ return `${this.currency} ${this.amount.toLocaleString()}`;
221
+ }
222
+ getTargetType() {
223
+ return this.devicePaymentPlanId ? "payment_plan" : "installment";
127
224
  }
225
+ getTargetId() {
226
+ return (this.devicePaymentPlanId || this.installmentId);
227
+ }
228
+ // Static methods
128
229
  static async getUserPayments(userId, limit = 50) {
129
230
  return await this.findAll({
130
231
  where: { userId },
131
232
  order: [["createdAt", "DESC"]],
132
233
  limit,
234
+ include: ["devicePaymentPlan", "installment"],
235
+ });
236
+ }
237
+ static async getPlanPayments(planId) {
238
+ return await this.findAll({
239
+ where: { devicePaymentPlanId: planId },
240
+ order: [["createdAt", "ASC"]],
241
+ });
242
+ }
243
+ static async getInstallmentPayments(installmentId) {
244
+ return await this.findAll({
245
+ where: { installmentId },
246
+ order: [["createdAt", "ASC"]],
133
247
  });
134
248
  }
135
- static async getPaymentPlanPayments(devicePaymentPlanId) {
249
+ static async getPendingPayments() {
136
250
  return await this.findAll({
137
- where: { devicePaymentPlanId },
251
+ where: { status: "PENDING" },
138
252
  order: [["createdAt", "ASC"]],
139
253
  });
140
254
  }
@@ -0,0 +1,69 @@
1
+ import { Model, InferAttributes, InferCreationAttributes, CreationOptional, ModelStatic, NonAttribute } from "vr-migrations";
2
+ import type { Payment } from "./payment.models";
3
+ export type EventDirection = "OUTGOING_REQUEST" | "INCOMING_WEBHOOK";
4
+ export type EventStatus = "PENDING" | "SUCCESS" | "FAILED" | "RETRIED";
5
+ export declare const EVENT_DIRECTION: readonly ["OUTGOING_REQUEST", "INCOMING_WEBHOOK"];
6
+ export declare const EVENT_STATUS: readonly ["PENDING", "SUCCESS", "FAILED", "RETRIED"];
7
+ export interface PaymentEventLogAttributes {
8
+ id: string;
9
+ paymentId: string | null;
10
+ provider: string;
11
+ direction: EventDirection;
12
+ action: string;
13
+ externalId: string | null;
14
+ requestPayload: Record<string, any> | null;
15
+ responsePayload: Record<string, any> | null;
16
+ statusCode: number | null;
17
+ status: EventStatus;
18
+ error: string | null;
19
+ durationMs: number | null;
20
+ retryCount: number;
21
+ nextRetryAt: Date | null;
22
+ processedAt: Date | null;
23
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ }
26
+ export interface PaymentEventLogCreationAttributes extends Omit<PaymentEventLogAttributes, "id" | "createdAt" | "updatedAt" | "status" | "error" | "retryCount" | "nextRetryAt" | "processedAt" | "durationMs"> {
27
+ id?: string;
28
+ status?: EventStatus;
29
+ error?: string | null;
30
+ retryCount?: number;
31
+ nextRetryAt?: Date | null;
32
+ processedAt?: Date | null;
33
+ durationMs?: number | null;
34
+ payment?: Payment;
35
+ }
36
+ export declare class PaymentEventLog extends Model<InferAttributes<PaymentEventLog>, InferCreationAttributes<PaymentEventLog>> implements PaymentEventLogAttributes {
37
+ id: CreationOptional<string>;
38
+ paymentId: CreationOptional<string | null>;
39
+ provider: string;
40
+ direction: EventDirection;
41
+ action: string;
42
+ externalId: CreationOptional<string | null>;
43
+ requestPayload: CreationOptional<Record<string, any> | null>;
44
+ responsePayload: CreationOptional<Record<string, any> | null>;
45
+ statusCode: CreationOptional<number | null>;
46
+ status: EventStatus;
47
+ error: CreationOptional<string | null>;
48
+ durationMs: CreationOptional<number | null>;
49
+ retryCount: CreationOptional<number>;
50
+ nextRetryAt: CreationOptional<Date | null>;
51
+ processedAt: CreationOptional<Date | null>;
52
+ payment?: NonAttribute<Payment>;
53
+ readonly createdAt: CreationOptional<Date>;
54
+ readonly updatedAt: CreationOptional<Date>;
55
+ static initialize(sequelize: any): void;
56
+ static associate(models: Record<string, ModelStatic<Model>>): void;
57
+ markAsSuccess(durationMs?: number): Promise<void>;
58
+ markAsFailed(error: string, durationMs?: number): Promise<void>;
59
+ scheduleRetry(retryAfterSeconds: number): Promise<void>;
60
+ isPending(): boolean;
61
+ isSuccess(): boolean;
62
+ isFailed(): boolean;
63
+ isRetryable(): boolean;
64
+ static getUnprocessedWebhooks(limit?: number): Promise<PaymentEventLog[]>;
65
+ static getPendingRetries(): Promise<PaymentEventLog[]>;
66
+ static getPaymentEvents(paymentId: string): Promise<PaymentEventLog[]>;
67
+ static getProviderEvents(provider: string, externalId: string): Promise<PaymentEventLog[]>;
68
+ }
69
+ export type PaymentEventLogModel = typeof PaymentEventLog;
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PaymentEventLog = exports.EVENT_STATUS = exports.EVENT_DIRECTION = void 0;
4
+ // src/models/paymentEventLog.models.ts
5
+ const vr_migrations_1 = require("vr-migrations");
6
+ exports.EVENT_DIRECTION = [
7
+ "OUTGOING_REQUEST",
8
+ "INCOMING_WEBHOOK",
9
+ ];
10
+ exports.EVENT_STATUS = [
11
+ "PENDING",
12
+ "SUCCESS",
13
+ "FAILED",
14
+ "RETRIED",
15
+ ];
16
+ class PaymentEventLog extends vr_migrations_1.Model {
17
+ // Static initialization method
18
+ static initialize(sequelize) {
19
+ this.init({
20
+ id: {
21
+ type: vr_migrations_1.DataTypes.UUID,
22
+ defaultValue: vr_migrations_1.DataTypes.UUIDV4,
23
+ primaryKey: true,
24
+ },
25
+ paymentId: {
26
+ type: vr_migrations_1.DataTypes.UUID,
27
+ allowNull: true,
28
+ references: {
29
+ model: "payments",
30
+ key: "id",
31
+ },
32
+ },
33
+ provider: {
34
+ type: vr_migrations_1.DataTypes.STRING(50),
35
+ allowNull: false,
36
+ },
37
+ direction: {
38
+ type: vr_migrations_1.DataTypes.ENUM("OUTGOING_REQUEST", "INCOMING_WEBHOOK"),
39
+ allowNull: false,
40
+ },
41
+ action: {
42
+ type: vr_migrations_1.DataTypes.STRING(50),
43
+ allowNull: false,
44
+ },
45
+ externalId: {
46
+ type: vr_migrations_1.DataTypes.STRING(255),
47
+ allowNull: true,
48
+ },
49
+ requestPayload: {
50
+ type: vr_migrations_1.DataTypes.JSONB,
51
+ allowNull: true,
52
+ },
53
+ responsePayload: {
54
+ type: vr_migrations_1.DataTypes.JSONB,
55
+ allowNull: true,
56
+ },
57
+ statusCode: {
58
+ type: vr_migrations_1.DataTypes.INTEGER,
59
+ allowNull: true,
60
+ },
61
+ status: {
62
+ type: vr_migrations_1.DataTypes.ENUM("PENDING", "SUCCESS", "FAILED", "RETRIED"),
63
+ allowNull: false,
64
+ defaultValue: "PENDING",
65
+ },
66
+ error: {
67
+ type: vr_migrations_1.DataTypes.TEXT,
68
+ allowNull: true,
69
+ },
70
+ durationMs: {
71
+ type: vr_migrations_1.DataTypes.INTEGER,
72
+ allowNull: true,
73
+ },
74
+ retryCount: {
75
+ type: vr_migrations_1.DataTypes.INTEGER,
76
+ allowNull: false,
77
+ defaultValue: 0,
78
+ },
79
+ nextRetryAt: {
80
+ type: vr_migrations_1.DataTypes.DATE,
81
+ allowNull: true,
82
+ },
83
+ processedAt: {
84
+ type: vr_migrations_1.DataTypes.DATE,
85
+ allowNull: true,
86
+ },
87
+ createdAt: {
88
+ type: vr_migrations_1.DataTypes.DATE,
89
+ defaultValue: vr_migrations_1.DataTypes.NOW,
90
+ },
91
+ updatedAt: {
92
+ type: vr_migrations_1.DataTypes.DATE,
93
+ defaultValue: vr_migrations_1.DataTypes.NOW,
94
+ },
95
+ }, {
96
+ sequelize,
97
+ modelName: "PaymentEventLog",
98
+ tableName: "payment_event_logs",
99
+ timestamps: true,
100
+ freezeTableName: true,
101
+ indexes: [
102
+ {
103
+ fields: ["paymentId"],
104
+ name: "payment_event_payment_idx",
105
+ },
106
+ {
107
+ fields: ["provider", "externalId"],
108
+ name: "payment_event_provider_external_idx",
109
+ },
110
+ {
111
+ fields: ["status", "nextRetryAt"],
112
+ name: "payment_event_retry_idx",
113
+ },
114
+ {
115
+ fields: ["createdAt"],
116
+ name: "payment_event_created_at_idx",
117
+ },
118
+ {
119
+ fields: ["direction", "action"],
120
+ name: "payment_event_direction_action_idx",
121
+ },
122
+ ],
123
+ });
124
+ }
125
+ // Static association method
126
+ static associate(models) {
127
+ this.belongsTo(models.Payment, {
128
+ foreignKey: "paymentId",
129
+ as: "payment",
130
+ onDelete: "SET NULL",
131
+ onUpdate: "CASCADE",
132
+ });
133
+ }
134
+ // Custom instance methods
135
+ async markAsSuccess(durationMs) {
136
+ this.status = "SUCCESS";
137
+ this.processedAt = new Date();
138
+ if (durationMs !== undefined) {
139
+ this.durationMs = durationMs;
140
+ }
141
+ await this.save();
142
+ }
143
+ async markAsFailed(error, durationMs) {
144
+ this.status = "FAILED";
145
+ this.error = error;
146
+ this.processedAt = new Date();
147
+ if (durationMs !== undefined) {
148
+ this.durationMs = durationMs;
149
+ }
150
+ await this.save();
151
+ }
152
+ async scheduleRetry(retryAfterSeconds) {
153
+ this.status = "RETRIED";
154
+ this.retryCount += 1;
155
+ this.nextRetryAt = new Date(Date.now() + retryAfterSeconds * 1000);
156
+ await this.save();
157
+ }
158
+ isPending() {
159
+ return this.status === "PENDING";
160
+ }
161
+ isSuccess() {
162
+ return this.status === "SUCCESS";
163
+ }
164
+ isFailed() {
165
+ return this.status === "FAILED";
166
+ }
167
+ isRetryable() {
168
+ return this.status === "FAILED" && this.retryCount < 5;
169
+ }
170
+ // Static methods
171
+ static async getUnprocessedWebhooks(limit = 100) {
172
+ return await this.findAll({
173
+ where: {
174
+ direction: "INCOMING_WEBHOOK",
175
+ status: "PENDING",
176
+ processedAt: null,
177
+ },
178
+ limit,
179
+ order: [["createdAt", "ASC"]],
180
+ });
181
+ }
182
+ static async getPendingRetries() {
183
+ return await this.findAll({
184
+ where: {
185
+ status: "RETRIED",
186
+ nextRetryAt: { [vr_migrations_1.Op.lte]: new Date() },
187
+ },
188
+ order: [["nextRetryAt", "ASC"]],
189
+ });
190
+ }
191
+ static async getPaymentEvents(paymentId) {
192
+ return await this.findAll({
193
+ where: { paymentId },
194
+ order: [["createdAt", "ASC"]],
195
+ });
196
+ }
197
+ static async getProviderEvents(provider, externalId) {
198
+ return await this.findAll({
199
+ where: { provider, externalId },
200
+ order: [["createdAt", "ASC"]],
201
+ });
202
+ }
203
+ }
204
+ exports.PaymentEventLog = PaymentEventLog;
@@ -1,5 +1,4 @@
1
1
  export type { UserModel } from "./user.models";
2
- export type { TransactionModel } from "./transaction.models";
3
2
  export type { SecurityClearanceModel } from "./securityClearance.models";
4
3
  export type { ProductModel } from "./product.models";
5
4
  export type { PricingModel } from "./pricing.models";
@@ -13,3 +12,4 @@ export type { BanModel } from "./ban.models";
13
12
  export type { SuspensionModel } from "./suspension.models";
14
13
  export type { AppSpecsModel } from "./appSpecs.models";
15
14
  export type { PhoneContactModel } from "./phoneContact.models";
15
+ export type { PaymentEventLogModel } from "./paymentEventLog.models";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vr-models",
3
- "version": "1.0.56",
3
+ "version": "1.0.57",
4
4
  "description": "Shared database models package for VR applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,44 +0,0 @@
1
- import { Model, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ModelStatic } from "vr-migrations";
2
- import type { Payment } from "./payment.models";
3
- import type { User } from "./user.models";
4
- export type TransactionStatus = "succeeded" | "failed";
5
- export interface TransactionAttributes {
6
- id: string;
7
- userId: string;
8
- paymentId: string;
9
- amount: number;
10
- status: TransactionStatus;
11
- providerReference: string;
12
- metadata: Record<string, any>;
13
- createdAt: Date;
14
- updatedAt: Date;
15
- payment?: Payment;
16
- user?: User;
17
- }
18
- export interface TransactionCreationAttributes extends Omit<TransactionAttributes, "id" | "createdAt" | "updatedAt" | "metadata"> {
19
- id?: string;
20
- metadata?: Record<string, any>;
21
- }
22
- export declare class Transaction extends Model<InferAttributes<Transaction>, InferCreationAttributes<Transaction>> implements TransactionAttributes {
23
- id: CreationOptional<string>;
24
- userId: string;
25
- paymentId: string;
26
- amount: number;
27
- status: TransactionStatus;
28
- providerReference: string;
29
- metadata: CreationOptional<Record<string, any>>;
30
- readonly createdAt: CreationOptional<Date>;
31
- readonly updatedAt: CreationOptional<Date>;
32
- payment?: NonAttribute<Payment>;
33
- user?: NonAttribute<User>;
34
- static initialize(sequelize: any): void;
35
- static associate(models: Record<string, ModelStatic<Model>>): void;
36
- markAsSucceeded(metadata?: Record<string, any>): Promise<void>;
37
- markAsFailed(reason: string, metadata?: Record<string, any>): Promise<void>;
38
- isSuccessful(): boolean;
39
- isFailed(): boolean;
40
- getFormattedAmount(): string;
41
- static getUserTransactions(userId: string, limit?: number): Promise<Transaction[]>;
42
- static getPaymentTransactions(paymentId: string): Promise<Transaction[]>;
43
- }
44
- export type TransactionModel = typeof Transaction;
@@ -1,97 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Transaction = void 0;
4
- const vr_migrations_1 = require("vr-migrations");
5
- class Transaction extends vr_migrations_1.Model {
6
- // Static initialization method
7
- static initialize(sequelize) {
8
- this.init({
9
- id: {
10
- type: vr_migrations_1.DataTypes.UUID,
11
- defaultValue: vr_migrations_1.DataTypes.UUIDV4,
12
- primaryKey: true,
13
- },
14
- userId: { type: vr_migrations_1.DataTypes.UUID, allowNull: false },
15
- paymentId: { type: vr_migrations_1.DataTypes.UUID, allowNull: false },
16
- amount: {
17
- type: vr_migrations_1.DataTypes.FLOAT,
18
- allowNull: false,
19
- validate: { min: 1 },
20
- },
21
- status: {
22
- type: vr_migrations_1.DataTypes.ENUM("succeeded", "failed"),
23
- allowNull: false,
24
- },
25
- providerReference: { type: vr_migrations_1.DataTypes.STRING, allowNull: false },
26
- metadata: {
27
- type: vr_migrations_1.DataTypes.JSONB,
28
- allowNull: false,
29
- defaultValue: {},
30
- },
31
- createdAt: { type: vr_migrations_1.DataTypes.DATE, defaultValue: vr_migrations_1.DataTypes.NOW },
32
- updatedAt: { type: vr_migrations_1.DataTypes.DATE, defaultValue: vr_migrations_1.DataTypes.NOW },
33
- }, {
34
- sequelize,
35
- tableName: "transactions",
36
- modelName: "Transaction",
37
- timestamps: true,
38
- });
39
- }
40
- // Static association method
41
- static associate(models) {
42
- this.belongsTo(models.Payment, {
43
- foreignKey: "paymentId",
44
- as: "payment",
45
- onDelete: "SET NULL", // ← This is SET NULL, not CASCADE
46
- onUpdate: "CASCADE",
47
- });
48
- this.belongsTo(models.User, {
49
- foreignKey: "userId",
50
- as: "user",
51
- onDelete: "RESTRICT",
52
- onUpdate: "CASCADE",
53
- });
54
- }
55
- // Custom instance methods
56
- async markAsSucceeded(metadata) {
57
- this.status = "succeeded";
58
- if (metadata) {
59
- this.metadata = { ...this.metadata, ...metadata };
60
- }
61
- await this.save();
62
- }
63
- async markAsFailed(reason, metadata) {
64
- this.status = "failed";
65
- const failureMetadata = {
66
- failureReason: reason,
67
- failedAt: new Date().toISOString(),
68
- ...metadata,
69
- };
70
- this.metadata = { ...this.metadata, ...failureMetadata };
71
- await this.save();
72
- }
73
- isSuccessful() {
74
- return this.status === "succeeded";
75
- }
76
- isFailed() {
77
- return this.status === "failed";
78
- }
79
- getFormattedAmount() {
80
- return `RWF ${this.amount.toLocaleString()}`;
81
- }
82
- static async getUserTransactions(userId, limit = 50) {
83
- return await this.findAll({
84
- where: { userId },
85
- order: [["createdAt", "DESC"]],
86
- limit,
87
- include: ["payment"],
88
- });
89
- }
90
- static async getPaymentTransactions(paymentId) {
91
- return await this.findAll({
92
- where: { paymentId },
93
- order: [["createdAt", "DESC"]],
94
- });
95
- }
96
- }
97
- exports.Transaction = Transaction;