@devx-retailos/cms 0.0.1 → 0.0.2
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/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 +108 -38
- package/README.md +116 -0
- package/package.json +1 -1
- 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 +163 -37
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
+
|
|
3
|
+
export class Migration20260626000000 extends Migration {
|
|
4
|
+
override async up(): Promise<void> {
|
|
5
|
+
this.addSql(
|
|
6
|
+
`alter table if exists "retailos_cms_handover" add column if not exists "opening_balance" numeric null;`
|
|
7
|
+
)
|
|
8
|
+
this.addSql(
|
|
9
|
+
`alter table if exists "retailos_cms_handover" add column if not exists "closing_balance" numeric null;`
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async down(): Promise<void> {
|
|
14
|
+
this.addSql(`alter table if exists "retailos_cms_handover" drop column if exists "opening_balance";`)
|
|
15
|
+
this.addSql(`alter table if exists "retailos_cms_handover" drop column if exists "closing_balance";`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -7,6 +7,8 @@ const CmsHandover = model.define("retailos_cms_handover", {
|
|
|
7
7
|
handover_id: model.text().nullable(), // free-text correlation field: the brand can pass their own ID (e.g. a bank slip number, a vault reference, a POS transaction ID) so they can cross-reference this handover record with their own external system.
|
|
8
8
|
handover_amount: model.number().nullable(),
|
|
9
9
|
total_cash: model.number().nullable(),
|
|
10
|
+
opening_balance: model.number().nullable(),
|
|
11
|
+
closing_balance: model.number().nullable(),
|
|
10
12
|
type: model.text().nullable(),
|
|
11
13
|
image_url: model.text().nullable(),
|
|
12
14
|
remark: model.text().nullable(),
|
|
@@ -31,4 +31,9 @@ export const CMS_PERMISSIONS: PermissionRegistration[] = [
|
|
|
31
31
|
description: "View current cash accumulation for a store",
|
|
32
32
|
registered_by: "cms",
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
key: "cms.reconciliation.read",
|
|
36
|
+
description: "View daily cash reconciliation report with variance analysis",
|
|
37
|
+
registered_by: "cms",
|
|
38
|
+
},
|
|
34
39
|
]
|
|
@@ -59,11 +59,37 @@ 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 ReconciliationReport {
|
|
71
|
+
date: string
|
|
72
|
+
store_id: string
|
|
73
|
+
opening_balance: number
|
|
74
|
+
net_cash_sales: number
|
|
75
|
+
total_handovers_in: number
|
|
76
|
+
total_handovers_out: number
|
|
77
|
+
petty_cash_in: number
|
|
78
|
+
petty_cash_out: number
|
|
79
|
+
expected_closing_balance: number
|
|
80
|
+
closing_balance: number | null
|
|
81
|
+
cms_balance: number | null
|
|
82
|
+
variance: number | null
|
|
83
|
+
is_balanced: boolean | null
|
|
84
|
+
handovers: unknown[]
|
|
85
|
+
shift_logs: unknown[]
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
class CmsModuleService extends MedusaService({
|
|
63
|
-
CmsHandover,
|
|
64
|
-
CmsAccumulation,
|
|
65
|
-
PettyCash,
|
|
66
|
-
PettyCashTransaction,
|
|
89
|
+
RetailosCmsHandover: CmsHandover,
|
|
90
|
+
RetailosCmsAccumulation: CmsAccumulation,
|
|
91
|
+
RetailosPettyCash: PettyCash,
|
|
92
|
+
RetailosPettyCashTransaction: PettyCashTransaction,
|
|
67
93
|
}) {
|
|
68
94
|
protected logger_: Logger
|
|
69
95
|
|
|
@@ -80,14 +106,14 @@ class CmsModuleService extends MedusaService({
|
|
|
80
106
|
throw new Error("[retailos/cms] opening_amount must be >= 0")
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
const existing = await this.
|
|
109
|
+
const existing = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
84
110
|
if (existing.length === 0) {
|
|
85
|
-
await this.
|
|
111
|
+
await this.createRetailosCmsAccumulations([{ store_id, cash_in_store: opening_amount }])
|
|
86
112
|
} else {
|
|
87
|
-
await this.
|
|
113
|
+
await this.updateRetailosCmsAccumulations([{ id: (existing[0] as any).id, cash_in_store: opening_amount }])
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
await this.
|
|
116
|
+
await this.createRetailosPettyCashTransactions([{
|
|
91
117
|
store_id,
|
|
92
118
|
employee_id,
|
|
93
119
|
transaction_type: "open" as TransactionType,
|
|
@@ -115,11 +141,11 @@ class CmsModuleService extends MedusaService({
|
|
|
115
141
|
throw new Error("[retailos/cms] closing_amount must be >= 0")
|
|
116
142
|
}
|
|
117
143
|
|
|
118
|
-
const accumulations = await this.
|
|
144
|
+
const accumulations = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
119
145
|
const current_cash = accumulations[0] ? (accumulations[0] as any).cash_in_store : 0
|
|
120
146
|
const difference = closing_amount - current_cash
|
|
121
147
|
|
|
122
|
-
await this.
|
|
148
|
+
await this.createRetailosPettyCashTransactions([{
|
|
123
149
|
store_id,
|
|
124
150
|
employee_id,
|
|
125
151
|
transaction_type: "close" as TransactionType,
|
|
@@ -147,39 +173,48 @@ class CmsModuleService extends MedusaService({
|
|
|
147
173
|
throw new Error("[retailos/cms] handover_amount must be >= 0")
|
|
148
174
|
}
|
|
149
175
|
|
|
150
|
-
const
|
|
176
|
+
const accumulations = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
177
|
+
if (accumulations.length === 0) {
|
|
178
|
+
throw new Error("[retailos/cms] Day Start must be completed before creating a handover")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const opening_balance = (accumulations[0] as any).cash_in_store as number
|
|
182
|
+
|
|
183
|
+
let closing_balance = opening_balance
|
|
184
|
+
if (type === "CR") {
|
|
185
|
+
closing_balance = opening_balance + handover_amount
|
|
186
|
+
} else if (type === "DB") {
|
|
187
|
+
if (handover_amount > opening_balance) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`[retailos/cms] Insufficient cash in store: cannot debit ${handover_amount}, current balance is ${opening_balance}`
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
closing_balance = opening_balance - handover_amount
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const [created] = await this.createRetailosCmsHandovers([{
|
|
151
196
|
store_id,
|
|
152
197
|
employee_id: input.employee_id,
|
|
153
198
|
handover_id: input.handover_id ?? null,
|
|
154
199
|
handover_amount,
|
|
155
|
-
total_cash:
|
|
200
|
+
total_cash: closing_balance,
|
|
201
|
+
opening_balance,
|
|
202
|
+
closing_balance,
|
|
156
203
|
type: type ?? null,
|
|
157
204
|
image_url: input.image_url ?? null,
|
|
158
205
|
remark: input.remark ?? null,
|
|
159
206
|
metadata: input.metadata ?? null,
|
|
160
207
|
}])
|
|
161
208
|
|
|
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
|
-
}
|
|
209
|
+
await this.updateRetailosCmsAccumulations([{ id: (accumulations[0] as any).id, cash_in_store: closing_balance }])
|
|
177
210
|
|
|
178
211
|
this.logger_.info("[retailos/cms] handover created", {
|
|
179
212
|
id: (created as any)?.id,
|
|
180
213
|
store_id,
|
|
181
214
|
handover_amount,
|
|
182
215
|
type,
|
|
216
|
+
opening_balance,
|
|
217
|
+
closing_balance,
|
|
183
218
|
})
|
|
184
219
|
|
|
185
220
|
return created
|
|
@@ -191,17 +226,34 @@ class CmsModuleService extends MedusaService({
|
|
|
191
226
|
throw new Error("[retailos/cms] amount must be > 0")
|
|
192
227
|
}
|
|
193
228
|
|
|
194
|
-
const existing = await this.
|
|
229
|
+
const existing = await this.listRetailosPettyCashes({ store_id: [store_id] })
|
|
195
230
|
const current_balance = existing[0] ? (existing[0] as any).balance : 0
|
|
196
231
|
const new_balance = current_balance + amount
|
|
197
232
|
|
|
198
233
|
if (existing.length === 0) {
|
|
199
|
-
await this.
|
|
234
|
+
await this.createRetailosPettyCashes([{ store_id, balance: new_balance }])
|
|
200
235
|
} else {
|
|
201
|
-
await this.
|
|
236
|
+
await this.updateRetailosPettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// When funded from store cash, deduct the amount from cms_accumulation
|
|
240
|
+
if (source === "from_petty_cash") {
|
|
241
|
+
const accumulations = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
242
|
+
const current_store_cash = accumulations[0] ? (accumulations[0] as any).cash_in_store : 0
|
|
243
|
+
const new_store_cash = current_store_cash - amount
|
|
244
|
+
|
|
245
|
+
if (accumulations.length === 0) {
|
|
246
|
+
await this.createRetailosCmsAccumulations([{ store_id, cash_in_store: new_store_cash }])
|
|
247
|
+
} else {
|
|
248
|
+
await this.updateRetailosCmsAccumulations([{ id: (accumulations[0] as any).id, cash_in_store: new_store_cash }])
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.logger_.info("[retailos/cms] store cash deducted for petty cash transfer", {
|
|
252
|
+
store_id, amount, new_store_cash,
|
|
253
|
+
})
|
|
202
254
|
}
|
|
203
255
|
|
|
204
|
-
const [tx] = await this.
|
|
256
|
+
const [tx] = await this.createRetailosPettyCashTransactions([{
|
|
205
257
|
store_id,
|
|
206
258
|
employee_id,
|
|
207
259
|
transaction_type: "petty_cash" as TransactionType,
|
|
@@ -230,17 +282,17 @@ class CmsModuleService extends MedusaService({
|
|
|
230
282
|
throw new Error("[retailos/cms] expense amount must be > 0")
|
|
231
283
|
}
|
|
232
284
|
|
|
233
|
-
const existing = await this.
|
|
285
|
+
const existing = await this.listRetailosPettyCashes({ store_id: [store_id] })
|
|
234
286
|
const current_balance = existing[0] ? (existing[0] as any).balance : 0
|
|
235
287
|
const new_balance = current_balance - amount
|
|
236
288
|
|
|
237
289
|
if (existing.length === 0) {
|
|
238
|
-
await this.
|
|
290
|
+
await this.createRetailosPettyCashes([{ store_id, balance: new_balance }])
|
|
239
291
|
} else {
|
|
240
|
-
await this.
|
|
292
|
+
await this.updateRetailosPettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
|
|
241
293
|
}
|
|
242
294
|
|
|
243
|
-
const [tx] = await this.
|
|
295
|
+
const [tx] = await this.createRetailosPettyCashTransactions([{
|
|
244
296
|
store_id,
|
|
245
297
|
employee_id,
|
|
246
298
|
transaction_type: "petty_cash" as TransactionType,
|
|
@@ -264,16 +316,90 @@ class CmsModuleService extends MedusaService({
|
|
|
264
316
|
}
|
|
265
317
|
|
|
266
318
|
async getAccumulation(store_id: string) {
|
|
267
|
-
const rows = await this.
|
|
319
|
+
const rows = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
|
|
268
320
|
return rows[0] ?? null
|
|
269
321
|
}
|
|
270
322
|
|
|
323
|
+
async getReconciliationReport(input: ReconciliationInput): Promise<ReconciliationReport> {
|
|
324
|
+
const { store_id, date, net_cash_sales = 0 } = input
|
|
325
|
+
|
|
326
|
+
const start = new Date(`${date}T00:00:00.000Z`)
|
|
327
|
+
const end = new Date(`${date}T23:59:59.999Z`)
|
|
328
|
+
const dateFilter = { $gte: start, $lte: end }
|
|
329
|
+
|
|
330
|
+
const txns = await this.listRetailosPettyCashTransactions(
|
|
331
|
+
{ store_id: [store_id], created_at: dateFilter },
|
|
332
|
+
{ take: 1000, order: { created_at: "ASC" } as any }
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
const handovers = await this.listRetailosCmsHandovers(
|
|
336
|
+
{ store_id: [store_id], created_at: dateFilter },
|
|
337
|
+
{ take: 1000, order: { created_at: "ASC" } as any }
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
const accumulation = await this.getAccumulation(store_id)
|
|
341
|
+
|
|
342
|
+
const txnList = txns as any[]
|
|
343
|
+
const handoverList = handovers as any[]
|
|
344
|
+
|
|
345
|
+
const openTx = txnList.find((t) => t.transaction_type === "open")
|
|
346
|
+
const closeTx = txnList.find((t) => t.transaction_type === "close")
|
|
347
|
+
|
|
348
|
+
const opening_balance: number = openTx ? openTx.amount : 0
|
|
349
|
+
const closing_balance: number | null = closeTx ? closeTx.amount : null
|
|
350
|
+
|
|
351
|
+
const pettyCashTxns = txnList.filter((t) => t.transaction_type === "petty_cash")
|
|
352
|
+
const petty_cash_in = pettyCashTxns
|
|
353
|
+
.filter((t) => t.entry_type === "CR")
|
|
354
|
+
.reduce((sum: number, t: any) => sum + t.amount, 0)
|
|
355
|
+
const petty_cash_out = pettyCashTxns
|
|
356
|
+
.filter((t) => t.entry_type === "DB")
|
|
357
|
+
.reduce((sum: number, t: any) => sum + t.amount, 0)
|
|
358
|
+
|
|
359
|
+
const total_handovers_in = handoverList
|
|
360
|
+
.filter((h) => h.type === "CR")
|
|
361
|
+
.reduce((sum: number, h: any) => sum + h.handover_amount, 0)
|
|
362
|
+
const total_handovers_out = handoverList
|
|
363
|
+
.filter((h) => h.type === "DB")
|
|
364
|
+
.reduce((sum: number, h: any) => sum + h.handover_amount, 0)
|
|
365
|
+
|
|
366
|
+
const expected_closing_balance =
|
|
367
|
+
opening_balance + net_cash_sales + petty_cash_in - petty_cash_out + total_handovers_in - total_handovers_out
|
|
368
|
+
|
|
369
|
+
const variance = closing_balance !== null ? closing_balance - expected_closing_balance : null
|
|
370
|
+
const is_balanced = variance !== null ? variance === 0 : null
|
|
371
|
+
|
|
372
|
+
const shift_logs = txnList.filter(
|
|
373
|
+
(t) => t.transaction_type === "open" || t.transaction_type === "close"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
this.logger_.info("[retailos/cms] reconciliation report generated", { store_id, date })
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
date,
|
|
380
|
+
store_id,
|
|
381
|
+
opening_balance,
|
|
382
|
+
net_cash_sales,
|
|
383
|
+
total_handovers_in,
|
|
384
|
+
total_handovers_out,
|
|
385
|
+
petty_cash_in,
|
|
386
|
+
petty_cash_out,
|
|
387
|
+
expected_closing_balance,
|
|
388
|
+
closing_balance,
|
|
389
|
+
cms_balance: accumulation ? (accumulation as any).cash_in_store : null,
|
|
390
|
+
variance,
|
|
391
|
+
is_balanced,
|
|
392
|
+
handovers: handoverList,
|
|
393
|
+
shift_logs,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
271
397
|
async getShiftLogs(filters: ShiftLogFilters) {
|
|
272
398
|
const dbFilters: Record<string, unknown> = {}
|
|
273
399
|
if (filters.store_id) dbFilters.store_id = [filters.store_id]
|
|
274
400
|
dbFilters.transaction_type = ["open", "close"]
|
|
275
401
|
|
|
276
|
-
const rows = await this.
|
|
402
|
+
const rows = await this.listRetailosPettyCashTransactions(dbFilters, {
|
|
277
403
|
take: filters.limit ?? 100,
|
|
278
404
|
order: { created_at: "DESC" } as any,
|
|
279
405
|
})
|