@devx-retailos/cms 0.0.1 → 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/.medusa/server/src/api/admin/retailos/cms/accumulation/[storeId]/route.js +3 -5
- package/.medusa/server/src/api/admin/retailos/cms/export/route.js +2 -2
- package/.medusa/server/src/api/admin/retailos/cms/handovers/[id]/route.js +2 -2
- package/.medusa/server/src/api/admin/retailos/cms/handovers/route.js +72 -7
- package/.medusa/server/src/api/admin/retailos/cms/petty-cash/route.js +35 -4
- package/.medusa/server/src/api/admin/retailos/cms/reconciliation/route.js +42 -0
- package/.medusa/server/src/modules/cms/migrations/Migration20260626000000.js +16 -0
- package/.medusa/server/src/modules/cms/models/cms-handover.js +3 -1
- package/.medusa/server/src/modules/cms/permissions.js +6 -1
- package/.medusa/server/src/modules/cms/services/cms-module-service.js +137 -42
- package/README.md +116 -0
- package/package.json +3 -3
- package/src/api/admin/retailos/cms/accumulation/[storeId]/__tests__/route.test.ts +27 -9
- package/src/api/admin/retailos/cms/accumulation/[storeId]/route.ts +2 -4
- package/src/api/admin/retailos/cms/export/__tests__/route.test.ts +9 -9
- package/src/api/admin/retailos/cms/export/route.ts +1 -1
- package/src/api/admin/retailos/cms/handovers/[id]/__tests__/route.test.ts +5 -5
- package/src/api/admin/retailos/cms/handovers/[id]/route.ts +1 -1
- package/src/api/admin/retailos/cms/handovers/__tests__/route.test.ts +22 -11
- package/src/api/admin/retailos/cms/handovers/route.ts +78 -6
- package/src/api/admin/retailos/cms/petty-cash/__tests__/route.test.ts +18 -12
- package/src/api/admin/retailos/cms/petty-cash/route.ts +46 -4
- package/src/api/admin/retailos/cms/reconciliation/__tests__/route.test.ts +124 -0
- package/src/api/admin/retailos/cms/reconciliation/route.ts +47 -0
- package/src/modules/cms/__tests__/cms-module-service.test.ts +219 -78
- package/src/modules/cms/__tests__/permissions.test.ts +3 -2
- package/src/modules/cms/migrations/Migration20260626000000.ts +17 -0
- package/src/modules/cms/models/cms-handover.ts +2 -0
- package/src/modules/cms/permissions.ts +5 -0
- package/src/modules/cms/services/cms-module-service.ts +217 -41
|
@@ -59,11 +59,51 @@ export interface ShiftLogFilters {
|
|
|
59
59
|
limit?: number
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface ReconciliationInput {
|
|
63
|
+
store_id: string
|
|
64
|
+
/** Calendar date in YYYY-MM-DD format (treated as UTC). */
|
|
65
|
+
date: string
|
|
66
|
+
/** Net cash collected from orders for the day. Defaults to 0 when not provided. */
|
|
67
|
+
net_cash_sales?: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface EnrichedAccumulation {
|
|
71
|
+
id?: string
|
|
72
|
+
store_id: string
|
|
73
|
+
cash_in_store: number
|
|
74
|
+
petty_cash: number
|
|
75
|
+
opening_store_cash: number | null
|
|
76
|
+
opening_petty_cash: number | null
|
|
77
|
+
closing_store_cash: number | null
|
|
78
|
+
closing_petty_cash: number | null
|
|
79
|
+
created_at?: string
|
|
80
|
+
updated_at?: string
|
|
81
|
+
metadata?: Record<string, unknown> | null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ReconciliationReport {
|
|
85
|
+
date: string
|
|
86
|
+
store_id: string
|
|
87
|
+
opening_balance: number
|
|
88
|
+
net_cash_sales: number
|
|
89
|
+
total_handovers_in: number
|
|
90
|
+
total_handovers_out: number
|
|
91
|
+
petty_cash_in: number
|
|
92
|
+
petty_cash_out: number
|
|
93
|
+
expected_closing_balance: number
|
|
94
|
+
closing_balance: number | null
|
|
95
|
+
cms_balance: number | null
|
|
96
|
+
variance: number | null
|
|
97
|
+
is_balanced: boolean | null
|
|
98
|
+
handovers: unknown[]
|
|
99
|
+
shift_logs: unknown[]
|
|
100
|
+
}
|
|
101
|
+
|
|
62
102
|
class CmsModuleService extends MedusaService({
|
|
63
|
-
CmsHandover,
|
|
64
|
-
CmsAccumulation,
|
|
65
|
-
PettyCash,
|
|
66
|
-
PettyCashTransaction,
|
|
103
|
+
RetailosCmsHandover: CmsHandover,
|
|
104
|
+
RetailosCmsAccumulation: CmsAccumulation,
|
|
105
|
+
RetailosPettyCash: PettyCash,
|
|
106
|
+
RetailosPettyCashTransaction: PettyCashTransaction,
|
|
67
107
|
}) {
|
|
68
108
|
protected logger_: Logger
|
|
69
109
|
|
|
@@ -80,14 +120,17 @@ class CmsModuleService extends MedusaService({
|
|
|
80
120
|
throw new Error("[retailos/cms] opening_amount must be >= 0")
|
|
81
121
|
}
|
|
82
122
|
|
|
83
|
-
const existing = await this.
|
|
123
|
+
const existing = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
84
124
|
if (existing.length === 0) {
|
|
85
|
-
await this.
|
|
125
|
+
await this.createRetailosCmsAccumulations([{ store_id, cash_in_store: opening_amount }])
|
|
86
126
|
} else {
|
|
87
|
-
await this.
|
|
127
|
+
await this.updateRetailosCmsAccumulations([{ id: (existing[0] as any).id, cash_in_store: opening_amount }])
|
|
88
128
|
}
|
|
89
129
|
|
|
90
|
-
await this.
|
|
130
|
+
const pettyCashes = await this.listRetailosPettyCashes({ store_id: [store_id] })
|
|
131
|
+
const petty_cash_balance = pettyCashes[0] ? (pettyCashes[0] as any).balance : 0
|
|
132
|
+
|
|
133
|
+
await this.createRetailosPettyCashTransactions([{
|
|
91
134
|
store_id,
|
|
92
135
|
employee_id,
|
|
93
136
|
transaction_type: "open" as TransactionType,
|
|
@@ -103,10 +146,10 @@ class CmsModuleService extends MedusaService({
|
|
|
103
146
|
reason: null,
|
|
104
147
|
image_url: null,
|
|
105
148
|
date: new Date(),
|
|
106
|
-
metadata:
|
|
149
|
+
metadata: { petty_cash_balance },
|
|
107
150
|
}])
|
|
108
151
|
|
|
109
|
-
this.logger_.info("cms.day_started", { store_id, employee_id, opening_amount })
|
|
152
|
+
this.logger_.info("cms.day_started", { store_id, employee_id, opening_amount, petty_cash_balance })
|
|
110
153
|
}
|
|
111
154
|
|
|
112
155
|
async dayEnd(input: DayEndInput): Promise<void> {
|
|
@@ -115,11 +158,14 @@ class CmsModuleService extends MedusaService({
|
|
|
115
158
|
throw new Error("[retailos/cms] closing_amount must be >= 0")
|
|
116
159
|
}
|
|
117
160
|
|
|
118
|
-
const accumulations = await this.
|
|
161
|
+
const accumulations = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
119
162
|
const current_cash = accumulations[0] ? (accumulations[0] as any).cash_in_store : 0
|
|
120
163
|
const difference = closing_amount - current_cash
|
|
121
164
|
|
|
122
|
-
await this.
|
|
165
|
+
const pettyCashes = await this.listRetailosPettyCashes({ store_id: [store_id] })
|
|
166
|
+
const petty_cash_balance = pettyCashes[0] ? (pettyCashes[0] as any).balance : 0
|
|
167
|
+
|
|
168
|
+
await this.createRetailosPettyCashTransactions([{
|
|
123
169
|
store_id,
|
|
124
170
|
employee_id,
|
|
125
171
|
transaction_type: "close" as TransactionType,
|
|
@@ -135,10 +181,10 @@ class CmsModuleService extends MedusaService({
|
|
|
135
181
|
reason: null,
|
|
136
182
|
image_url: null,
|
|
137
183
|
date: new Date(),
|
|
138
|
-
metadata:
|
|
184
|
+
metadata: { petty_cash_balance },
|
|
139
185
|
}])
|
|
140
186
|
|
|
141
|
-
this.logger_.info("cms.day_ended", { store_id, employee_id, closing_amount, difference })
|
|
187
|
+
this.logger_.info("cms.day_ended", { store_id, employee_id, closing_amount, difference, petty_cash_balance })
|
|
142
188
|
}
|
|
143
189
|
|
|
144
190
|
async handover(input: HandoverInput) {
|
|
@@ -147,39 +193,48 @@ class CmsModuleService extends MedusaService({
|
|
|
147
193
|
throw new Error("[retailos/cms] handover_amount must be >= 0")
|
|
148
194
|
}
|
|
149
195
|
|
|
150
|
-
const
|
|
196
|
+
const accumulations = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
197
|
+
if (accumulations.length === 0) {
|
|
198
|
+
throw new Error("[retailos/cms] Day Start must be completed before creating a handover")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const opening_balance = (accumulations[0] as any).cash_in_store as number
|
|
202
|
+
|
|
203
|
+
let closing_balance = opening_balance
|
|
204
|
+
if (type === "CR") {
|
|
205
|
+
closing_balance = opening_balance + handover_amount
|
|
206
|
+
} else if (type === "DB") {
|
|
207
|
+
if (handover_amount > opening_balance) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`[retailos/cms] Insufficient cash in store: cannot debit ${handover_amount}, current balance is ${opening_balance}`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
closing_balance = opening_balance - handover_amount
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const [created] = await this.createRetailosCmsHandovers([{
|
|
151
216
|
store_id,
|
|
152
217
|
employee_id: input.employee_id,
|
|
153
218
|
handover_id: input.handover_id ?? null,
|
|
154
219
|
handover_amount,
|
|
155
|
-
total_cash:
|
|
220
|
+
total_cash: closing_balance,
|
|
221
|
+
opening_balance,
|
|
222
|
+
closing_balance,
|
|
156
223
|
type: type ?? null,
|
|
157
224
|
image_url: input.image_url ?? null,
|
|
158
225
|
remark: input.remark ?? null,
|
|
159
226
|
metadata: input.metadata ?? null,
|
|
160
227
|
}])
|
|
161
228
|
|
|
162
|
-
|
|
163
|
-
const current = accumulations[0] ? (accumulations[0] as any).cash_in_store : 0
|
|
164
|
-
|
|
165
|
-
let updated_cash = current
|
|
166
|
-
if (type === "CR") {
|
|
167
|
-
updated_cash = current + handover_amount
|
|
168
|
-
} else if (type === "DB") {
|
|
169
|
-
updated_cash = current - handover_amount
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (accumulations.length === 0) {
|
|
173
|
-
await this.createCmsAccumulations([{ store_id, cash_in_store: updated_cash }])
|
|
174
|
-
} else {
|
|
175
|
-
await this.updateCmsAccumulations([{ id: (accumulations[0] as any).id, cash_in_store: updated_cash }])
|
|
176
|
-
}
|
|
229
|
+
await this.updateRetailosCmsAccumulations([{ id: (accumulations[0] as any).id, cash_in_store: closing_balance }])
|
|
177
230
|
|
|
178
231
|
this.logger_.info("[retailos/cms] handover created", {
|
|
179
232
|
id: (created as any)?.id,
|
|
180
233
|
store_id,
|
|
181
234
|
handover_amount,
|
|
182
235
|
type,
|
|
236
|
+
opening_balance,
|
|
237
|
+
closing_balance,
|
|
183
238
|
})
|
|
184
239
|
|
|
185
240
|
return created
|
|
@@ -191,17 +246,34 @@ class CmsModuleService extends MedusaService({
|
|
|
191
246
|
throw new Error("[retailos/cms] amount must be > 0")
|
|
192
247
|
}
|
|
193
248
|
|
|
194
|
-
const existing = await this.
|
|
249
|
+
const existing = await this.listRetailosPettyCashes({ store_id: [store_id] })
|
|
195
250
|
const current_balance = existing[0] ? (existing[0] as any).balance : 0
|
|
196
251
|
const new_balance = current_balance + amount
|
|
197
252
|
|
|
198
253
|
if (existing.length === 0) {
|
|
199
|
-
await this.
|
|
254
|
+
await this.createRetailosPettyCashes([{ store_id, balance: new_balance }])
|
|
200
255
|
} else {
|
|
201
|
-
await this.
|
|
256
|
+
await this.updateRetailosPettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
|
|
202
257
|
}
|
|
203
258
|
|
|
204
|
-
|
|
259
|
+
// When funded from store cash, deduct the amount from cms_accumulation
|
|
260
|
+
if (source === "from_petty_cash") {
|
|
261
|
+
const accumulations = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
262
|
+
const current_store_cash = accumulations[0] ? (accumulations[0] as any).cash_in_store : 0
|
|
263
|
+
const new_store_cash = current_store_cash - amount
|
|
264
|
+
|
|
265
|
+
if (accumulations.length === 0) {
|
|
266
|
+
await this.createRetailosCmsAccumulations([{ store_id, cash_in_store: new_store_cash }])
|
|
267
|
+
} else {
|
|
268
|
+
await this.updateRetailosCmsAccumulations([{ id: (accumulations[0] as any).id, cash_in_store: new_store_cash }])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
this.logger_.info("[retailos/cms] store cash deducted for petty cash transfer", {
|
|
272
|
+
store_id, amount, new_store_cash,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const [tx] = await this.createRetailosPettyCashTransactions([{
|
|
205
277
|
store_id,
|
|
206
278
|
employee_id,
|
|
207
279
|
transaction_type: "petty_cash" as TransactionType,
|
|
@@ -230,17 +302,17 @@ class CmsModuleService extends MedusaService({
|
|
|
230
302
|
throw new Error("[retailos/cms] expense amount must be > 0")
|
|
231
303
|
}
|
|
232
304
|
|
|
233
|
-
const existing = await this.
|
|
305
|
+
const existing = await this.listRetailosPettyCashes({ store_id: [store_id] })
|
|
234
306
|
const current_balance = existing[0] ? (existing[0] as any).balance : 0
|
|
235
307
|
const new_balance = current_balance - amount
|
|
236
308
|
|
|
237
309
|
if (existing.length === 0) {
|
|
238
|
-
await this.
|
|
310
|
+
await this.createRetailosPettyCashes([{ store_id, balance: new_balance }])
|
|
239
311
|
} else {
|
|
240
|
-
await this.
|
|
312
|
+
await this.updateRetailosPettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
|
|
241
313
|
}
|
|
242
314
|
|
|
243
|
-
const [tx] = await this.
|
|
315
|
+
const [tx] = await this.createRetailosPettyCashTransactions([{
|
|
244
316
|
store_id,
|
|
245
317
|
employee_id,
|
|
246
318
|
transaction_type: "petty_cash" as TransactionType,
|
|
@@ -264,16 +336,120 @@ class CmsModuleService extends MedusaService({
|
|
|
264
336
|
}
|
|
265
337
|
|
|
266
338
|
async getAccumulation(store_id: string) {
|
|
267
|
-
const rows = await this.
|
|
339
|
+
const rows = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
268
340
|
return rows[0] ?? null
|
|
269
341
|
}
|
|
270
342
|
|
|
343
|
+
async getEnrichedAccumulation(store_id: string): Promise<EnrichedAccumulation> {
|
|
344
|
+
const accumulation = await this.getAccumulation(store_id)
|
|
345
|
+
|
|
346
|
+
const pettyCashes = await this.listRetailosPettyCashes({ store_id: [store_id] })
|
|
347
|
+
const petty_cash = pettyCashes[0] ? (pettyCashes[0] as any).balance : 0
|
|
348
|
+
|
|
349
|
+
const now = new Date()
|
|
350
|
+
const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`
|
|
351
|
+
const start = new Date(`${dateStr}T00:00:00.000Z`)
|
|
352
|
+
const end = new Date(`${dateStr}T23:59:59.999Z`)
|
|
353
|
+
|
|
354
|
+
const todayTxns = await this.listRetailosPettyCashTransactions(
|
|
355
|
+
{ store_id: [store_id], created_at: { $gte: start, $lte: end } as any },
|
|
356
|
+
{ take: 10, order: { created_at: "ASC" } as any }
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const txnList = todayTxns as any[]
|
|
360
|
+
const openTx = txnList.find((t) => t.transaction_type === "open")
|
|
361
|
+
const closeTx = txnList.find((t) => t.transaction_type === "close")
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
...(accumulation ? (accumulation as any) : { store_id, cash_in_store: 0 }),
|
|
365
|
+
petty_cash,
|
|
366
|
+
opening_store_cash: openTx ? openTx.amount : null,
|
|
367
|
+
opening_petty_cash: openTx?.metadata?.petty_cash_balance ?? null,
|
|
368
|
+
closing_store_cash: closeTx ? closeTx.amount : null,
|
|
369
|
+
closing_petty_cash: closeTx?.metadata?.petty_cash_balance ?? null,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async getReconciliationReport(input: ReconciliationInput): Promise<ReconciliationReport> {
|
|
374
|
+
const { store_id, date, net_cash_sales = 0 } = input
|
|
375
|
+
|
|
376
|
+
const start = new Date(`${date}T00:00:00.000Z`)
|
|
377
|
+
const end = new Date(`${date}T23:59:59.999Z`)
|
|
378
|
+
const dateFilter = { $gte: start, $lte: end }
|
|
379
|
+
|
|
380
|
+
const txns = await this.listRetailosPettyCashTransactions(
|
|
381
|
+
{ store_id: [store_id], created_at: dateFilter },
|
|
382
|
+
{ take: 1000, order: { created_at: "ASC" } as any }
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
const handovers = await this.listRetailosCmsHandovers(
|
|
386
|
+
{ store_id: [store_id], created_at: dateFilter },
|
|
387
|
+
{ take: 1000, order: { created_at: "ASC" } as any }
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
const accumulation = await this.getAccumulation(store_id)
|
|
391
|
+
|
|
392
|
+
const txnList = txns as any[]
|
|
393
|
+
const handoverList = handovers as any[]
|
|
394
|
+
|
|
395
|
+
const openTx = txnList.find((t) => t.transaction_type === "open")
|
|
396
|
+
const closeTx = txnList.find((t) => t.transaction_type === "close")
|
|
397
|
+
|
|
398
|
+
const opening_balance: number = openTx ? openTx.amount : 0
|
|
399
|
+
const closing_balance: number | null = closeTx ? closeTx.amount : null
|
|
400
|
+
|
|
401
|
+
const pettyCashTxns = txnList.filter((t) => t.transaction_type === "petty_cash")
|
|
402
|
+
const petty_cash_in = pettyCashTxns
|
|
403
|
+
.filter((t) => t.entry_type === "CR")
|
|
404
|
+
.reduce((sum: number, t: any) => sum + t.amount, 0)
|
|
405
|
+
const petty_cash_out = pettyCashTxns
|
|
406
|
+
.filter((t) => t.entry_type === "DB")
|
|
407
|
+
.reduce((sum: number, t: any) => sum + t.amount, 0)
|
|
408
|
+
|
|
409
|
+
const total_handovers_in = handoverList
|
|
410
|
+
.filter((h) => h.type === "CR")
|
|
411
|
+
.reduce((sum: number, h: any) => sum + h.handover_amount, 0)
|
|
412
|
+
const total_handovers_out = handoverList
|
|
413
|
+
.filter((h) => h.type === "DB")
|
|
414
|
+
.reduce((sum: number, h: any) => sum + h.handover_amount, 0)
|
|
415
|
+
|
|
416
|
+
const expected_closing_balance =
|
|
417
|
+
opening_balance + net_cash_sales + petty_cash_in - petty_cash_out + total_handovers_in - total_handovers_out
|
|
418
|
+
|
|
419
|
+
const variance = closing_balance !== null ? closing_balance - expected_closing_balance : null
|
|
420
|
+
const is_balanced = variance !== null ? variance === 0 : null
|
|
421
|
+
|
|
422
|
+
const shift_logs = txnList.filter(
|
|
423
|
+
(t) => t.transaction_type === "open" || t.transaction_type === "close"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
this.logger_.info("[retailos/cms] reconciliation report generated", { store_id, date })
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
date,
|
|
430
|
+
store_id,
|
|
431
|
+
opening_balance,
|
|
432
|
+
net_cash_sales,
|
|
433
|
+
total_handovers_in,
|
|
434
|
+
total_handovers_out,
|
|
435
|
+
petty_cash_in,
|
|
436
|
+
petty_cash_out,
|
|
437
|
+
expected_closing_balance,
|
|
438
|
+
closing_balance,
|
|
439
|
+
cms_balance: accumulation ? (accumulation as any).cash_in_store : null,
|
|
440
|
+
variance,
|
|
441
|
+
is_balanced,
|
|
442
|
+
handovers: handoverList,
|
|
443
|
+
shift_logs,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
271
447
|
async getShiftLogs(filters: ShiftLogFilters) {
|
|
272
448
|
const dbFilters: Record<string, unknown> = {}
|
|
273
449
|
if (filters.store_id) dbFilters.store_id = [filters.store_id]
|
|
274
450
|
dbFilters.transaction_type = ["open", "close"]
|
|
275
451
|
|
|
276
|
-
const rows = await this.
|
|
452
|
+
const rows = await this.listRetailosPettyCashTransactions(dbFilters, {
|
|
277
453
|
take: filters.limit ?? 100,
|
|
278
454
|
order: { created_at: "DESC" } as any,
|
|
279
455
|
})
|