tt-entities 0.0.55 → 0.1.0
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.
- package/dist/libs/tatayab-entities-library/entities/order-edit-item.entity.d.ts +24 -0
- package/dist/libs/tatayab-entities-library/entities/order-edit-item.entity.js +98 -0
- package/dist/libs/tatayab-entities-library/entities/order-edit-item.entity.js.map +1 -0
- package/dist/libs/tatayab-entities-library/entities/order-edit-session.entity.d.ts +10 -0
- package/dist/libs/tatayab-entities-library/entities/order-edit-session.entity.js +43 -0
- package/dist/libs/tatayab-entities-library/entities/order-edit-session.entity.js.map +1 -0
- package/dist/libs/tatayab-entities-library/entities/order-item-change-log.entity.d.ts +22 -0
- package/dist/libs/tatayab-entities-library/entities/order-item-change-log.entity.js +77 -0
- package/dist/libs/tatayab-entities-library/entities/order-item-change-log.entity.js.map +1 -0
- package/dist/libs/tatayab-entities-library/entities/order-item.entity.d.ts +1 -0
- package/dist/libs/tatayab-entities-library/entities/order-item.entity.js +4 -0
- package/dist/libs/tatayab-entities-library/entities/order-item.entity.js.map +1 -1
- package/dist/libs/tatayab-entities-library/entities/order-payment.entity.d.ts +28 -0
- package/dist/libs/tatayab-entities-library/entities/order-payment.entity.js +95 -0
- package/dist/libs/tatayab-entities-library/entities/order-payment.entity.js.map +1 -0
- package/dist/libs/tatayab-entities-library/entities/order.entity.d.ts +7 -0
- package/dist/libs/tatayab-entities-library/entities/order.entity.js +19 -0
- package/dist/libs/tatayab-entities-library/entities/order.entity.js.map +1 -1
- package/dist/libs/tatayab-entities-library/entities/refund.entity.d.ts +8 -0
- package/dist/libs/tatayab-entities-library/entities/refund.entity.js +24 -1
- package/dist/libs/tatayab-entities-library/entities/refund.entity.js.map +1 -1
- package/libs/tatayab-entities-library/src/entities/order-edit-item.entity.ts +101 -0
- package/libs/tatayab-entities-library/src/entities/order-edit-session.entity.ts +47 -0
- package/libs/tatayab-entities-library/src/entities/order-item-change-log.entity.ts +84 -0
- package/libs/tatayab-entities-library/src/entities/order-item.entity.ts +5 -0
- package/libs/tatayab-entities-library/src/entities/order-payment.entity.ts +107 -0
- package/libs/tatayab-entities-library/src/entities/order.entity.ts +15 -0
- package/libs/tatayab-entities-library/src/entities/refund.entity.ts +25 -0
- package/package.json +1 -1
- package/src/app.module.ts +0 -2
- package/src/database/config.js +4 -4
- package/src/database/migrations/20260615103000-add-edit-order-feature.js +283 -0
- package/libs/tatayab-entities-library/src/entities.zip +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refund.entity.js","sourceRoot":"","sources":["../../../../libs/tatayab-entities-library/src/entities/refund.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,+DAO8B;AAC9B,iDAAuC;AACvC,+CAAqC;AACrC,uDAA4C;
|
|
1
|
+
{"version":3,"file":"refund.entity.js","sourceRoot":"","sources":["../../../../libs/tatayab-entities-library/src/entities/refund.entity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Table,
|
|
3
|
+
Column,
|
|
4
|
+
Model,
|
|
5
|
+
ForeignKey,
|
|
6
|
+
BelongsTo,
|
|
7
|
+
DataType,
|
|
8
|
+
} from 'sequelize-typescript';
|
|
9
|
+
import { OrderEditSession } from './order-edit-session.entity';
|
|
10
|
+
import { OrderItem, OrderItemType } from './order-item.entity';
|
|
11
|
+
import { Product } from './product.entity';
|
|
12
|
+
import { ProductVariant } from './product-variant.entity';
|
|
13
|
+
import { Bundle } from './bundle.entity';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* OrderEditItem — Edit Order feature.
|
|
17
|
+
*
|
|
18
|
+
* Working-copy basket line for an OrderEditSession. Seeded 1:1 from the
|
|
19
|
+
* order's current OrderItems when a session is opened (sourceOrderItemId
|
|
20
|
+
* set), then mutated freely (add/remove/change quantity) without touching
|
|
21
|
+
* the live OrderItems. On confirm, diffed against OrderItems and applied —
|
|
22
|
+
* see OrderEditController::replaceOrderItems / reconcileStock in
|
|
23
|
+
* tatayab-dashboard.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors OrderItem's shape closely (itemType/productId/productVariantId/
|
|
26
|
+
* bundleId/composedSku/quantity/unitPrice/lineTotal) so the diff is a
|
|
27
|
+
* straightforward field-by-field comparison. bundleSelectionsJson mirrors
|
|
28
|
+
* OrderItemBundleSelection rows for itemType=bundle lines.
|
|
29
|
+
*/
|
|
30
|
+
@Table
|
|
31
|
+
export class OrderEditItem extends Model {
|
|
32
|
+
@ForeignKey(() => OrderEditSession)
|
|
33
|
+
@Column({ allowNull: false })
|
|
34
|
+
declare sessionId: number;
|
|
35
|
+
|
|
36
|
+
@Column({
|
|
37
|
+
type: DataType.ENUM(...Object.values(OrderItemType)),
|
|
38
|
+
allowNull: false,
|
|
39
|
+
defaultValue: OrderItemType.PRODUCT,
|
|
40
|
+
})
|
|
41
|
+
declare itemType: OrderItemType;
|
|
42
|
+
|
|
43
|
+
// ── Product fields ────────────────────────────────────────────────────────────
|
|
44
|
+
@ForeignKey(() => Product)
|
|
45
|
+
@Column({ allowNull: true })
|
|
46
|
+
declare productId?: number;
|
|
47
|
+
|
|
48
|
+
@ForeignKey(() => ProductVariant)
|
|
49
|
+
@Column({ allowNull: true })
|
|
50
|
+
declare productVariantId?: number;
|
|
51
|
+
|
|
52
|
+
// ── Bundle fields ─────────────────────────────────────────────────────────────
|
|
53
|
+
@ForeignKey(() => Bundle)
|
|
54
|
+
@Column({ allowNull: true })
|
|
55
|
+
declare bundleId?: number;
|
|
56
|
+
|
|
57
|
+
@Column({ allowNull: true })
|
|
58
|
+
declare composedSku?: string;
|
|
59
|
+
|
|
60
|
+
// ── Pricing ───────────────────────────────────────────────────────────────────
|
|
61
|
+
@Column({ allowNull: false, defaultValue: 1 })
|
|
62
|
+
declare quantity: number;
|
|
63
|
+
|
|
64
|
+
@Column({ type: DataType.DECIMAL(10, 3), allowNull: false })
|
|
65
|
+
declare unitPrice: number;
|
|
66
|
+
|
|
67
|
+
@Column({ type: DataType.DECIMAL(10, 3), allowNull: false })
|
|
68
|
+
declare lineTotal: number;
|
|
69
|
+
|
|
70
|
+
// ── Diff anchor ───────────────────────────────────────────────────────────────
|
|
71
|
+
// Set when this line corresponds to an existing OrderItem (possibly with a
|
|
72
|
+
// changed quantity). Null if this line was newly added during this edit.
|
|
73
|
+
// On confirm: present -> update that OrderItem in place (preserving id);
|
|
74
|
+
// absent -> create a new OrderItem.
|
|
75
|
+
@ForeignKey(() => OrderItem)
|
|
76
|
+
@Column({ allowNull: true })
|
|
77
|
+
declare sourceOrderItemId?: number;
|
|
78
|
+
|
|
79
|
+
// Array of {productId, productVariantId, productNameEn, productNameAr,
|
|
80
|
+
// variantLabelEn, variantLabelAr, sortOrder} — same shape as
|
|
81
|
+
// OrderItemBundleSelection rows. Only set for itemType=bundle.
|
|
82
|
+
@Column({ type: DataType.JSON, allowNull: true })
|
|
83
|
+
declare bundleSelectionsJson?: object;
|
|
84
|
+
|
|
85
|
+
// ── Associations ──────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
@BelongsTo(() => OrderEditSession)
|
|
88
|
+
declare session: OrderEditSession;
|
|
89
|
+
|
|
90
|
+
@BelongsTo(() => OrderItem, { foreignKey: 'sourceOrderItemId' })
|
|
91
|
+
declare sourceOrderItem?: OrderItem;
|
|
92
|
+
|
|
93
|
+
@BelongsTo(() => Product)
|
|
94
|
+
declare product?: Product;
|
|
95
|
+
|
|
96
|
+
@BelongsTo(() => ProductVariant)
|
|
97
|
+
declare variant?: ProductVariant;
|
|
98
|
+
|
|
99
|
+
@BelongsTo(() => Bundle)
|
|
100
|
+
declare bundle?: Bundle;
|
|
101
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Table,
|
|
3
|
+
Column,
|
|
4
|
+
Model,
|
|
5
|
+
ForeignKey,
|
|
6
|
+
BelongsTo,
|
|
7
|
+
HasMany,
|
|
8
|
+
DataType,
|
|
9
|
+
} from 'sequelize-typescript';
|
|
10
|
+
import { Order } from './order.entity';
|
|
11
|
+
import { OrderEditItem } from './order-edit-item.entity';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* OrderEditSession — Edit Order feature.
|
|
15
|
+
*
|
|
16
|
+
* An in-progress edit lock on an Order. One active (non-expired) session
|
|
17
|
+
* per order — a second admin opening "Edit Order" while one exists sees a
|
|
18
|
+
* "currently being edited by X" warning with a takeover option. Cancel =
|
|
19
|
+
* delete this row (cascades to its OrderEditItems) — zero impact on the
|
|
20
|
+
* live order. Confirm = applies the edit in a transaction (see
|
|
21
|
+
* OrderEditController::confirm in tatayab-dashboard) then deletes this row.
|
|
22
|
+
*
|
|
23
|
+
* expiresAt = createdAt + 2 hours (SESSION_LIFETIME_MINUTES in
|
|
24
|
+
* OrderEditController). Expired sessions are treated as gone and silently
|
|
25
|
+
* replaced on the next "Edit Order" click.
|
|
26
|
+
*/
|
|
27
|
+
@Table
|
|
28
|
+
export class OrderEditSession extends Model {
|
|
29
|
+
@ForeignKey(() => Order)
|
|
30
|
+
@Column({ allowNull: false })
|
|
31
|
+
declare orderId: number;
|
|
32
|
+
|
|
33
|
+
// e.g. "admin:42" — the admin who opened this session.
|
|
34
|
+
@Column({ allowNull: false })
|
|
35
|
+
declare createdBy: string;
|
|
36
|
+
|
|
37
|
+
@Column({ type: DataType.DATE, allowNull: false })
|
|
38
|
+
declare expiresAt: Date;
|
|
39
|
+
|
|
40
|
+
// ── Associations ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
@BelongsTo(() => Order)
|
|
43
|
+
declare order: Order;
|
|
44
|
+
|
|
45
|
+
@HasMany(() => OrderEditItem)
|
|
46
|
+
declare items: OrderEditItem[];
|
|
47
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Table,
|
|
3
|
+
Column,
|
|
4
|
+
Model,
|
|
5
|
+
ForeignKey,
|
|
6
|
+
BelongsTo,
|
|
7
|
+
HasOne,
|
|
8
|
+
DataType,
|
|
9
|
+
} from 'sequelize-typescript';
|
|
10
|
+
import { Order } from './order.entity';
|
|
11
|
+
import { Refund } from './refund.entity';
|
|
12
|
+
import { OrderPayment } from './order-payment.entity';
|
|
13
|
+
|
|
14
|
+
export enum OrderEditScenario {
|
|
15
|
+
SAME = 'same',
|
|
16
|
+
REFUND = 'refund',
|
|
17
|
+
ADDITIONAL_PAYMENT = 'additional_payment',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* OrderItemChangeLog — Edit Order feature, rule 2 (full historical
|
|
22
|
+
* basket-change log).
|
|
23
|
+
*
|
|
24
|
+
* One row PER CONFIRMED EDIT (not per item) — editNumber is sequential per
|
|
25
|
+
* order (1, 2, 3...). beforeItemsJson/afterItemsJson are full snapshots of
|
|
26
|
+
* the basket immediately before/after this edit, so the "Edit History" tab
|
|
27
|
+
* on Order Detail never needs to re-resolve product names for old entries
|
|
28
|
+
* even if a product is later renamed or deleted.
|
|
29
|
+
*
|
|
30
|
+
* priceDelta = newTotal - oldTotal (positive = customer owes more, negative
|
|
31
|
+
* = refund issued, ~zero = no payment change). scenario determines which of
|
|
32
|
+
* refund/additionalPayment (below) is populated, if either.
|
|
33
|
+
*/
|
|
34
|
+
@Table
|
|
35
|
+
export class OrderItemChangeLog extends Model {
|
|
36
|
+
@ForeignKey(() => Order)
|
|
37
|
+
@Column({ allowNull: false })
|
|
38
|
+
declare orderId: number;
|
|
39
|
+
|
|
40
|
+
// Sequential per order: 1, 2, 3... — the Nth confirmed edit on this order.
|
|
41
|
+
@Column({ allowNull: false })
|
|
42
|
+
declare editNumber: number;
|
|
43
|
+
|
|
44
|
+
// Snapshot of the basket immediately BEFORE this edit was applied.
|
|
45
|
+
@Column({ type: DataType.JSON, allowNull: false })
|
|
46
|
+
declare beforeItemsJson: object;
|
|
47
|
+
|
|
48
|
+
// Snapshot of the basket immediately AFTER this edit was applied.
|
|
49
|
+
@Column({ type: DataType.JSON, allowNull: false })
|
|
50
|
+
declare afterItemsJson: object;
|
|
51
|
+
|
|
52
|
+
// Pre-rendered human-readable diff, e.g.
|
|
53
|
+
// "Invictus: 2 -> 1, Champion: 0 -> 1 (+1)". Computed once at confirm-time.
|
|
54
|
+
@Column({ type: DataType.TEXT, allowNull: false })
|
|
55
|
+
declare summary: string;
|
|
56
|
+
|
|
57
|
+
@Column({ type: DataType.DECIMAL(10, 3), allowNull: false, defaultValue: 0 })
|
|
58
|
+
declare priceDelta: number;
|
|
59
|
+
|
|
60
|
+
@Column({
|
|
61
|
+
type: DataType.ENUM(...Object.values(OrderEditScenario)),
|
|
62
|
+
allowNull: false,
|
|
63
|
+
})
|
|
64
|
+
declare scenario: OrderEditScenario;
|
|
65
|
+
|
|
66
|
+
// e.g. "admin:42"
|
|
67
|
+
@Column({ allowNull: false })
|
|
68
|
+
declare changedBy: string;
|
|
69
|
+
|
|
70
|
+
// ── Associations ──────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
@BelongsTo(() => Order)
|
|
73
|
+
declare order: Order;
|
|
74
|
+
|
|
75
|
+
// Set only when scenario = 'refund' (and not a COD adjustment) — the
|
|
76
|
+
// refund this edit produced.
|
|
77
|
+
@HasOne(() => Refund, { foreignKey: 'editLogId' })
|
|
78
|
+
declare refund?: Refund;
|
|
79
|
+
|
|
80
|
+
// Set only when scenario = 'additional_payment' (and not a COD
|
|
81
|
+
// adjustment) — the OrderPayments top-up this edit produced.
|
|
82
|
+
@HasOne(() => OrderPayment, { foreignKey: 'editLogId' })
|
|
83
|
+
declare additionalPayment?: OrderPayment;
|
|
84
|
+
}
|
|
@@ -110,6 +110,11 @@ export class OrderItem extends Model {
|
|
|
110
110
|
})
|
|
111
111
|
declare fulfillmentStatus: OrderItemFulfillmentStatus;
|
|
112
112
|
|
|
113
|
+
// How many units have been physically received from vendor POs so far.
|
|
114
|
+
// When fulfilledQuantity >= quantity, the item is fully sourced.
|
|
115
|
+
@Column({ defaultValue: 0 })
|
|
116
|
+
declare fulfilledQuantity: number;
|
|
117
|
+
|
|
113
118
|
// ── Associations ──────────────────────────────────────────────────────────────
|
|
114
119
|
|
|
115
120
|
@BelongsTo(() => Order)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Table,
|
|
3
|
+
Column,
|
|
4
|
+
Model,
|
|
5
|
+
ForeignKey,
|
|
6
|
+
BelongsTo,
|
|
7
|
+
DataType,
|
|
8
|
+
} from 'sequelize-typescript';
|
|
9
|
+
import { Order } from './order.entity';
|
|
10
|
+
import { OrderItemChangeLog } from './order-item-change-log.entity';
|
|
11
|
+
|
|
12
|
+
export enum OrderPaymentType {
|
|
13
|
+
ONLINE = 'online',
|
|
14
|
+
OFFLINE = 'offline',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export enum OrderPaymentStatus {
|
|
18
|
+
PENDING = 'pending',
|
|
19
|
+
PAID = 'paid',
|
|
20
|
+
FAILED = 'failed',
|
|
21
|
+
CANCELLED = 'cancelled',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* OrderPayment — Edit Order feature, rule 3c (additional payments beyond
|
|
26
|
+
* the original).
|
|
27
|
+
*
|
|
28
|
+
* The ORIGINAL payment stays in Orders.paymentMethodSlug/paymentStatus/
|
|
29
|
+
* paymentReference/paidAt exactly as today — implicitly "sequence 0" and
|
|
30
|
+
* NEVER has a row here. Each edit that raises the order total and requires
|
|
31
|
+
* an extra payment gets its own row with sequence >= 1, in order
|
|
32
|
+
* (UNIQUE(orderId, sequence)). A never-edited order has zero rows.
|
|
33
|
+
*
|
|
34
|
+
* Multiple edits over an order's lifetime each get their own sequence —
|
|
35
|
+
* Order Detail's Payment tab renders the original payment plus
|
|
36
|
+
* order.additionalPayments[] (this table, ordered by sequence), so "two (or
|
|
37
|
+
* N) payment methods on one order" scales naturally.
|
|
38
|
+
*
|
|
39
|
+
* sequence is also used as:
|
|
40
|
+
* - the gateway-facing reference suffix "-P{sequence}" (each top-up is a
|
|
41
|
+
* distinct transaction on the gateway's side — see
|
|
42
|
+
* *PaymentService::buildOrderInput()'s $sequence param)
|
|
43
|
+
* - the trailing /{sequence} path segment in the unified payment-result
|
|
44
|
+
* URL (AbstractPaymentGatewayService::paymentResultUrl()), so
|
|
45
|
+
* PaymentResultController knows which row to display/update.
|
|
46
|
+
*
|
|
47
|
+
* For an offline payment method (e.g. cash), confirm() marks this row
|
|
48
|
+
* status='paid'/type='offline'/paidAt=now() immediately — no link is
|
|
49
|
+
* generated. For an online method, status starts 'pending' and a checkout
|
|
50
|
+
* link is generated post-commit (paymentLinkToken set on success).
|
|
51
|
+
*/
|
|
52
|
+
@Table
|
|
53
|
+
export class OrderPayment extends Model {
|
|
54
|
+
@ForeignKey(() => Order)
|
|
55
|
+
@Column({ allowNull: false })
|
|
56
|
+
declare orderId: number;
|
|
57
|
+
|
|
58
|
+
// The edit that created this top-up requirement.
|
|
59
|
+
@ForeignKey(() => OrderItemChangeLog)
|
|
60
|
+
@Column({ allowNull: true })
|
|
61
|
+
declare editLogId?: number;
|
|
62
|
+
|
|
63
|
+
// 1, 2, 3... per order — the Nth additional payment.
|
|
64
|
+
@Column({ allowNull: false })
|
|
65
|
+
declare sequence: number;
|
|
66
|
+
|
|
67
|
+
@Column({ type: DataType.DECIMAL(10, 3), allowNull: false })
|
|
68
|
+
declare amount: number;
|
|
69
|
+
|
|
70
|
+
@Column({ allowNull: false })
|
|
71
|
+
declare currency: string;
|
|
72
|
+
|
|
73
|
+
@Column({ allowNull: false })
|
|
74
|
+
declare paymentMethodSlug: string;
|
|
75
|
+
|
|
76
|
+
@Column({
|
|
77
|
+
type: DataType.ENUM(...Object.values(OrderPaymentType)),
|
|
78
|
+
allowNull: false,
|
|
79
|
+
})
|
|
80
|
+
declare type: OrderPaymentType;
|
|
81
|
+
|
|
82
|
+
@Column({
|
|
83
|
+
type: DataType.ENUM(...Object.values(OrderPaymentStatus)),
|
|
84
|
+
allowNull: false,
|
|
85
|
+
defaultValue: OrderPaymentStatus.PENDING,
|
|
86
|
+
})
|
|
87
|
+
declare status: OrderPaymentStatus;
|
|
88
|
+
|
|
89
|
+
// Gateway transaction ID (online) or manual reference (offline).
|
|
90
|
+
@Column({ allowNull: true })
|
|
91
|
+
declare paymentReference?: string;
|
|
92
|
+
|
|
93
|
+
// Token used in the unified payment-result URL for this top-up.
|
|
94
|
+
@Column({ allowNull: true })
|
|
95
|
+
declare paymentLinkToken?: string;
|
|
96
|
+
|
|
97
|
+
@Column({ type: DataType.DATE, allowNull: true })
|
|
98
|
+
declare paidAt?: Date;
|
|
99
|
+
|
|
100
|
+
// ── Associations ──────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
@BelongsTo(() => Order)
|
|
103
|
+
declare order: Order;
|
|
104
|
+
|
|
105
|
+
@BelongsTo(() => OrderItemChangeLog)
|
|
106
|
+
declare editLog?: OrderItemChangeLog;
|
|
107
|
+
}
|
|
@@ -14,6 +14,9 @@ import { OrderStatus } from './order-status.entity';
|
|
|
14
14
|
import { OrderItem } from './order-item.entity';
|
|
15
15
|
import { OrderStatusHistory } from './order-status-history.entity';
|
|
16
16
|
import { UserTransaction } from './user-transaction.entity';
|
|
17
|
+
import { OrderItemChangeLog } from './order-item-change-log.entity';
|
|
18
|
+
import { OrderPayment } from './order-payment.entity';
|
|
19
|
+
import { OrderEditSession } from './order-edit-session.entity';
|
|
17
20
|
|
|
18
21
|
@Table
|
|
19
22
|
export class Order extends Model {
|
|
@@ -159,6 +162,9 @@ export class Order extends Model {
|
|
|
159
162
|
})
|
|
160
163
|
declare refundStatus: string;
|
|
161
164
|
|
|
165
|
+
@Column({ type: DataType.BOOLEAN, defaultValue: false })
|
|
166
|
+
declare hasEverNeededVendor: boolean;
|
|
167
|
+
|
|
162
168
|
@Column({ type: DataType.BOOLEAN, defaultValue: false })
|
|
163
169
|
declare cashCollected: boolean;
|
|
164
170
|
|
|
@@ -197,4 +203,13 @@ export class Order extends Model {
|
|
|
197
203
|
|
|
198
204
|
@HasMany(() => UserTransaction)
|
|
199
205
|
declare transactions: UserTransaction[];
|
|
206
|
+
|
|
207
|
+
@HasMany(() => OrderItemChangeLog)
|
|
208
|
+
declare changeLogs: OrderItemChangeLog[];
|
|
209
|
+
|
|
210
|
+
@HasMany(() => OrderPayment)
|
|
211
|
+
declare additionalPayments: OrderPayment[];
|
|
212
|
+
|
|
213
|
+
@HasMany(() => OrderEditSession)
|
|
214
|
+
declare editSessions: OrderEditSession[];
|
|
200
215
|
}
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
import { Order } from './order.entity';
|
|
10
10
|
import { User } from './user.entity';
|
|
11
11
|
import { SysUser } from './sys.user.entity';
|
|
12
|
+
import { OrderItemChangeLog } from './order-item-change-log.entity';
|
|
13
|
+
|
|
14
|
+
export enum RefundSource {
|
|
15
|
+
CANCELLATION = 'cancellation',
|
|
16
|
+
ORDER_EDIT = 'order_edit',
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
export enum RefundMethod {
|
|
14
20
|
WALLET = 'wallet',
|
|
@@ -55,6 +61,23 @@ export class Refund extends Model {
|
|
|
55
61
|
@Column({ type: DataType.TEXT, allowNull: true })
|
|
56
62
|
declare notes?: string;
|
|
57
63
|
|
|
64
|
+
// Where this refund originated. 'order_edit' refunds are created when an
|
|
65
|
+
// edit lowers the order total (rule 3b) and the admin chose 'wallet' or
|
|
66
|
+
// 'bank' as the refund method. Existing cancellation-flow refunds keep the
|
|
67
|
+
// default 'cancellation' — no backfill needed.
|
|
68
|
+
@Column({
|
|
69
|
+
type: DataType.ENUM(...Object.values(RefundSource)),
|
|
70
|
+
allowNull: false,
|
|
71
|
+
defaultValue: RefundSource.CANCELLATION,
|
|
72
|
+
})
|
|
73
|
+
declare source: RefundSource;
|
|
74
|
+
|
|
75
|
+
// Set when source = 'order_edit' — links this refund to the specific edit
|
|
76
|
+
// that produced it (OrderItemChangeLogs.id). Null for cancellation refunds.
|
|
77
|
+
@ForeignKey(() => OrderItemChangeLog)
|
|
78
|
+
@Column({ allowNull: true })
|
|
79
|
+
declare editLogId?: number;
|
|
80
|
+
|
|
58
81
|
@Column({ type: DataType.DATE, allowNull: true })
|
|
59
82
|
declare processedAt?: Date;
|
|
60
83
|
|
|
@@ -72,4 +95,6 @@ export class Refund extends Model {
|
|
|
72
95
|
declare processor?: SysUser;
|
|
73
96
|
@BelongsTo(() => SysUser, { foreignKey: 'createdBy', as: 'creator' })
|
|
74
97
|
declare creator?: SysUser;
|
|
98
|
+
@BelongsTo(() => OrderItemChangeLog, { foreignKey: 'editLogId' })
|
|
99
|
+
declare editLog?: OrderItemChangeLog;
|
|
75
100
|
}
|
package/package.json
CHANGED
package/src/app.module.ts
CHANGED
package/src/database/config.js
CHANGED
|
@@ -5,7 +5,7 @@ module.exports = {
|
|
|
5
5
|
host: process.env.MYSQL_HOST,
|
|
6
6
|
port: process.env.MYSQL_PORT,
|
|
7
7
|
username: process.env.MYSQL_USER,
|
|
8
|
-
password: process.env.
|
|
8
|
+
password: process.env.MYSQL_PASSWORD,
|
|
9
9
|
database: process.env.MYSQL_DATABASE,
|
|
10
10
|
},
|
|
11
11
|
LOCAL: {
|
|
@@ -13,7 +13,7 @@ module.exports = {
|
|
|
13
13
|
host: process.env.MYSQL_HOST,
|
|
14
14
|
port: process.env.MYSQL_PORT,
|
|
15
15
|
username: process.env.MYSQL_USER,
|
|
16
|
-
password: process.env.
|
|
16
|
+
password: process.env.MYSQL_PASSWORD,
|
|
17
17
|
database: process.env.MYSQL_DATABASE,
|
|
18
18
|
},
|
|
19
19
|
STAGING: {
|
|
@@ -21,7 +21,7 @@ module.exports = {
|
|
|
21
21
|
host: process.env.MYSQL_HOST,
|
|
22
22
|
port: process.env.MYSQL_PORT,
|
|
23
23
|
username: process.env.MYSQL_USER,
|
|
24
|
-
password: process.env.
|
|
24
|
+
password: process.env.MYSQL_PASSWORD,
|
|
25
25
|
database: process.env.MYSQL_DATABASE,
|
|
26
26
|
},
|
|
27
27
|
PRODUCTION: {
|
|
@@ -29,7 +29,7 @@ module.exports = {
|
|
|
29
29
|
host: process.env.MYSQL_HOST,
|
|
30
30
|
port: process.env.MYSQL_PORT,
|
|
31
31
|
username: process.env.MYSQL_USER,
|
|
32
|
-
password: process.env.
|
|
32
|
+
password: process.env.MYSQL_PASSWORD,
|
|
33
33
|
database: process.env.MYSQL_DATABASE,
|
|
34
34
|
},
|
|
35
35
|
};
|