vr-models 1.0.55 → 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() {
@@ -10,7 +10,7 @@ export interface DeviceSessionPayload {
10
10
  deviceSerialNumber: string;
11
11
  minutesGranted: number;
12
12
  }
13
- export declare const EVENT_ACTIONS: readonly ["USER_READ", "USER_CREATED", "USER_UPDATED", "USER_DELETED", "USER_SUSPENDED", "USER_BANNED", "USER_VERIFIED", "USER_UPGRADED_TO_RIDER", "USERS_LISTED", "ADMIN_FORGOT_PASSWORD", "ADMIN_LOGGED_IN", "ADMIN_LOGGED_OUT", "PASSENGER_ACCOUNT_DEACTIVATED", "PASSENGER_ACCOUNT_DELETED", "AGENT_CREATED_RIDER", "AGENT_UPDATED_RIDER", "ADMIN_CREATED_USER", "ADMIN_UPDATED_USER", "ADMIN_CHANGED_PASSWORD", "ADMIN_RESET_USER_PASSWORD", "ADMIN_BULK_PASSWORD_RESET", "SUPER_ADMIN_PASSWORD_CHANGED", "SUPER_ADMIN_CHANGED_USER_ACCOUNT_STATUS", "SUPER_ADMIN_UPDATED_USER", "SUPER_ADMIN_CREATED_USER", "PRODUCT_CREATED", "PRODUCT_READ", "PRODUCT_UPDATED", "PRODUCT_DELETED", "PRODUCTS_LISTED", "PRODUCTS_ACTIVATED", "PRODUCTS_DEACTIVATED", "PRODUCT_STOCK_SET_MANUALLY", "PRODUCT_STOCK_INCREMENTED", "PRODUCT_STOCK_DECREMENTED", "PRICING_CREATED", "PRICING_READ", "PRICING_UPDATED", "PRICING_DELETED", "DEVICE_CREATED", "DEVICES_BULK_CREATED", "DEVICE_ASSIGNED", "DEVICE_READ", "DEVICE_UPDATED", "DEVICES_LISTED", "DEVICE_LOCKED", "DEVICE_UNLOCKED", "DEVICE_DISABLED", "DEVICE_REACTIVATED", "DEVICE_DELETED_PERMANENTLY", "DEVICE_REPOSSESSED", "DEVICE_PAYMENT_PLAN_CREATED", "DEVICE_PAYMENT_PLAN_READ", "DEVICE_PAYMENT_PLAN_UPDATED", "DEVICE_PAYMENT_PLAN_MARKED_DEFAULTED", "DEVICE_PAYMENT_PLAN_SOFT_DELETED", "DEVICE_PAYMENT_PLAN_DELETED_PERMANENTLY", "PAYMENT_RECORDED", "PAYMENT_READ", "PAYMENTS_LISTED", "TRANSACTION_READ", "TRANSACTIONS_LISTED", "EVENT_LOG_READ", "EVENT_LOGS_LISTED", "SECURITY_CLEARANCE_MANAGED", "DEVICE_LOCK_OVERRIDDEN", "SESSION_EXTENDED"];
13
+ export declare const EVENT_ACTIONS: readonly ["USER_READ", "USER_CREATED", "ADMIN_CREATED", "PROFILE_COMPLETED", "USER_UPDATED", "USER_DELETED", "USER_SUSPENDED", "USER_BANNED", "USER_VERIFIED", "USER_UPGRADED_TO_RIDER", "USERS_LISTED", "ADMIN_FORGOT_PASSWORD", "ADMIN_LOGGED_IN", "ADMIN_LOGGED_OUT", "PASSENGER_ACCOUNT_DEACTIVATED", "PASSENGER_ACCOUNT_DELETED", "AGENT_CREATED_RIDER", "AGENT_UPDATED_RIDER", "ADMIN_CREATED_USER", "ADMIN_UPDATED_USER", "ADMIN_CHANGED_PASSWORD", "ADMIN_RESET_USER_PASSWORD", "ADMIN_BULK_PASSWORD_RESET", "SUPER_ADMIN_PASSWORD_CHANGED", "SUPER_ADMIN_CHANGED_USER_ACCOUNT_STATUS", "SUPER_ADMIN_UPDATED_USER", "SUPER_ADMIN_CREATED_USER", "PRODUCT_CREATED", "PRODUCT_READ", "PRODUCT_UPDATED", "PRODUCT_DELETED", "PRODUCTS_LISTED", "PRODUCTS_ACTIVATED", "PRODUCTS_DEACTIVATED", "PRODUCT_STOCK_SET_MANUALLY", "PRODUCT_STOCK_INCREMENTED", "PRODUCT_STOCK_DECREMENTED", "PRICING_CREATED", "PRICING_READ", "PRICING_UPDATED", "PRICING_DELETED", "DEVICE_CREATED", "DEVICES_BULK_CREATED", "DEVICE_ASSIGNED", "DEVICE_READ", "DEVICE_UPDATED", "DEVICES_LISTED", "DEVICE_LOCKED", "DEVICE_UNLOCKED", "DEVICE_DISABLED", "DEVICE_REACTIVATED", "DEVICE_DELETED_PERMANENTLY", "DEVICE_REPOSSESSED", "DEVICE_PAYMENT_PLAN_CREATED", "DEVICE_PAYMENT_PLAN_READ", "DEVICE_PAYMENT_PLAN_UPDATED", "DEVICE_PAYMENT_PLAN_MARKED_DEFAULTED", "DEVICE_PAYMENT_PLAN_SOFT_DELETED", "DEVICE_PAYMENT_PLAN_DELETED_PERMANENTLY", "PAYMENT_RECORDED", "PAYMENT_READ", "PAYMENTS_LISTED", "TRANSACTION_READ", "TRANSACTIONS_LISTED", "EVENT_LOG_READ", "EVENT_LOGS_LISTED", "SECURITY_CLEARANCE_MANAGED", "DEVICE_LOCK_OVERRIDDEN", "SESSION_EXTENDED"];
14
14
  export type EventAction = (typeof EVENT_ACTIONS)[number];
15
15
  export interface EventLogAttributes {
16
16
  id: string;
@@ -6,6 +6,8 @@ const vr_migrations_1 = require("vr-migrations");
6
6
  exports.EVENT_ACTIONS = [
7
7
  "USER_READ",
8
8
  "USER_CREATED",
9
+ "ADMIN_CREATED",
10
+ "PROFILE_COMPLETED",
9
11
  "USER_UPDATED",
10
12
  "USER_DELETED",
11
13
  "USER_SUSPENDED",
@@ -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";
@@ -11,6 +11,8 @@ export interface DeletionStatus {
11
11
  deletedBy?: string | null;
12
12
  reason?: string | null;
13
13
  }
14
+ export declare const GENDER_OPTIONS: readonly ["male", "female"];
15
+ export type Gender = (typeof GENDER_OPTIONS)[number];
14
16
  export interface UserAttributes {
15
17
  id: string;
16
18
  firstName: string;
@@ -20,7 +22,10 @@ export interface UserAttributes {
20
22
  password?: string | null;
21
23
  securityClearanceId: string;
22
24
  plateNumber?: string | null;
23
- nationalId?: string | null;
25
+ gender: Gender;
26
+ birthDate?: number | null;
27
+ birthMonth?: number | null;
28
+ birthYear?: number | null;
24
29
  primaryPhoneId: string | null;
25
30
  isActive: boolean;
26
31
  forgotPassword: boolean;
@@ -54,7 +59,10 @@ export declare class User extends Model<InferAttributes<User>, InferCreationAttr
54
59
  password: CreationOptional<string | null>;
55
60
  securityClearanceId: string;
56
61
  plateNumber: CreationOptional<string | null>;
57
- nationalId: CreationOptional<string | null>;
62
+ gender: Gender;
63
+ birthDate: CreationOptional<number | null>;
64
+ birthMonth: CreationOptional<number | null>;
65
+ birthYear: CreationOptional<number | null>;
58
66
  primaryPhoneId: CreationOptional<string | null>;
59
67
  isActive: CreationOptional<boolean>;
60
68
  forgotPassword: CreationOptional<boolean>;
@@ -78,5 +86,7 @@ export declare class User extends Model<InferAttributes<User>, InferCreationAttr
78
86
  deactivate(): Promise<void>;
79
87
  activate(): Promise<void>;
80
88
  hasVerifiedPhone(): Promise<boolean>;
89
+ getBirthDate(): Date | null;
90
+ getAge(): number | null;
81
91
  }
82
92
  export type UserModel = typeof User;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.User = void 0;
3
+ exports.User = exports.GENDER_OPTIONS = void 0;
4
4
  // src/models/user.models.ts
5
5
  const vr_migrations_1 = require("vr-migrations");
6
+ // ==================== GENDER OPTIONS ====================
7
+ exports.GENDER_OPTIONS = ["male", "female"];
6
8
  class User extends vr_migrations_1.Model {
7
9
  // Static initialization method
8
10
  static initialize(sequelize) {
@@ -20,11 +22,6 @@ class User extends vr_migrations_1.Model {
20
22
  type: vr_migrations_1.DataTypes.STRING(100),
21
23
  allowNull: false,
22
24
  },
23
- nationalId: {
24
- type: vr_migrations_1.DataTypes.STRING(100),
25
- allowNull: true,
26
- unique: true,
27
- },
28
25
  jacketId: {
29
26
  type: vr_migrations_1.DataTypes.STRING(100),
30
27
  allowNull: true,
@@ -47,6 +44,35 @@ class User extends vr_migrations_1.Model {
47
44
  type: vr_migrations_1.DataTypes.STRING(100),
48
45
  allowNull: true,
49
46
  },
47
+ gender: {
48
+ type: vr_migrations_1.DataTypes.ENUM("male", "female"),
49
+ allowNull: false,
50
+ defaultValue: "male",
51
+ },
52
+ birthDate: {
53
+ type: vr_migrations_1.DataTypes.INTEGER,
54
+ allowNull: true,
55
+ validate: {
56
+ min: 1,
57
+ max: 31,
58
+ },
59
+ },
60
+ birthMonth: {
61
+ type: vr_migrations_1.DataTypes.INTEGER,
62
+ allowNull: true,
63
+ validate: {
64
+ min: 1,
65
+ max: 12,
66
+ },
67
+ },
68
+ birthYear: {
69
+ type: vr_migrations_1.DataTypes.INTEGER,
70
+ allowNull: true,
71
+ validate: {
72
+ min: 1900,
73
+ max: new Date().getFullYear(),
74
+ },
75
+ },
50
76
  primaryPhoneId: {
51
77
  type: vr_migrations_1.DataTypes.UUID,
52
78
  allowNull: true,
@@ -122,25 +148,25 @@ class User extends vr_migrations_1.Model {
122
148
  this.hasMany(models.DevicePaymentPlan, {
123
149
  foreignKey: "userId",
124
150
  as: "paymentPlans",
125
- onDelete: "CASCADE", // ← This is SET NULL, not CASCADE
151
+ onDelete: "CASCADE",
126
152
  onUpdate: "CASCADE",
127
153
  });
128
154
  this.hasMany(models.Payment, {
129
155
  foreignKey: "userId",
130
156
  as: "payments",
131
- onDelete: "SET NULL", // ← This is SET NULL, not CASCADE
157
+ onDelete: "SET NULL",
132
158
  onUpdate: "CASCADE",
133
159
  });
134
160
  this.hasMany(models.Suspension, {
135
161
  foreignKey: "userId",
136
162
  as: "suspensions",
137
- onDelete: "SET NULL", // ← This is SET NULL, not CASCADE
163
+ onDelete: "SET NULL",
138
164
  onUpdate: "CASCADE",
139
165
  });
140
166
  this.hasMany(models.Ban, {
141
167
  foreignKey: "userId",
142
168
  as: "bans",
143
- onDelete: "SET NULL", // ← This is SET NULL, not CASCADE
169
+ onDelete: "SET NULL",
144
170
  onUpdate: "CASCADE",
145
171
  });
146
172
  }
@@ -182,5 +208,26 @@ class User extends vr_migrations_1.Model {
182
208
  });
183
209
  return !!verifiedPhone;
184
210
  }
211
+ // Helper to get full birth date as Date object
212
+ getBirthDate() {
213
+ if (this.birthYear && this.birthMonth && this.birthDate) {
214
+ return new Date(this.birthYear, this.birthMonth - 1, this.birthDate);
215
+ }
216
+ return null;
217
+ }
218
+ // Helper to get age
219
+ getAge() {
220
+ const birthDate = this.getBirthDate();
221
+ if (!birthDate)
222
+ return null;
223
+ const today = new Date();
224
+ let age = today.getFullYear() - birthDate.getFullYear();
225
+ const monthDiff = today.getMonth() - birthDate.getMonth();
226
+ if (monthDiff < 0 ||
227
+ (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
228
+ age--;
229
+ }
230
+ return age;
231
+ }
185
232
  }
186
233
  exports.User = User;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vr-models",
3
- "version": "1.0.55",
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;