@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
|
@@ -4,18 +4,18 @@ import CmsModuleService from "../services/cms-module-service"
|
|
|
4
4
|
|
|
5
5
|
// Test subclass that overrides all MedusaService base CRUD methods
|
|
6
6
|
class TestCmsService extends (CmsModuleService as any) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
createRetailosCmsHandovers = vi.fn()
|
|
8
|
+
listRetailosCmsHandovers = vi.fn().mockResolvedValue([])
|
|
9
|
+
listAndCountRetailosCmsHandovers = vi.fn()
|
|
10
|
+
retrieveRetailosCmsHandover = vi.fn()
|
|
11
|
+
createRetailosCmsAccumulations = vi.fn()
|
|
12
|
+
listRetailosCmsAccumulations = vi.fn()
|
|
13
|
+
updateRetailosCmsAccumulations = vi.fn()
|
|
14
|
+
createRetailosPettyCashes = vi.fn()
|
|
15
|
+
listRetailosPettyCashes = vi.fn().mockResolvedValue([])
|
|
16
|
+
updateRetailosPettyCashes = vi.fn()
|
|
17
|
+
createRetailosPettyCashTransactions = vi.fn()
|
|
18
|
+
listRetailosPettyCashTransactions = vi.fn()
|
|
19
19
|
|
|
20
20
|
constructor() {
|
|
21
21
|
// Skip super() to avoid MedusaService container requirement
|
|
@@ -37,30 +37,30 @@ function makeService(): TestCmsService {
|
|
|
37
37
|
describe("CmsModuleService.dayStart", () => {
|
|
38
38
|
it("creates accumulation when none exists", async () => {
|
|
39
39
|
const service = makeService()
|
|
40
|
-
service.
|
|
41
|
-
service.
|
|
42
|
-
service.
|
|
40
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([])
|
|
41
|
+
service.createRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 500 }])
|
|
42
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_1" }])
|
|
43
43
|
|
|
44
44
|
await service.dayStart({ store_id: "s1", employee_id: "emp_1", opening_amount: 500 })
|
|
45
45
|
|
|
46
|
-
expect(service.
|
|
46
|
+
expect(service.createRetailosCmsAccumulations).toHaveBeenCalledWith([
|
|
47
47
|
expect.objectContaining({ store_id: "s1", cash_in_store: 500 }),
|
|
48
48
|
])
|
|
49
|
-
expect(service.
|
|
49
|
+
expect(service.createRetailosPettyCashTransactions).toHaveBeenCalledWith([
|
|
50
50
|
expect.objectContaining({ transaction_type: "open", entry_type: "CR", amount: 500 }),
|
|
51
51
|
])
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
it("updates accumulation when already exists", async () => {
|
|
55
55
|
const service = makeService()
|
|
56
|
-
service.
|
|
57
|
-
service.
|
|
58
|
-
service.
|
|
56
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 0 }])
|
|
57
|
+
service.updateRetailosCmsAccumulations.mockResolvedValue([])
|
|
58
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_1" }])
|
|
59
59
|
|
|
60
60
|
await service.dayStart({ store_id: "s1", employee_id: "emp_1", opening_amount: 1000 })
|
|
61
61
|
|
|
62
|
-
expect(service.
|
|
63
|
-
expect(service.
|
|
62
|
+
expect(service.createRetailosCmsAccumulations).not.toHaveBeenCalled()
|
|
63
|
+
expect(service.updateRetailosCmsAccumulations).toHaveBeenCalled()
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
it("throws when opening_amount is negative", async () => {
|
|
@@ -74,12 +74,12 @@ describe("CmsModuleService.dayStart", () => {
|
|
|
74
74
|
describe("CmsModuleService.dayEnd", () => {
|
|
75
75
|
it("records closing transaction with difference", async () => {
|
|
76
76
|
const service = makeService()
|
|
77
|
-
service.
|
|
78
|
-
service.
|
|
77
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
78
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_2" }])
|
|
79
79
|
|
|
80
80
|
await service.dayEnd({ store_id: "s1", employee_id: "emp_1", closing_amount: 800 })
|
|
81
81
|
|
|
82
|
-
expect(service.
|
|
82
|
+
expect(service.createRetailosPettyCashTransactions).toHaveBeenCalledWith([
|
|
83
83
|
expect.objectContaining({
|
|
84
84
|
transaction_type: "close",
|
|
85
85
|
entry_type: "DB",
|
|
@@ -101,9 +101,9 @@ describe("CmsModuleService.dayEnd", () => {
|
|
|
101
101
|
describe("CmsModuleService.handover", () => {
|
|
102
102
|
it("creates handover and increases accumulation for CR type", async () => {
|
|
103
103
|
const service = makeService()
|
|
104
|
-
service.
|
|
105
|
-
service.
|
|
106
|
-
service.
|
|
104
|
+
service.createRetailosCmsHandovers.mockResolvedValue([{ id: "cmsh_1", store_id: "s1", handover_amount: 200, type: "CR" }])
|
|
105
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
106
|
+
service.updateRetailosCmsAccumulations.mockResolvedValue([])
|
|
107
107
|
|
|
108
108
|
const result = await service.handover({
|
|
109
109
|
store_id: "s1",
|
|
@@ -113,20 +113,20 @@ describe("CmsModuleService.handover", () => {
|
|
|
113
113
|
})
|
|
114
114
|
|
|
115
115
|
expect(result).toMatchObject({ id: "cmsh_1" })
|
|
116
|
-
expect(service.
|
|
116
|
+
expect(service.updateRetailosCmsAccumulations).toHaveBeenCalledWith([
|
|
117
117
|
expect.objectContaining({ id: "cmsa_1", cash_in_store: 1200 }),
|
|
118
118
|
])
|
|
119
119
|
})
|
|
120
120
|
|
|
121
121
|
it("decreases accumulation for DB type", async () => {
|
|
122
122
|
const service = makeService()
|
|
123
|
-
service.
|
|
124
|
-
service.
|
|
125
|
-
service.
|
|
123
|
+
service.createRetailosCmsHandovers.mockResolvedValue([{ id: "cmsh_2", store_id: "s1", handover_amount: 300, type: "DB" }])
|
|
124
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
125
|
+
service.updateRetailosCmsAccumulations.mockResolvedValue([])
|
|
126
126
|
|
|
127
127
|
await service.handover({ store_id: "s1", employee_id: "emp_1", handover_amount: 300, type: "DB" })
|
|
128
128
|
|
|
129
|
-
expect(service.
|
|
129
|
+
expect(service.updateRetailosCmsAccumulations).toHaveBeenCalledWith([
|
|
130
130
|
expect.objectContaining({ id: "cmsa_1", cash_in_store: 700 }),
|
|
131
131
|
])
|
|
132
132
|
})
|
|
@@ -142,16 +142,16 @@ describe("CmsModuleService.handover", () => {
|
|
|
142
142
|
describe("CmsModuleService.addPettyCash", () => {
|
|
143
143
|
it("creates petty cash record and transaction when none exists", async () => {
|
|
144
144
|
const service = makeService()
|
|
145
|
-
service.
|
|
146
|
-
service.
|
|
147
|
-
service.
|
|
145
|
+
service.listRetailosPettyCashes.mockResolvedValue([])
|
|
146
|
+
service.createRetailosPettyCashes.mockResolvedValue([{ id: "pc_1", store_id: "s1", balance: 500 }])
|
|
147
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_3" }])
|
|
148
148
|
|
|
149
149
|
await service.addPettyCash({ store_id: "s1", employee_id: "emp_1", amount: 500 })
|
|
150
150
|
|
|
151
|
-
expect(service.
|
|
151
|
+
expect(service.createRetailosPettyCashes).toHaveBeenCalledWith([
|
|
152
152
|
expect.objectContaining({ store_id: "s1", balance: 500 }),
|
|
153
153
|
])
|
|
154
|
-
expect(service.
|
|
154
|
+
expect(service.createRetailosPettyCashTransactions).toHaveBeenCalledWith([
|
|
155
155
|
expect.objectContaining({ entry_type: "CR", amount: 500 }),
|
|
156
156
|
])
|
|
157
157
|
})
|
|
@@ -167,16 +167,16 @@ describe("CmsModuleService.addPettyCash", () => {
|
|
|
167
167
|
describe("CmsModuleService.addExpense", () => {
|
|
168
168
|
it("debits petty cash and records transaction", async () => {
|
|
169
169
|
const service = makeService()
|
|
170
|
-
service.
|
|
171
|
-
service.
|
|
172
|
-
service.
|
|
170
|
+
service.listRetailosPettyCashes.mockResolvedValue([{ id: "pc_1", store_id: "s1", balance: 500 }])
|
|
171
|
+
service.updateRetailosPettyCashes.mockResolvedValue([])
|
|
172
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_4" }])
|
|
173
173
|
|
|
174
174
|
await service.addExpense({ store_id: "s1", employee_id: "emp_1", amount: 100, category: "office" })
|
|
175
175
|
|
|
176
|
-
expect(service.
|
|
176
|
+
expect(service.updateRetailosPettyCashes).toHaveBeenCalledWith([
|
|
177
177
|
expect.objectContaining({ id: "pc_1", balance: 400 }),
|
|
178
178
|
])
|
|
179
|
-
expect(service.
|
|
179
|
+
expect(service.createRetailosPettyCashTransactions).toHaveBeenCalledWith([
|
|
180
180
|
expect.objectContaining({ entry_type: "DB", expense_amount: 100, category: "office" }),
|
|
181
181
|
])
|
|
182
182
|
})
|
|
@@ -192,7 +192,7 @@ describe("CmsModuleService.addExpense", () => {
|
|
|
192
192
|
describe("CmsModuleService.getAccumulation", () => {
|
|
193
193
|
it("returns accumulation for store", async () => {
|
|
194
194
|
const service = makeService()
|
|
195
|
-
service.
|
|
195
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 750 }])
|
|
196
196
|
|
|
197
197
|
const result = await service.getAccumulation("s1")
|
|
198
198
|
|
|
@@ -201,7 +201,7 @@ describe("CmsModuleService.getAccumulation", () => {
|
|
|
201
201
|
|
|
202
202
|
it("returns null when no accumulation exists", async () => {
|
|
203
203
|
const service = makeService()
|
|
204
|
-
service.
|
|
204
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([])
|
|
205
205
|
|
|
206
206
|
const result = await service.getAccumulation("s1")
|
|
207
207
|
|
|
@@ -216,11 +216,11 @@ describe("CmsModuleService.getShiftLogs", () => {
|
|
|
216
216
|
{ id: "pct_1", transaction_type: "open", store_id: "s1" },
|
|
217
217
|
{ id: "pct_2", transaction_type: "close", store_id: "s1" },
|
|
218
218
|
]
|
|
219
|
-
service.
|
|
219
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue(mockLogs)
|
|
220
220
|
|
|
221
221
|
const result = await service.getShiftLogs({ store_id: "s1" })
|
|
222
222
|
|
|
223
|
-
expect(service.
|
|
223
|
+
expect(service.listRetailosPettyCashTransactions).toHaveBeenCalledWith(
|
|
224
224
|
expect.objectContaining({ transaction_type: ["open", "close"], store_id: ["s1"] }),
|
|
225
225
|
expect.any(Object)
|
|
226
226
|
)
|
|
@@ -229,23 +229,23 @@ describe("CmsModuleService.getShiftLogs", () => {
|
|
|
229
229
|
|
|
230
230
|
it("omits store_id filter when not provided", async () => {
|
|
231
231
|
const service = makeService()
|
|
232
|
-
service.
|
|
232
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([])
|
|
233
233
|
|
|
234
234
|
await service.getShiftLogs({})
|
|
235
235
|
|
|
236
|
-
expect(service.
|
|
236
|
+
expect(service.listRetailosPettyCashTransactions).toHaveBeenCalledWith(
|
|
237
237
|
expect.not.objectContaining({ store_id: expect.anything() }),
|
|
238
238
|
expect.any(Object)
|
|
239
239
|
)
|
|
240
240
|
})
|
|
241
241
|
|
|
242
|
-
it("passes custom limit to
|
|
242
|
+
it("passes custom limit to listRetailosPettyCashTransactions", async () => {
|
|
243
243
|
const service = makeService()
|
|
244
|
-
service.
|
|
244
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([])
|
|
245
245
|
|
|
246
246
|
await service.getShiftLogs({ store_id: "s1", limit: 10 })
|
|
247
247
|
|
|
248
|
-
expect(service.
|
|
248
|
+
expect(service.listRetailosPettyCashTransactions).toHaveBeenCalledWith(
|
|
249
249
|
expect.any(Object),
|
|
250
250
|
expect.objectContaining({ take: 10 })
|
|
251
251
|
)
|
|
@@ -255,12 +255,12 @@ describe("CmsModuleService.getShiftLogs", () => {
|
|
|
255
255
|
describe("CmsModuleService.dayEnd - no prior accumulation", () => {
|
|
256
256
|
it("uses zero as current_cash when no accumulation exists", async () => {
|
|
257
257
|
const service = makeService()
|
|
258
|
-
service.
|
|
259
|
-
service.
|
|
258
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([])
|
|
259
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_x" }])
|
|
260
260
|
|
|
261
261
|
await service.dayEnd({ store_id: "s1", employee_id: "emp_1", closing_amount: 300 })
|
|
262
262
|
|
|
263
|
-
expect(service.
|
|
263
|
+
expect(service.createRetailosPettyCashTransactions).toHaveBeenCalledWith([
|
|
264
264
|
expect.objectContaining({
|
|
265
265
|
opening_balance: 0,
|
|
266
266
|
closing_balance: 300,
|
|
@@ -271,28 +271,24 @@ describe("CmsModuleService.dayEnd - no prior accumulation", () => {
|
|
|
271
271
|
})
|
|
272
272
|
|
|
273
273
|
describe("CmsModuleService.handover - edge cases", () => {
|
|
274
|
-
it("
|
|
274
|
+
it("throws when no Day Start has been run for the store", async () => {
|
|
275
275
|
const service = makeService()
|
|
276
|
-
service.
|
|
277
|
-
service.listCmsAccumulations.mockResolvedValue([])
|
|
278
|
-
service.createCmsAccumulations.mockResolvedValue([])
|
|
276
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([])
|
|
279
277
|
|
|
280
|
-
await
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
expect.objectContaining({ store_id: "s1", cash_in_store: 100 }),
|
|
284
|
-
])
|
|
278
|
+
await expect(
|
|
279
|
+
service.handover({ store_id: "s1", employee_id: "emp_1", handover_amount: 100, type: "CR" })
|
|
280
|
+
).rejects.toThrow("Day Start must be completed before creating a handover")
|
|
285
281
|
})
|
|
286
282
|
|
|
287
283
|
it("does not change cash when type is null", async () => {
|
|
288
284
|
const service = makeService()
|
|
289
|
-
service.
|
|
290
|
-
service.
|
|
291
|
-
service.
|
|
285
|
+
service.createRetailosCmsHandovers.mockResolvedValue([{ id: "cmsh_4", store_id: "s1", handover_amount: 50, type: null }])
|
|
286
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
287
|
+
service.updateRetailosCmsAccumulations.mockResolvedValue([])
|
|
292
288
|
|
|
293
289
|
await service.handover({ store_id: "s1", employee_id: "emp_1", handover_amount: 50, type: null })
|
|
294
290
|
|
|
295
|
-
expect(service.
|
|
291
|
+
expect(service.updateRetailosCmsAccumulations).toHaveBeenCalledWith([
|
|
296
292
|
expect.objectContaining({ cash_in_store: 1000 }),
|
|
297
293
|
])
|
|
298
294
|
})
|
|
@@ -301,17 +297,17 @@ describe("CmsModuleService.handover - edge cases", () => {
|
|
|
301
297
|
describe("CmsModuleService.addPettyCash - update path", () => {
|
|
302
298
|
it("updates existing petty cash balance", async () => {
|
|
303
299
|
const service = makeService()
|
|
304
|
-
service.
|
|
305
|
-
service.
|
|
306
|
-
service.
|
|
300
|
+
service.listRetailosPettyCashes.mockResolvedValue([{ id: "pc_1", store_id: "s1", balance: 200 }])
|
|
301
|
+
service.updateRetailosPettyCashes.mockResolvedValue([])
|
|
302
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_5" }])
|
|
307
303
|
|
|
308
304
|
await service.addPettyCash({ store_id: "s1", employee_id: "emp_1", amount: 300 })
|
|
309
305
|
|
|
310
|
-
expect(service.
|
|
311
|
-
expect(service.
|
|
306
|
+
expect(service.createRetailosPettyCashes).not.toHaveBeenCalled()
|
|
307
|
+
expect(service.updateRetailosPettyCashes).toHaveBeenCalledWith([
|
|
312
308
|
expect.objectContaining({ id: "pc_1", balance: 500 }),
|
|
313
309
|
])
|
|
314
|
-
expect(service.
|
|
310
|
+
expect(service.createRetailosPettyCashTransactions).toHaveBeenCalledWith([
|
|
315
311
|
expect.objectContaining({ opening_balance: 200, closing_balance: 500 }),
|
|
316
312
|
])
|
|
317
313
|
})
|
|
@@ -320,14 +316,159 @@ describe("CmsModuleService.addPettyCash - update path", () => {
|
|
|
320
316
|
describe("CmsModuleService.addExpense - no existing petty cash", () => {
|
|
321
317
|
it("creates petty cash record when none exists", async () => {
|
|
322
318
|
const service = makeService()
|
|
323
|
-
service.
|
|
324
|
-
service.
|
|
325
|
-
service.
|
|
319
|
+
service.listRetailosPettyCashes.mockResolvedValue([])
|
|
320
|
+
service.createRetailosPettyCashes.mockResolvedValue([{ id: "pc_new", store_id: "s1", balance: -100 }])
|
|
321
|
+
service.createRetailosPettyCashTransactions.mockResolvedValue([{ id: "pct_6" }])
|
|
326
322
|
|
|
327
323
|
await service.addExpense({ store_id: "s1", employee_id: "emp_1", amount: 100 })
|
|
328
324
|
|
|
329
|
-
expect(service.
|
|
325
|
+
expect(service.createRetailosPettyCashes).toHaveBeenCalledWith([
|
|
330
326
|
expect.objectContaining({ store_id: "s1", balance: -100 }),
|
|
331
327
|
])
|
|
332
328
|
})
|
|
333
329
|
})
|
|
330
|
+
|
|
331
|
+
describe("CmsModuleService.getReconciliationReport", () => {
|
|
332
|
+
it("returns zeros when no transactions exist for the day", async () => {
|
|
333
|
+
const service = makeService()
|
|
334
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([])
|
|
335
|
+
service.listRetailosCmsHandovers.mockResolvedValue([])
|
|
336
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", cash_in_store: 5000 }])
|
|
337
|
+
|
|
338
|
+
const result = await service.getReconciliationReport({ store_id: "s1", date: "2026-06-23" })
|
|
339
|
+
|
|
340
|
+
expect(result).toMatchObject({
|
|
341
|
+
date: "2026-06-23",
|
|
342
|
+
store_id: "s1",
|
|
343
|
+
opening_balance: 0,
|
|
344
|
+
net_cash_sales: 0,
|
|
345
|
+
total_handovers_in: 0,
|
|
346
|
+
total_handovers_out: 0,
|
|
347
|
+
petty_cash_in: 0,
|
|
348
|
+
petty_cash_out: 0,
|
|
349
|
+
expected_closing_balance: 0,
|
|
350
|
+
closing_balance: null,
|
|
351
|
+
cms_balance: 5000,
|
|
352
|
+
variance: null,
|
|
353
|
+
is_balanced: null,
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it("computes expected_closing_balance correctly", async () => {
|
|
358
|
+
const service = makeService()
|
|
359
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([
|
|
360
|
+
{ transaction_type: "open", entry_type: "CR", amount: 8200 },
|
|
361
|
+
{ transaction_type: "petty_cash", entry_type: "CR", amount: 500 },
|
|
362
|
+
{ transaction_type: "petty_cash", entry_type: "DB", amount: 1000 },
|
|
363
|
+
])
|
|
364
|
+
service.listRetailosCmsHandovers.mockResolvedValue([
|
|
365
|
+
{ type: "CR", handover_amount: 2000 },
|
|
366
|
+
{ type: "DB", handover_amount: 5000 },
|
|
367
|
+
])
|
|
368
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", cash_in_store: 12700 }])
|
|
369
|
+
|
|
370
|
+
const result = await service.getReconciliationReport({
|
|
371
|
+
store_id: "s1",
|
|
372
|
+
date: "2026-06-23",
|
|
373
|
+
net_cash_sales: 28450,
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// 8200 + 28450 + 500 - 1000 + 2000 - 5000 = 33150
|
|
377
|
+
expect(result.expected_closing_balance).toBe(33150)
|
|
378
|
+
expect(result.opening_balance).toBe(8200)
|
|
379
|
+
expect(result.petty_cash_in).toBe(500)
|
|
380
|
+
expect(result.petty_cash_out).toBe(1000)
|
|
381
|
+
expect(result.total_handovers_in).toBe(2000)
|
|
382
|
+
expect(result.total_handovers_out).toBe(5000)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it("computes variance and is_balanced when day is closed", async () => {
|
|
386
|
+
const service = makeService()
|
|
387
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([
|
|
388
|
+
{ transaction_type: "open", entry_type: "CR", amount: 8200 },
|
|
389
|
+
{ transaction_type: "close", entry_type: "DB", amount: 31650 },
|
|
390
|
+
])
|
|
391
|
+
service.listRetailosCmsHandovers.mockResolvedValue([
|
|
392
|
+
{ type: "DB", handover_amount: 5000 },
|
|
393
|
+
])
|
|
394
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([])
|
|
395
|
+
|
|
396
|
+
const result = await service.getReconciliationReport({
|
|
397
|
+
store_id: "s1",
|
|
398
|
+
date: "2026-06-23",
|
|
399
|
+
net_cash_sales: 28450,
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// expected = 8200 + 28450 - 5000 = 31650; actual = 31650
|
|
403
|
+
expect(result.expected_closing_balance).toBe(31650)
|
|
404
|
+
expect(result.closing_balance).toBe(31650)
|
|
405
|
+
expect(result.variance).toBe(0)
|
|
406
|
+
expect(result.is_balanced).toBe(true)
|
|
407
|
+
expect(result.cms_balance).toBeNull()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it("detects variance when actual closing differs from expected", async () => {
|
|
411
|
+
const service = makeService()
|
|
412
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([
|
|
413
|
+
{ transaction_type: "open", entry_type: "CR", amount: 8200 },
|
|
414
|
+
{ transaction_type: "close", entry_type: "DB", amount: 16350 },
|
|
415
|
+
])
|
|
416
|
+
service.listRetailosCmsHandovers.mockResolvedValue([
|
|
417
|
+
{ type: "DB", handover_amount: 5000 },
|
|
418
|
+
])
|
|
419
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", cash_in_store: 50500 }])
|
|
420
|
+
|
|
421
|
+
const result = await service.getReconciliationReport({
|
|
422
|
+
store_id: "s1",
|
|
423
|
+
date: "2026-06-23",
|
|
424
|
+
net_cash_sales: 28450,
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// expected = 8200 + 28450 - 5000 = 31650; actual = 16350
|
|
428
|
+
expect(result.variance).toBe(-15300)
|
|
429
|
+
expect(result.is_balanced).toBe(false)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it("filters transactions by date range using created_at", async () => {
|
|
433
|
+
const service = makeService()
|
|
434
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([])
|
|
435
|
+
service.listRetailosCmsHandovers.mockResolvedValue([])
|
|
436
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([])
|
|
437
|
+
|
|
438
|
+
await service.getReconciliationReport({ store_id: "s1", date: "2026-06-23" })
|
|
439
|
+
|
|
440
|
+
expect(service.listRetailosPettyCashTransactions).toHaveBeenCalledWith(
|
|
441
|
+
expect.objectContaining({
|
|
442
|
+
store_id: ["s1"],
|
|
443
|
+
created_at: expect.objectContaining({ $gte: expect.any(Date), $lte: expect.any(Date) }),
|
|
444
|
+
}),
|
|
445
|
+
expect.any(Object)
|
|
446
|
+
)
|
|
447
|
+
expect(service.listRetailosCmsHandovers).toHaveBeenCalledWith(
|
|
448
|
+
expect.objectContaining({
|
|
449
|
+
store_id: ["s1"],
|
|
450
|
+
created_at: expect.objectContaining({ $gte: expect.any(Date), $lte: expect.any(Date) }),
|
|
451
|
+
}),
|
|
452
|
+
expect.any(Object)
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it("separates shift_logs from handovers in the report", async () => {
|
|
457
|
+
const service = makeService()
|
|
458
|
+
const openTx = { id: "pct_open", transaction_type: "open", entry_type: "CR", amount: 8200 }
|
|
459
|
+
const closeTx = { id: "pct_close", transaction_type: "close", entry_type: "DB", amount: 8200 }
|
|
460
|
+
const pettyCashTx = { id: "pct_pc", transaction_type: "petty_cash", entry_type: "CR", amount: 500 }
|
|
461
|
+
const handover = { id: "cmsh_1", type: "DB", handover_amount: 1000 }
|
|
462
|
+
|
|
463
|
+
service.listRetailosPettyCashTransactions.mockResolvedValue([openTx, pettyCashTx, closeTx])
|
|
464
|
+
service.listRetailosCmsHandovers.mockResolvedValue([handover])
|
|
465
|
+
service.listRetailosCmsAccumulations.mockResolvedValue([])
|
|
466
|
+
|
|
467
|
+
const result = await service.getReconciliationReport({ store_id: "s1", date: "2026-06-23" })
|
|
468
|
+
|
|
469
|
+
expect(result.shift_logs).toHaveLength(2)
|
|
470
|
+
expect(result.shift_logs).toEqual(expect.arrayContaining([openTx, closeTx]))
|
|
471
|
+
expect(result.handovers).toHaveLength(1)
|
|
472
|
+
expect(result.handovers[0]).toMatchObject({ id: "cmsh_1" })
|
|
473
|
+
})
|
|
474
|
+
})
|
|
@@ -9,10 +9,11 @@ describe("CMS_PERMISSIONS", () => {
|
|
|
9
9
|
"cms.petty_cash.read",
|
|
10
10
|
"cms.petty_cash.operate",
|
|
11
11
|
"cms.accumulation.read",
|
|
12
|
+
"cms.reconciliation.read",
|
|
12
13
|
]
|
|
13
14
|
|
|
14
|
-
it("exports exactly
|
|
15
|
-
expect(CMS_PERMISSIONS).toHaveLength(
|
|
15
|
+
it("exports exactly 7 permission keys", () => {
|
|
16
|
+
expect(CMS_PERMISSIONS).toHaveLength(7)
|
|
16
17
|
})
|
|
17
18
|
|
|
18
19
|
it("contains all required permission keys", () => {
|
|
@@ -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
|
]
|