@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.
Files changed (30) hide show
  1. package/.medusa/server/src/api/admin/retailos/cms/accumulation/[storeId]/route.js +3 -5
  2. package/.medusa/server/src/api/admin/retailos/cms/export/route.js +2 -2
  3. package/.medusa/server/src/api/admin/retailos/cms/handovers/[id]/route.js +2 -2
  4. package/.medusa/server/src/api/admin/retailos/cms/handovers/route.js +72 -7
  5. package/.medusa/server/src/api/admin/retailos/cms/petty-cash/route.js +35 -4
  6. package/.medusa/server/src/api/admin/retailos/cms/reconciliation/route.js +42 -0
  7. package/.medusa/server/src/modules/cms/migrations/Migration20260626000000.js +16 -0
  8. package/.medusa/server/src/modules/cms/models/cms-handover.js +3 -1
  9. package/.medusa/server/src/modules/cms/permissions.js +6 -1
  10. package/.medusa/server/src/modules/cms/services/cms-module-service.js +137 -42
  11. package/README.md +116 -0
  12. package/package.json +3 -3
  13. package/src/api/admin/retailos/cms/accumulation/[storeId]/__tests__/route.test.ts +27 -9
  14. package/src/api/admin/retailos/cms/accumulation/[storeId]/route.ts +2 -4
  15. package/src/api/admin/retailos/cms/export/__tests__/route.test.ts +9 -9
  16. package/src/api/admin/retailos/cms/export/route.ts +1 -1
  17. package/src/api/admin/retailos/cms/handovers/[id]/__tests__/route.test.ts +5 -5
  18. package/src/api/admin/retailos/cms/handovers/[id]/route.ts +1 -1
  19. package/src/api/admin/retailos/cms/handovers/__tests__/route.test.ts +22 -11
  20. package/src/api/admin/retailos/cms/handovers/route.ts +78 -6
  21. package/src/api/admin/retailos/cms/petty-cash/__tests__/route.test.ts +18 -12
  22. package/src/api/admin/retailos/cms/petty-cash/route.ts +46 -4
  23. package/src/api/admin/retailos/cms/reconciliation/__tests__/route.test.ts +124 -0
  24. package/src/api/admin/retailos/cms/reconciliation/route.ts +47 -0
  25. package/src/modules/cms/__tests__/cms-module-service.test.ts +219 -78
  26. package/src/modules/cms/__tests__/permissions.test.ts +3 -2
  27. package/src/modules/cms/migrations/Migration20260626000000.ts +17 -0
  28. package/src/modules/cms/models/cms-handover.ts +2 -0
  29. package/src/modules/cms/permissions.ts +5 -0
  30. 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.listCmsAccumulations({ store_id: [store_id] })
123
+ const existing = await this.listRetailosCmsAccumulations({ store_id: [store_id] })
84
124
  if (existing.length === 0) {
85
- await this.createCmsAccumulations([{ store_id, cash_in_store: opening_amount }])
125
+ await this.createRetailosCmsAccumulations([{ store_id, cash_in_store: opening_amount }])
86
126
  } else {
87
- await this.updateCmsAccumulations([{ id: (existing[0] as any).id, cash_in_store: opening_amount }])
127
+ await this.updateRetailosCmsAccumulations([{ id: (existing[0] as any).id, cash_in_store: opening_amount }])
88
128
  }
89
129
 
90
- await this.createPettyCashTransactions([{
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: null,
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.listCmsAccumulations({ store_id: [store_id] })
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.createPettyCashTransactions([{
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: null,
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 [created] = await this.createCmsHandovers([{
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: input.total_cash ?? null,
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
- const accumulations = await this.listCmsAccumulations({ store_id: [store_id] })
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.listPettyCashes({ store_id: [store_id] })
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.createPettyCashes([{ store_id, balance: new_balance }])
254
+ await this.createRetailosPettyCashes([{ store_id, balance: new_balance }])
200
255
  } else {
201
- await this.updatePettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
256
+ await this.updateRetailosPettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
202
257
  }
203
258
 
204
- const [tx] = await this.createPettyCashTransactions([{
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.listPettyCashes({ store_id: [store_id] })
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.createPettyCashes([{ store_id, balance: new_balance }])
310
+ await this.createRetailosPettyCashes([{ store_id, balance: new_balance }])
239
311
  } else {
240
- await this.updatePettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
312
+ await this.updateRetailosPettyCashes([{ id: (existing[0] as any).id, balance: new_balance }])
241
313
  }
242
314
 
243
- const [tx] = await this.createPettyCashTransactions([{
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.listCmsAccumulations({ store_id: [store_id] })
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.listPettyCashTransactions(dbFilters, {
452
+ const rows = await this.listRetailosPettyCashTransactions(dbFilters, {
277
453
  take: filters.limit ?? 100,
278
454
  order: { created_at: "DESC" } as any,
279
455
  })