vr-models 1.0.15 → 1.0.17

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.
@@ -3,12 +3,11 @@ import type { Product } from "./product.models";
3
3
  import type { DevicePaymentPlan } from "./devicePaymentPlan.models";
4
4
  export type DeviceStatus = "locked" | "unlocked" | "disabled";
5
5
  export type DeviceDedication = "PASSENGER" | "RIDER" | "N/A";
6
- export declare const DEVICE_STATUS: readonly ["locked", "unlocked", "disabled"];
7
- export declare const DEDICATED_USER: readonly ["RIDER", "PASSENGER", "N/A"];
8
6
  export interface DeviceAttributes {
9
7
  id: string;
10
8
  serialNumber: string;
11
9
  productId: string;
10
+ devicePaymentPlanId: string | null;
12
11
  status: DeviceStatus;
13
12
  isPermanentlyUnlocked: boolean;
14
13
  dedicatedUser: DeviceDedication;
@@ -16,17 +15,19 @@ export interface DeviceAttributes {
16
15
  createdAt: Date;
17
16
  updatedAt: Date;
18
17
  product?: Product;
19
- paymentPlans?: DevicePaymentPlan[];
18
+ paymentPlan?: DevicePaymentPlan;
20
19
  }
21
- export interface DeviceCreationAttributes extends Omit<DeviceAttributes, "id" | "createdAt" | "updatedAt" | "status" | "isPermanentlyUnlocked"> {
20
+ export interface DeviceCreationAttributes extends Omit<DeviceAttributes, "id" | "createdAt" | "updatedAt" | "status" | "isPermanentlyUnlocked" | "devicePaymentPlanId"> {
22
21
  id?: string;
23
22
  status?: DeviceStatus;
24
23
  isPermanentlyUnlocked?: boolean;
24
+ devicePaymentPlanId?: string | null;
25
25
  }
26
26
  export declare class Device extends Model<InferAttributes<Device>, InferCreationAttributes<Device>> implements DeviceAttributes {
27
27
  id: CreationOptional<string>;
28
28
  serialNumber: string;
29
29
  productId: string;
30
+ devicePaymentPlanId: CreationOptional<string | null>;
30
31
  status: CreationOptional<DeviceStatus>;
31
32
  isPermanentlyUnlocked: CreationOptional<boolean>;
32
33
  dedicatedUser: DeviceDedication;
@@ -34,7 +35,7 @@ export declare class Device extends Model<InferAttributes<Device>, InferCreation
34
35
  readonly createdAt: CreationOptional<Date>;
35
36
  readonly updatedAt: CreationOptional<Date>;
36
37
  product?: NonAttribute<Product>;
37
- paymentPlans?: NonAttribute<DevicePaymentPlan[]>;
38
+ paymentPlan?: NonAttribute<DevicePaymentPlan>;
38
39
  static initialize(sequelize: any): void;
39
40
  static associate(models: Record<string, ModelStatic<Model>>): void;
40
41
  activate(): Promise<void>;
@@ -44,5 +45,7 @@ export declare class Device extends Model<InferAttributes<Device>, InferCreation
44
45
  makePermanent(): Promise<void>;
45
46
  isActive(): boolean;
46
47
  canBeActivated(): boolean;
48
+ isAssigned(): boolean;
49
+ getPaymentPlan(): Promise<DevicePaymentPlan | null>;
47
50
  }
48
51
  export type DeviceModel = typeof Device;
@@ -1,10 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Device = exports.DEDICATED_USER = exports.DEVICE_STATUS = void 0;
3
+ exports.Device = void 0;
4
4
  // src/models/device.models.ts
5
5
  const vr_migrations_1 = require("vr-migrations");
6
- exports.DEVICE_STATUS = ["locked", "unlocked", "disabled"];
7
- exports.DEDICATED_USER = ["RIDER", "PASSENGER", "N/A"];
8
6
  class Device extends vr_migrations_1.Model {
9
7
  // Static initialization method
10
8
  static initialize(sequelize) {
@@ -23,6 +21,10 @@ class Device extends vr_migrations_1.Model {
23
21
  type: vr_migrations_1.DataTypes.UUID,
24
22
  allowNull: false,
25
23
  },
24
+ devicePaymentPlanId: {
25
+ type: vr_migrations_1.DataTypes.UUID,
26
+ allowNull: true,
27
+ },
26
28
  status: {
27
29
  type: vr_migrations_1.DataTypes.ENUM("locked", "unlocked", "disabled"),
28
30
  allowNull: false,
@@ -66,10 +68,10 @@ class Device extends vr_migrations_1.Model {
66
68
  onDelete: "RESTRICT",
67
69
  onUpdate: "CASCADE",
68
70
  });
69
- this.hasMany(models.DevicePaymentPlan, {
70
- foreignKey: "deviceId",
71
- as: "paymentPlans",
72
- onDelete: "CASCADE",
71
+ this.belongsTo(models.DevicePaymentPlan, {
72
+ foreignKey: "devicePaymentPlanId",
73
+ as: "paymentPlan",
74
+ onDelete: "SET NULL",
73
75
  onUpdate: "CASCADE",
74
76
  });
75
77
  }
@@ -101,5 +103,16 @@ class Device extends vr_migrations_1.Model {
101
103
  canBeActivated() {
102
104
  return this.status !== "disabled";
103
105
  }
106
+ isAssigned() {
107
+ return !!this.devicePaymentPlanId;
108
+ }
109
+ // Helper to get payment plan
110
+ async getPaymentPlan() {
111
+ if (!this.devicePaymentPlanId)
112
+ return null;
113
+ const DevicePaymentPlan = this.constructor.sequelize.models
114
+ .DevicePaymentPlan;
115
+ return await DevicePaymentPlan.findByPk(this.devicePaymentPlanId);
116
+ }
104
117
  }
105
118
  exports.Device = Device;
@@ -1,66 +1,57 @@
1
1
  import { Model, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ModelStatic } from "vr-migrations";
2
2
  import type { Device } from "./device.models";
3
3
  import type { User } from "./user.models";
4
+ import type { Installment } from "./installment.models";
4
5
  export type PaymentPlanStatus = "ACTIVE" | "COMPLETED" | "DEFAULTED" | "CANCELLED";
5
- export type InstallmentFrequency = "DAILY" | "WEEKLY" | "MONTHLY";
6
6
  export interface DevicePaymentPlanAttributes {
7
7
  id: string;
8
- deviceId: string;
9
8
  userId: string;
10
9
  pricingSnapshot: Record<string, any>;
11
- totalAmount: number;
12
- downPayment: number;
13
- installmentAmount: number;
14
- installmentFrequency: InstallmentFrequency;
15
10
  paidAmount: number;
16
11
  outstandingAmount: number;
17
12
  lastPaymentAt: Date | null;
18
13
  nextInstallmentDueAt: Date | null;
19
- gracePeriodDays: number;
20
- autoLockOnMiss: boolean;
21
14
  status: PaymentPlanStatus;
22
15
  completedAt: Date | null;
23
16
  createdAt: Date;
24
17
  updatedAt: Date;
25
- device?: Device;
18
+ devices?: Device[];
26
19
  user?: User;
20
+ installments?: Installment[];
27
21
  }
28
- export interface DevicePaymentPlanCreationAttributes extends Omit<DevicePaymentPlanAttributes, "id" | "createdAt" | "updatedAt" | "paidAmount" | "status" | "gracePeriodDays" | "autoLockOnMiss"> {
22
+ export interface DevicePaymentPlanCreationAttributes extends Omit<DevicePaymentPlanAttributes, "id" | "createdAt" | "updatedAt" | "paidAmount" | "outstandingAmount" | "lastPaymentAt" | "nextInstallmentDueAt" | "status" | "completedAt" | "devices" | "installments"> {
29
23
  id?: string;
30
24
  paidAmount?: number;
25
+ outstandingAmount?: number;
31
26
  status?: PaymentPlanStatus;
32
- gracePeriodDays?: number;
33
- autoLockOnMiss?: boolean;
34
27
  }
35
28
  export declare class DevicePaymentPlan extends Model<InferAttributes<DevicePaymentPlan>, InferCreationAttributes<DevicePaymentPlan>> implements DevicePaymentPlanAttributes {
36
29
  id: CreationOptional<string>;
37
- deviceId: string;
38
30
  userId: string;
39
31
  pricingSnapshot: Record<string, any>;
40
- totalAmount: number;
41
- downPayment: number;
42
- installmentAmount: number;
43
- installmentFrequency: InstallmentFrequency;
44
32
  paidAmount: CreationOptional<number>;
45
33
  outstandingAmount: number;
46
34
  lastPaymentAt: CreationOptional<Date | null>;
47
35
  nextInstallmentDueAt: CreationOptional<Date | null>;
48
- gracePeriodDays: CreationOptional<number>;
49
- autoLockOnMiss: CreationOptional<boolean>;
50
36
  status: CreationOptional<PaymentPlanStatus>;
51
37
  completedAt: CreationOptional<Date | null>;
52
38
  readonly createdAt: CreationOptional<Date>;
53
39
  readonly updatedAt: CreationOptional<Date>;
54
- device?: NonAttribute<Device>;
40
+ devices?: NonAttribute<Device[]>;
55
41
  user?: NonAttribute<User>;
42
+ installments?: NonAttribute<Installment[]>;
56
43
  static initialize(sequelize: any): void;
57
44
  static associate(models: Record<string, ModelStatic<Model>>): void;
58
- makePayment(amount: number): Promise<void>;
45
+ updateProgress(): Promise<void>;
59
46
  markAsDefaulted(): Promise<void>;
60
47
  cancel(): Promise<void>;
61
48
  calculateProgress(): number;
62
- isOverdue(): boolean;
63
- getDaysOverdue(): number;
64
- shouldAutoLock(): boolean;
49
+ getOverdueStatus(): Promise<{
50
+ isOverdue: boolean;
51
+ daysOverdue: number;
52
+ }>;
53
+ shouldAutoLock(): Promise<boolean>;
54
+ getDevices(): Promise<any[]>;
55
+ getInstallments(options?: any): Promise<any[]>;
65
56
  }
66
57
  export type DevicePaymentPlanModel = typeof DevicePaymentPlan;
@@ -12,11 +12,6 @@ class DevicePaymentPlan extends vr_migrations_1.Model {
12
12
  defaultValue: vr_migrations_1.DataTypes.UUIDV4,
13
13
  primaryKey: true,
14
14
  },
15
- deviceId: {
16
- type: vr_migrations_1.DataTypes.UUID,
17
- allowNull: false,
18
- unique: true,
19
- },
20
15
  userId: {
21
16
  type: vr_migrations_1.DataTypes.UUID,
22
17
  allowNull: false,
@@ -25,23 +20,6 @@ class DevicePaymentPlan extends vr_migrations_1.Model {
25
20
  type: vr_migrations_1.DataTypes.JSONB,
26
21
  allowNull: false,
27
22
  },
28
- totalAmount: {
29
- type: vr_migrations_1.DataTypes.FLOAT,
30
- allowNull: false,
31
- },
32
- downPayment: {
33
- type: vr_migrations_1.DataTypes.FLOAT,
34
- allowNull: false,
35
- },
36
- installmentAmount: {
37
- type: vr_migrations_1.DataTypes.FLOAT,
38
- allowNull: false,
39
- },
40
- installmentFrequency: {
41
- type: vr_migrations_1.DataTypes.ENUM("DAILY", "WEEKLY", "MONTHLY"),
42
- allowNull: false,
43
- defaultValue: "WEEKLY",
44
- },
45
23
  paidAmount: {
46
24
  type: vr_migrations_1.DataTypes.FLOAT,
47
25
  allowNull: false,
@@ -59,16 +37,6 @@ class DevicePaymentPlan extends vr_migrations_1.Model {
59
37
  type: vr_migrations_1.DataTypes.DATE,
60
38
  allowNull: true,
61
39
  },
62
- gracePeriodDays: {
63
- type: vr_migrations_1.DataTypes.INTEGER,
64
- allowNull: false,
65
- defaultValue: 2,
66
- },
67
- autoLockOnMiss: {
68
- type: vr_migrations_1.DataTypes.BOOLEAN,
69
- allowNull: false,
70
- defaultValue: true,
71
- },
72
40
  status: {
73
41
  type: vr_migrations_1.DataTypes.ENUM("ACTIVE", "COMPLETED", "DEFAULTED", "CANCELLED"),
74
42
  allowNull: false,
@@ -96,71 +64,112 @@ class DevicePaymentPlan extends vr_migrations_1.Model {
96
64
  }
97
65
  // Static association method
98
66
  static associate(models) {
99
- this.belongsTo(models.Device, {
100
- foreignKey: "deviceId",
101
- as: "device",
102
- onDelete: "CASCADE",
67
+ // One-to-many with Devices
68
+ this.hasMany(models.Device, {
69
+ foreignKey: "devicePaymentPlanId",
70
+ as: "devices",
71
+ onDelete: "SET NULL",
103
72
  onUpdate: "CASCADE",
104
73
  });
74
+ // Belongs to User
105
75
  this.belongsTo(models.User, {
106
76
  foreignKey: "userId",
107
77
  as: "user",
78
+ onDelete: "RESTRICT",
79
+ onUpdate: "CASCADE",
80
+ });
81
+ // One-to-many with Installments
82
+ this.hasMany(models.Installment, {
83
+ foreignKey: "devicePaymentPlanId",
84
+ as: "installments",
108
85
  onDelete: "CASCADE",
109
86
  onUpdate: "CASCADE",
110
87
  });
111
88
  }
112
89
  // Custom instance methods
113
- async makePayment(amount) {
114
- this.paidAmount = (this.paidAmount || 0) + amount;
115
- this.outstandingAmount = Math.max(0, this.outstandingAmount - amount);
116
- this.lastPaymentAt = new Date();
117
- // Update next installment due date
118
- if (this.installmentFrequency === "DAILY") {
119
- this.nextInstallmentDueAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
120
- }
121
- else if (this.installmentFrequency === "WEEKLY") {
122
- this.nextInstallmentDueAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
123
- }
124
- else if (this.installmentFrequency === "MONTHLY") {
125
- this.nextInstallmentDueAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
126
- }
90
+ async updateProgress() {
91
+ // Use Sequelize's built-in methods instead of mixins
92
+ const Installment = this.constructor.sequelize.models.Installment;
93
+ const installments = await Installment.findAll({
94
+ where: {
95
+ devicePaymentPlanId: this.id,
96
+ status: "PAID",
97
+ },
98
+ });
99
+ this.paidAmount = installments.reduce((sum, inst) => sum + inst.amount, 0);
100
+ this.outstandingAmount = this.pricingSnapshot.totalAmount - this.paidAmount;
101
+ // Find next pending installment
102
+ const nextPending = await Installment.findOne({
103
+ where: {
104
+ devicePaymentPlanId: this.id,
105
+ status: "PENDING",
106
+ },
107
+ order: [["dueDate", "ASC"]],
108
+ });
109
+ this.nextInstallmentDueAt = nextPending?.dueDate || null;
127
110
  // Check if completed
128
111
  if (this.outstandingAmount <= 0) {
129
112
  this.status = "COMPLETED";
130
113
  this.completedAt = new Date();
114
+ // Permanently unlock all devices
115
+ const Device = this.constructor.sequelize.models.Device;
116
+ await Device.update({
117
+ isPermanentlyUnlocked: true,
118
+ status: "unlocked",
119
+ }, { where: { devicePaymentPlanId: this.id } });
131
120
  }
132
121
  await this.save();
133
122
  }
134
123
  async markAsDefaulted() {
135
124
  this.status = "DEFAULTED";
136
125
  await this.save();
126
+ // Lock all devices
127
+ const Device = this.constructor.sequelize.models.Device;
128
+ await Device.update({ status: "locked" }, { where: { devicePaymentPlanId: this.id } });
137
129
  }
138
130
  async cancel() {
139
131
  this.status = "CANCELLED";
140
132
  await this.save();
141
133
  }
142
134
  calculateProgress() {
143
- return (this.paidAmount / this.totalAmount) * 100;
135
+ return (this.paidAmount / this.pricingSnapshot.totalAmount) * 100;
144
136
  }
145
- isOverdue() {
146
- if (!this.nextInstallmentDueAt || this.status !== "ACTIVE") {
147
- return false;
137
+ async getOverdueStatus() {
138
+ if (this.status !== "ACTIVE" || !this.nextInstallmentDueAt) {
139
+ return { isOverdue: false, daysOverdue: 0 };
148
140
  }
141
+ const graceDays = this.pricingSnapshot.gracePeriodDays || 2;
149
142
  const overdueDate = new Date(this.nextInstallmentDueAt);
150
- overdueDate.setDate(overdueDate.getDate() + this.gracePeriodDays);
151
- return new Date() > overdueDate;
152
- }
153
- getDaysOverdue() {
154
- if (!this.isOverdue() || !this.nextInstallmentDueAt) {
155
- return 0;
143
+ overdueDate.setDate(overdueDate.getDate() + graceDays);
144
+ const now = new Date();
145
+ if (now > overdueDate) {
146
+ const diffTime = Math.abs(now.getTime() - overdueDate.getTime());
147
+ const daysOverdue = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
148
+ return { isOverdue: true, daysOverdue };
156
149
  }
157
- const overdueDate = new Date(this.nextInstallmentDueAt);
158
- overdueDate.setDate(overdueDate.getDate() + this.gracePeriodDays);
159
- const diffTime = Math.abs(new Date().getTime() - overdueDate.getTime());
160
- return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
150
+ return { isOverdue: false, daysOverdue: 0 };
151
+ }
152
+ async shouldAutoLock() {
153
+ if (!this.pricingSnapshot.autoLockOnMiss)
154
+ return false;
155
+ const { isOverdue, daysOverdue } = await this.getOverdueStatus();
156
+ const graceDays = this.pricingSnapshot.gracePeriodDays || 2;
157
+ return isOverdue && daysOverdue > graceDays;
158
+ }
159
+ // Helper to get devices (using Sequelize directly)
160
+ async getDevices() {
161
+ const Device = this.constructor.sequelize.models.Device;
162
+ return await Device.findAll({
163
+ where: { devicePaymentPlanId: this.id },
164
+ });
161
165
  }
162
- shouldAutoLock() {
163
- return this.autoLockOnMiss && this.isOverdue();
166
+ // Helper to get installments
167
+ async getInstallments(options) {
168
+ const Installment = this.constructor.sequelize.models.Installment;
169
+ return await Installment.findAll({
170
+ where: { devicePaymentPlanId: this.id },
171
+ ...options,
172
+ });
164
173
  }
165
174
  }
166
175
  exports.DevicePaymentPlan = DevicePaymentPlan;
@@ -1,6 +1,6 @@
1
1
  import { Model, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ModelStatic } from "vr-migrations";
2
2
  import type { User } from "./user.models";
3
- export declare const EVENT_ACTIONS: readonly ["USER_READ", "USER_CREATED", "USER_UPDATED", "USER_DELETED", "USER_SUSPENDED", "USER_BANNED", "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", "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", "PRODUCTS_LISTED", "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_DEFAULT_MARKED", "PAYMENT_CREATED", "PAYMENT_READ", "PAYMENTS_LISTED", "TRANSACTION_READ", "TRANSACTIONS_LISTED", "EVENT_LOG_READ", "EVENT_LOGS_LISTED", "SECURITY_CLEARANCE_MANAGED", "DEVICE_LOCK_OVERRIDDEN"];
3
+ export declare const EVENT_ACTIONS: readonly ["USER_READ", "USER_CREATED", "USER_UPDATED", "USER_DELETED", "USER_SUSPENDED", "USER_BANNED", "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", "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", "PRODUCTS_LISTED", "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", "PAYMENT_RECORDED", "PAYMENT_READ", "PAYMENTS_LISTED", "TRANSACTION_READ", "TRANSACTIONS_LISTED", "EVENT_LOG_READ", "EVENT_LOGS_LISTED", "SECURITY_CLEARANCE_MANAGED", "DEVICE_LOCK_OVERRIDDEN"];
4
4
  export type EventAction = (typeof EVENT_ACTIONS)[number];
5
5
  export type EventActorType = "USER" | "SYSTEM" | "ADMIN";
6
6
  export interface EventLogAttributes {
@@ -54,8 +54,9 @@ exports.EVENT_ACTIONS = [
54
54
  "DEVICE_PAYMENT_PLAN_CREATED",
55
55
  "DEVICE_PAYMENT_PLAN_READ",
56
56
  "DEVICE_PAYMENT_PLAN_UPDATED",
57
- "DEVICE_PAYMENT_DEFAULT_MARKED",
58
- "PAYMENT_CREATED",
57
+ "DEVICE_PAYMENT_PLAN_MARKED_DEFAULTED",
58
+ "DEVICE_PAYMENT_PLAN_SOFT_DELETED",
59
+ "PAYMENT_RECORDED",
59
60
  "PAYMENT_READ",
60
61
  "PAYMENTS_LISTED",
61
62
  "TRANSACTION_READ",
@@ -8,4 +8,5 @@ export * from "./pricing.models";
8
8
  export * from "./product.models";
9
9
  export * from "./securityClearance.models";
10
10
  export * from "./transaction.models";
11
- export type { UserModel, TransactionModel, SecurityClearanceModel, ProductModel, PricingModel, PaymentModel, IdempotencyRecordModel, EventLogModel, DevicePaymentPlanModel, DeviceModel, } from "./types";
11
+ export * from "./installment.models";
12
+ export type { UserModel, TransactionModel, SecurityClearanceModel, ProductModel, PricingModel, PaymentModel, IdempotencyRecordModel, EventLogModel, DevicePaymentPlanModel, DeviceModel, InstallmentModel, } from "./types";
@@ -24,3 +24,4 @@ __exportStar(require("./pricing.models"), exports);
24
24
  __exportStar(require("./product.models"), exports);
25
25
  __exportStar(require("./securityClearance.models"), exports);
26
26
  __exportStar(require("./transaction.models"), exports);
27
+ __exportStar(require("./installment.models"), exports);
@@ -0,0 +1,59 @@
1
+ import { Model, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ModelStatic } from "vr-migrations";
2
+ import type { DevicePaymentPlan } from "./devicePaymentPlan.models";
3
+ import type { Payment } from "./payment.models";
4
+ export type InstallmentStatus = "PENDING" | "PAID" | "OVERDUE" | "SKIPPED";
5
+ export interface InstallmentAttributes {
6
+ id: string;
7
+ devicePaymentPlanId: string;
8
+ installmentNumber: number;
9
+ amount: number;
10
+ dueDate: Date;
11
+ paidAt: Date | null;
12
+ paidAmount: number | null;
13
+ status: InstallmentStatus;
14
+ paymentId: string | null;
15
+ isAutoGenerated: boolean;
16
+ metadata: Record<string, any>;
17
+ createdAt: Date;
18
+ updatedAt: Date;
19
+ paymentPlan?: DevicePaymentPlan;
20
+ payment?: Payment;
21
+ }
22
+ export interface InstallmentCreationAttributes extends Omit<InstallmentAttributes, "id" | "createdAt" | "updatedAt" | "paidAt" | "paidAmount" | "status" | "paymentId" | "metadata"> {
23
+ id?: string;
24
+ paidAt?: Date | null;
25
+ paidAmount?: number | null;
26
+ status?: InstallmentStatus;
27
+ paymentId?: string | null;
28
+ metadata?: Record<string, any>;
29
+ }
30
+ export declare class Installment extends Model<InferAttributes<Installment>, InferCreationAttributes<Installment>> implements InstallmentAttributes {
31
+ id: CreationOptional<string>;
32
+ devicePaymentPlanId: string;
33
+ installmentNumber: number;
34
+ amount: number;
35
+ dueDate: Date;
36
+ paidAt: CreationOptional<Date | null>;
37
+ paidAmount: CreationOptional<number | null>;
38
+ status: CreationOptional<InstallmentStatus>;
39
+ paymentId: CreationOptional<string | null>;
40
+ isAutoGenerated: boolean;
41
+ metadata: CreationOptional<Record<string, any>>;
42
+ readonly createdAt: CreationOptional<Date>;
43
+ readonly updatedAt: CreationOptional<Date>;
44
+ paymentPlan?: NonAttribute<DevicePaymentPlan>;
45
+ payment?: NonAttribute<Payment>;
46
+ static initialize(sequelize: any): void;
47
+ static associate(models: Record<string, ModelStatic<Model>>): void;
48
+ markAsPaid(paymentId: string, paidAmount?: number): Promise<void>;
49
+ markAsOverdue(): Promise<void>;
50
+ skip(): Promise<void>;
51
+ isPaid(): boolean;
52
+ isOverdue(): boolean;
53
+ isPending(): boolean;
54
+ daysUntilDue(): number;
55
+ daysOverdue(): number;
56
+ static getOverdueInstallments(): Promise<Installment[]>;
57
+ static getPendingForPlan(planId: string): Promise<Installment[]>;
58
+ }
59
+ export type InstallmentModel = typeof Installment;
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Installment = void 0;
4
+ // src/models/installment.models.ts
5
+ const vr_migrations_1 = require("vr-migrations");
6
+ class Installment extends vr_migrations_1.Model {
7
+ // Static initialization method
8
+ static initialize(sequelize) {
9
+ this.init({
10
+ id: {
11
+ type: vr_migrations_1.DataTypes.UUID,
12
+ defaultValue: vr_migrations_1.DataTypes.UUIDV4,
13
+ primaryKey: true,
14
+ },
15
+ devicePaymentPlanId: {
16
+ type: vr_migrations_1.DataTypes.UUID,
17
+ allowNull: false,
18
+ },
19
+ installmentNumber: {
20
+ type: vr_migrations_1.DataTypes.INTEGER,
21
+ allowNull: false,
22
+ },
23
+ amount: {
24
+ type: vr_migrations_1.DataTypes.DECIMAL(10, 2),
25
+ allowNull: false,
26
+ validate: { min: 0 },
27
+ },
28
+ dueDate: {
29
+ type: vr_migrations_1.DataTypes.DATE,
30
+ allowNull: false,
31
+ },
32
+ paidAt: {
33
+ type: vr_migrations_1.DataTypes.DATE,
34
+ allowNull: true,
35
+ },
36
+ paidAmount: {
37
+ type: vr_migrations_1.DataTypes.DECIMAL(10, 2),
38
+ allowNull: true,
39
+ validate: { min: 0 },
40
+ },
41
+ status: {
42
+ type: vr_migrations_1.DataTypes.ENUM("PENDING", "PAID", "OVERDUE", "SKIPPED"),
43
+ allowNull: false,
44
+ defaultValue: "PENDING",
45
+ },
46
+ paymentId: {
47
+ type: vr_migrations_1.DataTypes.UUID,
48
+ allowNull: true,
49
+ },
50
+ isAutoGenerated: {
51
+ type: vr_migrations_1.DataTypes.BOOLEAN,
52
+ allowNull: false,
53
+ defaultValue: true,
54
+ },
55
+ metadata: {
56
+ type: vr_migrations_1.DataTypes.JSONB,
57
+ allowNull: false,
58
+ defaultValue: {},
59
+ },
60
+ createdAt: {
61
+ type: vr_migrations_1.DataTypes.DATE,
62
+ defaultValue: vr_migrations_1.DataTypes.NOW,
63
+ },
64
+ updatedAt: {
65
+ type: vr_migrations_1.DataTypes.DATE,
66
+ defaultValue: vr_migrations_1.DataTypes.NOW,
67
+ },
68
+ }, {
69
+ sequelize,
70
+ modelName: "Installment",
71
+ tableName: "installments",
72
+ timestamps: true,
73
+ freezeTableName: true,
74
+ indexes: [
75
+ {
76
+ unique: true,
77
+ fields: ["devicePaymentPlanId", "installmentNumber"],
78
+ name: "unique_installment_number_per_plan",
79
+ },
80
+ {
81
+ fields: ["status"],
82
+ name: "installment_status_idx",
83
+ },
84
+ {
85
+ fields: ["dueDate"],
86
+ name: "installment_due_date_idx",
87
+ },
88
+ ],
89
+ validate: {
90
+ paidAmountMatchesAmount() {
91
+ if (this.status === "PAID" && this.paidAmount !== this.amount) {
92
+ throw new Error("Paid amount must equal installment amount when status is PAID");
93
+ }
94
+ },
95
+ },
96
+ });
97
+ }
98
+ // Static association method
99
+ static associate(models) {
100
+ this.belongsTo(models.DevicePaymentPlan, {
101
+ foreignKey: "devicePaymentPlanId",
102
+ as: "paymentPlan",
103
+ onDelete: "CASCADE",
104
+ onUpdate: "CASCADE",
105
+ });
106
+ this.belongsTo(models.Payment, {
107
+ foreignKey: "paymentId",
108
+ as: "payment",
109
+ onDelete: "SET NULL",
110
+ onUpdate: "CASCADE",
111
+ });
112
+ }
113
+ // Custom instance methods
114
+ async markAsPaid(paymentId, paidAmount) {
115
+ this.status = "PAID";
116
+ this.paidAt = new Date();
117
+ this.paidAmount = paidAmount || this.amount;
118
+ this.paymentId = paymentId;
119
+ await this.save();
120
+ // Update the parent plan's progress
121
+ const DevicePaymentPlan = this.constructor.sequelize.models
122
+ .DevicePaymentPlan;
123
+ const plan = await DevicePaymentPlan.findByPk(this.devicePaymentPlanId);
124
+ if (plan) {
125
+ await plan.updateProgress();
126
+ }
127
+ }
128
+ async markAsOverdue() {
129
+ if (this.status === "PENDING" && new Date() > this.dueDate) {
130
+ this.status = "OVERDUE";
131
+ await this.save();
132
+ }
133
+ }
134
+ async skip() {
135
+ this.status = "SKIPPED";
136
+ await this.save();
137
+ }
138
+ isPaid() {
139
+ return this.status === "PAID";
140
+ }
141
+ isOverdue() {
142
+ return this.status === "OVERDUE";
143
+ }
144
+ isPending() {
145
+ return this.status === "PENDING";
146
+ }
147
+ daysUntilDue() {
148
+ const today = new Date();
149
+ const diffTime = this.dueDate.getTime() - today.getTime();
150
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
151
+ }
152
+ daysOverdue() {
153
+ if (!this.isOverdue() &&
154
+ !(this.status === "PENDING" && new Date() > this.dueDate)) {
155
+ return 0;
156
+ }
157
+ const today = new Date();
158
+ const diffTime = today.getTime() - this.dueDate.getTime();
159
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
160
+ }
161
+ // Static methods
162
+ static async getOverdueInstallments() {
163
+ return await this.findAll({
164
+ where: {
165
+ status: "PENDING",
166
+ dueDate: { [vr_migrations_1.Op.lt]: new Date() },
167
+ },
168
+ include: ["paymentPlan"],
169
+ });
170
+ }
171
+ static async getPendingForPlan(planId) {
172
+ return await this.findAll({
173
+ where: {
174
+ devicePaymentPlanId: planId,
175
+ status: "PENDING",
176
+ },
177
+ order: [["dueDate", "ASC"]],
178
+ });
179
+ }
180
+ }
181
+ exports.Installment = Installment;
@@ -1,33 +1,43 @@
1
1
  import { Model, InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, ModelStatic } from "vr-migrations";
2
2
  import type { Product } from "./product.models";
3
+ export type PricingType = "HIRE_PURCHASE" | "FULL_PAYMENT";
4
+ export type InstallmentFrequency = "DAILY" | "WEEKLY" | "MONTHLY";
3
5
  export interface PricingAttributes {
4
6
  id: string;
5
7
  productId: string;
6
8
  name: string;
7
- upfrontPrice: number;
9
+ type: PricingType;
10
+ totalAmount: number;
8
11
  downPayment: number;
9
12
  installmentAmount: number | null;
10
- installmentIntervalDays: number | null;
11
- totalAmount: number;
13
+ installmentFrequency: InstallmentFrequency | null;
14
+ installmentCount: number | null;
15
+ gracePeriodDays: number;
16
+ autoLockOnMiss: boolean;
12
17
  isActive: boolean;
13
18
  createdAt: Date;
14
19
  updatedAt: Date;
15
20
  product?: Product;
16
21
  }
17
- export interface PricingCreationAttributes extends Omit<PricingAttributes, "id" | "createdAt" | "updatedAt" | "isActive" | "downPayment"> {
22
+ export interface PricingCreationAttributes extends Omit<PricingAttributes, "id" | "createdAt" | "updatedAt" | "isActive" | "downPayment" | "gracePeriodDays" | "autoLockOnMiss"> {
18
23
  id?: string;
19
24
  isActive?: boolean;
20
25
  downPayment?: number;
26
+ gracePeriodDays?: number;
27
+ autoLockOnMiss?: boolean;
21
28
  }
22
29
  export declare class Pricing extends Model<InferAttributes<Pricing>, InferCreationAttributes<Pricing>> implements PricingAttributes {
23
30
  id: CreationOptional<string>;
24
31
  productId: string;
25
32
  name: string;
26
- upfrontPrice: number;
33
+ type: PricingType;
34
+ totalAmount: number;
27
35
  downPayment: CreationOptional<number>;
28
36
  installmentAmount: CreationOptional<number | null>;
29
- installmentIntervalDays: CreationOptional<number | null>;
30
- totalAmount: number;
37
+ installmentFrequency: CreationOptional<InstallmentFrequency | null>;
38
+ installmentCount: CreationOptional<number | null>;
39
+ gracePeriodDays: CreationOptional<number>;
40
+ autoLockOnMiss: CreationOptional<boolean>;
31
41
  isActive: CreationOptional<boolean>;
32
42
  readonly createdAt: CreationOptional<Date>;
33
43
  readonly updatedAt: CreationOptional<Date>;
@@ -39,5 +49,6 @@ export declare class Pricing extends Model<InferAttributes<Pricing>, InferCreati
39
49
  isInstallmentPlan(): boolean;
40
50
  getInstallmentCount(): number | null;
41
51
  calculateRemainingAmount(paidAmount: number): number;
52
+ calculateNextDueDate(fromDate?: Date): Date | null;
42
53
  }
43
54
  export type PricingModel = typeof Pricing;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Pricing = void 0;
4
+ // src/models/pricing.models.ts
4
5
  const vr_migrations_1 = require("vr-migrations");
5
6
  class Pricing extends vr_migrations_1.Model {
6
7
  // Static initialization method
@@ -13,15 +14,42 @@ class Pricing extends vr_migrations_1.Model {
13
14
  },
14
15
  productId: { type: vr_migrations_1.DataTypes.UUID, allowNull: false },
15
16
  name: { type: vr_migrations_1.DataTypes.STRING(100), allowNull: false },
16
- upfrontPrice: { type: vr_migrations_1.DataTypes.DECIMAL(10, 2), allowNull: false },
17
+ type: {
18
+ type: vr_migrations_1.DataTypes.ENUM("HIRE_PURCHASE", "FULL_PAYMENT"),
19
+ allowNull: false,
20
+ },
21
+ totalAmount: {
22
+ type: vr_migrations_1.DataTypes.DECIMAL(10, 2),
23
+ allowNull: false,
24
+ field: "total_amount", // Maps to database column
25
+ },
17
26
  downPayment: {
18
27
  type: vr_migrations_1.DataTypes.DECIMAL(10, 2),
19
28
  allowNull: false,
20
29
  defaultValue: 0,
21
30
  },
22
- installmentAmount: { type: vr_migrations_1.DataTypes.DECIMAL(10, 2), allowNull: true },
23
- installmentIntervalDays: { type: vr_migrations_1.DataTypes.INTEGER, allowNull: true },
24
- totalAmount: { type: vr_migrations_1.DataTypes.DECIMAL(10, 2), allowNull: false },
31
+ installmentAmount: {
32
+ type: vr_migrations_1.DataTypes.DECIMAL(10, 2),
33
+ allowNull: true,
34
+ },
35
+ installmentFrequency: {
36
+ type: vr_migrations_1.DataTypes.ENUM("DAILY", "WEEKLY", "MONTHLY"),
37
+ allowNull: true,
38
+ },
39
+ installmentCount: {
40
+ type: vr_migrations_1.DataTypes.INTEGER,
41
+ allowNull: true,
42
+ },
43
+ gracePeriodDays: {
44
+ type: vr_migrations_1.DataTypes.INTEGER,
45
+ allowNull: false,
46
+ defaultValue: 2,
47
+ },
48
+ autoLockOnMiss: {
49
+ type: vr_migrations_1.DataTypes.BOOLEAN,
50
+ allowNull: false,
51
+ defaultValue: true,
52
+ },
25
53
  isActive: {
26
54
  type: vr_migrations_1.DataTypes.BOOLEAN,
27
55
  allowNull: false,
@@ -55,16 +83,33 @@ class Pricing extends vr_migrations_1.Model {
55
83
  await this.save();
56
84
  }
57
85
  isInstallmentPlan() {
58
- return !!(this.installmentAmount && this.installmentIntervalDays);
86
+ return this.type === "HIRE_PURCHASE";
59
87
  }
60
88
  getInstallmentCount() {
61
89
  if (!this.isInstallmentPlan())
62
90
  return null;
63
- const remainingAmount = this.totalAmount - this.downPayment;
64
- return Math.ceil(remainingAmount / (this.installmentAmount || 1));
91
+ return this.installmentCount;
65
92
  }
66
93
  calculateRemainingAmount(paidAmount) {
67
94
  return Math.max(0, this.totalAmount - paidAmount);
68
95
  }
96
+ // Helper to calculate next due date based on frequency
97
+ calculateNextDueDate(fromDate = new Date()) {
98
+ if (!this.installmentFrequency)
99
+ return null;
100
+ const nextDate = new Date(fromDate);
101
+ switch (this.installmentFrequency) {
102
+ case "DAILY":
103
+ nextDate.setDate(nextDate.getDate() + 1);
104
+ break;
105
+ case "WEEKLY":
106
+ nextDate.setDate(nextDate.getDate() + 7);
107
+ break;
108
+ case "MONTHLY":
109
+ nextDate.setMonth(nextDate.getMonth() + 1);
110
+ break;
111
+ }
112
+ return nextDate;
113
+ }
69
114
  }
70
115
  exports.Pricing = Pricing;
@@ -8,3 +8,4 @@ export type { IdempotencyRecordModel } from "./idempotencyRecord.models";
8
8
  export type { EventLogModel } from "./eventLog.models";
9
9
  export type { DevicePaymentPlanModel } from "./devicePaymentPlan.models";
10
10
  export type { DeviceModel } from "./device.models";
11
+ export type { InstallmentModel } from "./installment.models";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vr-models",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Shared database models package for VR applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",