@devx-retailos/cms 0.0.1
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/admin/index.js +23 -0
- package/.medusa/server/src/admin/index.mjs +24 -0
- package/.medusa/server/src/api/admin/retailos/cms/accumulation/[storeId]/route.js +34 -0
- package/.medusa/server/src/api/admin/retailos/cms/export/route.js +52 -0
- package/.medusa/server/src/api/admin/retailos/cms/handovers/[id]/route.js +35 -0
- package/.medusa/server/src/api/admin/retailos/cms/handovers/route.js +78 -0
- package/.medusa/server/src/api/admin/retailos/cms/operation/route.js +124 -0
- package/.medusa/server/src/api/admin/retailos/cms/petty-cash/route.js +31 -0
- package/.medusa/server/src/api/admin/retailos/cms/shift-logs/route.js +36 -0
- package/.medusa/server/src/links/employee-to-cms-handover.js +13 -0
- package/.medusa/server/src/links/store-to-cms-handover.js +13 -0
- package/.medusa/server/src/modules/cms/constants.js +5 -0
- package/.medusa/server/src/modules/cms/index.js +53 -0
- package/.medusa/server/src/modules/cms/migrations/Migration20260622000000.js +30 -0
- package/.medusa/server/src/modules/cms/migrations/Migration20260622000001.js +16 -0
- package/.medusa/server/src/modules/cms/models/cms-accumulation.js +11 -0
- package/.medusa/server/src/modules/cms/models/cms-handover.js +17 -0
- package/.medusa/server/src/modules/cms/models/index.js +15 -0
- package/.medusa/server/src/modules/cms/models/petty-cash-transaction.js +24 -0
- package/.medusa/server/src/modules/cms/models/petty-cash.js +11 -0
- package/.medusa/server/src/modules/cms/permissions.js +36 -0
- package/.medusa/server/src/modules/cms/services/cms-module-service.js +210 -0
- package/.medusa/server/src/modules/cms/services/index.js +9 -0
- package/README.md +155 -0
- package/package.json +60 -0
- package/src/admin/.gitkeep +0 -0
- package/src/api/admin/retailos/cms/accumulation/[storeId]/__tests__/route.test.ts +68 -0
- package/src/api/admin/retailos/cms/accumulation/[storeId]/route.ts +35 -0
- package/src/api/admin/retailos/cms/export/__tests__/route.test.ts +126 -0
- package/src/api/admin/retailos/cms/export/route.ts +58 -0
- package/src/api/admin/retailos/cms/handovers/[id]/__tests__/route.test.ts +68 -0
- package/src/api/admin/retailos/cms/handovers/[id]/route.ts +36 -0
- package/src/api/admin/retailos/cms/handovers/__tests__/route.test.ts +104 -0
- package/src/api/admin/retailos/cms/handovers/route.ts +79 -0
- package/src/api/admin/retailos/cms/operation/__tests__/route.test.ts +169 -0
- package/src/api/admin/retailos/cms/operation/route.ts +130 -0
- package/src/api/admin/retailos/cms/petty-cash/__tests__/route.test.ts +73 -0
- package/src/api/admin/retailos/cms/petty-cash/route.ts +35 -0
- package/src/api/admin/retailos/cms/shift-logs/__tests__/route.test.ts +77 -0
- package/src/api/admin/retailos/cms/shift-logs/route.ts +38 -0
- package/src/links/employee-to-cms-handover.ts +11 -0
- package/src/links/store-to-cms-handover.ts +11 -0
- package/src/modules/cms/__tests__/cms-module-service.test.ts +333 -0
- package/src/modules/cms/__tests__/permissions.test.ts +36 -0
- package/src/modules/cms/constants.ts +1 -0
- package/src/modules/cms/index.ts +24 -0
- package/src/modules/cms/migrations/Migration20260622000000.ts +58 -0
- package/src/modules/cms/migrations/Migration20260622000001.ts +17 -0
- package/src/modules/cms/models/cms-accumulation.ts +10 -0
- package/src/modules/cms/models/cms-handover.ts +16 -0
- package/src/modules/cms/models/index.ts +4 -0
- package/src/modules/cms/models/petty-cash-transaction.ts +23 -0
- package/src/modules/cms/models/petty-cash.ts +10 -0
- package/src/modules/cms/permissions.ts +34 -0
- package/src/modules/cms/services/cms-module-service.ts +284 -0
- package/src/modules/cms/services/index.ts +13 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
2
|
+
import { createNoopLogger } from "@devx-retailos/core"
|
|
3
|
+
import CmsModuleService from "../services/cms-module-service"
|
|
4
|
+
|
|
5
|
+
// Test subclass that overrides all MedusaService base CRUD methods
|
|
6
|
+
class TestCmsService extends (CmsModuleService as any) {
|
|
7
|
+
createCmsHandovers = vi.fn()
|
|
8
|
+
listCmsHandovers = vi.fn()
|
|
9
|
+
listAndCountCmsHandovers = vi.fn()
|
|
10
|
+
retrieveCmsHandover = vi.fn()
|
|
11
|
+
createCmsAccumulations = vi.fn()
|
|
12
|
+
listCmsAccumulations = vi.fn()
|
|
13
|
+
updateCmsAccumulations = vi.fn()
|
|
14
|
+
createPettyCashes = vi.fn()
|
|
15
|
+
listPettyCashes = vi.fn()
|
|
16
|
+
updatePettyCashes = vi.fn()
|
|
17
|
+
createPettyCashTransactions = vi.fn()
|
|
18
|
+
listPettyCashTransactions = vi.fn()
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
// Skip super() to avoid MedusaService container requirement
|
|
22
|
+
// We directly set the logger instead
|
|
23
|
+
Object.defineProperty(TestCmsService.prototype, "__skip_super__", { value: true })
|
|
24
|
+
try {
|
|
25
|
+
super({ logger: createNoopLogger() }, {}, {})
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore container errors in test env
|
|
28
|
+
}
|
|
29
|
+
this.logger_ = createNoopLogger()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeService(): TestCmsService {
|
|
34
|
+
return new TestCmsService()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("CmsModuleService.dayStart", () => {
|
|
38
|
+
it("creates accumulation when none exists", async () => {
|
|
39
|
+
const service = makeService()
|
|
40
|
+
service.listCmsAccumulations.mockResolvedValue([])
|
|
41
|
+
service.createCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 500 }])
|
|
42
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_1" }])
|
|
43
|
+
|
|
44
|
+
await service.dayStart({ store_id: "s1", employee_id: "emp_1", opening_amount: 500 })
|
|
45
|
+
|
|
46
|
+
expect(service.createCmsAccumulations).toHaveBeenCalledWith([
|
|
47
|
+
expect.objectContaining({ store_id: "s1", cash_in_store: 500 }),
|
|
48
|
+
])
|
|
49
|
+
expect(service.createPettyCashTransactions).toHaveBeenCalledWith([
|
|
50
|
+
expect.objectContaining({ transaction_type: "open", entry_type: "CR", amount: 500 }),
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("updates accumulation when already exists", async () => {
|
|
55
|
+
const service = makeService()
|
|
56
|
+
service.listCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 0 }])
|
|
57
|
+
service.updateCmsAccumulations.mockResolvedValue([])
|
|
58
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_1" }])
|
|
59
|
+
|
|
60
|
+
await service.dayStart({ store_id: "s1", employee_id: "emp_1", opening_amount: 1000 })
|
|
61
|
+
|
|
62
|
+
expect(service.createCmsAccumulations).not.toHaveBeenCalled()
|
|
63
|
+
expect(service.updateCmsAccumulations).toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("throws when opening_amount is negative", async () => {
|
|
67
|
+
const service = makeService()
|
|
68
|
+
await expect(
|
|
69
|
+
service.dayStart({ store_id: "s1", employee_id: "emp_1", opening_amount: -1 })
|
|
70
|
+
).rejects.toThrow("opening_amount must be >= 0")
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe("CmsModuleService.dayEnd", () => {
|
|
75
|
+
it("records closing transaction with difference", async () => {
|
|
76
|
+
const service = makeService()
|
|
77
|
+
service.listCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
78
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_2" }])
|
|
79
|
+
|
|
80
|
+
await service.dayEnd({ store_id: "s1", employee_id: "emp_1", closing_amount: 800 })
|
|
81
|
+
|
|
82
|
+
expect(service.createPettyCashTransactions).toHaveBeenCalledWith([
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
transaction_type: "close",
|
|
85
|
+
entry_type: "DB",
|
|
86
|
+
opening_balance: 1000,
|
|
87
|
+
closing_balance: 800,
|
|
88
|
+
difference_amount: -200,
|
|
89
|
+
}),
|
|
90
|
+
])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("throws when closing_amount is negative", async () => {
|
|
94
|
+
const service = makeService()
|
|
95
|
+
await expect(
|
|
96
|
+
service.dayEnd({ store_id: "s1", employee_id: "emp_1", closing_amount: -100 })
|
|
97
|
+
).rejects.toThrow("closing_amount must be >= 0")
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe("CmsModuleService.handover", () => {
|
|
102
|
+
it("creates handover and increases accumulation for CR type", async () => {
|
|
103
|
+
const service = makeService()
|
|
104
|
+
service.createCmsHandovers.mockResolvedValue([{ id: "cmsh_1", store_id: "s1", handover_amount: 200, type: "CR" }])
|
|
105
|
+
service.listCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
106
|
+
service.updateCmsAccumulations.mockResolvedValue([])
|
|
107
|
+
|
|
108
|
+
const result = await service.handover({
|
|
109
|
+
store_id: "s1",
|
|
110
|
+
employee_id: "emp_1",
|
|
111
|
+
handover_amount: 200,
|
|
112
|
+
type: "CR",
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(result).toMatchObject({ id: "cmsh_1" })
|
|
116
|
+
expect(service.updateCmsAccumulations).toHaveBeenCalledWith([
|
|
117
|
+
expect.objectContaining({ id: "cmsa_1", cash_in_store: 1200 }),
|
|
118
|
+
])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("decreases accumulation for DB type", async () => {
|
|
122
|
+
const service = makeService()
|
|
123
|
+
service.createCmsHandovers.mockResolvedValue([{ id: "cmsh_2", store_id: "s1", handover_amount: 300, type: "DB" }])
|
|
124
|
+
service.listCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
125
|
+
service.updateCmsAccumulations.mockResolvedValue([])
|
|
126
|
+
|
|
127
|
+
await service.handover({ store_id: "s1", employee_id: "emp_1", handover_amount: 300, type: "DB" })
|
|
128
|
+
|
|
129
|
+
expect(service.updateCmsAccumulations).toHaveBeenCalledWith([
|
|
130
|
+
expect.objectContaining({ id: "cmsa_1", cash_in_store: 700 }),
|
|
131
|
+
])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("throws when handover_amount is negative", async () => {
|
|
135
|
+
const service = makeService()
|
|
136
|
+
await expect(
|
|
137
|
+
service.handover({ store_id: "s1", employee_id: "emp_1", handover_amount: -50 })
|
|
138
|
+
).rejects.toThrow("handover_amount must be >= 0")
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe("CmsModuleService.addPettyCash", () => {
|
|
143
|
+
it("creates petty cash record and transaction when none exists", async () => {
|
|
144
|
+
const service = makeService()
|
|
145
|
+
service.listPettyCashes.mockResolvedValue([])
|
|
146
|
+
service.createPettyCashes.mockResolvedValue([{ id: "pc_1", store_id: "s1", balance: 500 }])
|
|
147
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_3" }])
|
|
148
|
+
|
|
149
|
+
await service.addPettyCash({ store_id: "s1", employee_id: "emp_1", amount: 500 })
|
|
150
|
+
|
|
151
|
+
expect(service.createPettyCashes).toHaveBeenCalledWith([
|
|
152
|
+
expect.objectContaining({ store_id: "s1", balance: 500 }),
|
|
153
|
+
])
|
|
154
|
+
expect(service.createPettyCashTransactions).toHaveBeenCalledWith([
|
|
155
|
+
expect.objectContaining({ entry_type: "CR", amount: 500 }),
|
|
156
|
+
])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("throws when amount is zero or negative", async () => {
|
|
160
|
+
const service = makeService()
|
|
161
|
+
await expect(
|
|
162
|
+
service.addPettyCash({ store_id: "s1", employee_id: "emp_1", amount: 0 })
|
|
163
|
+
).rejects.toThrow("amount must be > 0")
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe("CmsModuleService.addExpense", () => {
|
|
168
|
+
it("debits petty cash and records transaction", async () => {
|
|
169
|
+
const service = makeService()
|
|
170
|
+
service.listPettyCashes.mockResolvedValue([{ id: "pc_1", store_id: "s1", balance: 500 }])
|
|
171
|
+
service.updatePettyCashes.mockResolvedValue([])
|
|
172
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_4" }])
|
|
173
|
+
|
|
174
|
+
await service.addExpense({ store_id: "s1", employee_id: "emp_1", amount: 100, category: "office" })
|
|
175
|
+
|
|
176
|
+
expect(service.updatePettyCashes).toHaveBeenCalledWith([
|
|
177
|
+
expect.objectContaining({ id: "pc_1", balance: 400 }),
|
|
178
|
+
])
|
|
179
|
+
expect(service.createPettyCashTransactions).toHaveBeenCalledWith([
|
|
180
|
+
expect.objectContaining({ entry_type: "DB", expense_amount: 100, category: "office" }),
|
|
181
|
+
])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("throws when expense amount is zero or negative", async () => {
|
|
185
|
+
const service = makeService()
|
|
186
|
+
await expect(
|
|
187
|
+
service.addExpense({ store_id: "s1", employee_id: "emp_1", amount: -10 })
|
|
188
|
+
).rejects.toThrow("expense amount must be > 0")
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe("CmsModuleService.getAccumulation", () => {
|
|
193
|
+
it("returns accumulation for store", async () => {
|
|
194
|
+
const service = makeService()
|
|
195
|
+
service.listCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 750 }])
|
|
196
|
+
|
|
197
|
+
const result = await service.getAccumulation("s1")
|
|
198
|
+
|
|
199
|
+
expect(result).toMatchObject({ store_id: "s1", cash_in_store: 750 })
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("returns null when no accumulation exists", async () => {
|
|
203
|
+
const service = makeService()
|
|
204
|
+
service.listCmsAccumulations.mockResolvedValue([])
|
|
205
|
+
|
|
206
|
+
const result = await service.getAccumulation("s1")
|
|
207
|
+
|
|
208
|
+
expect(result).toBeNull()
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe("CmsModuleService.getShiftLogs", () => {
|
|
213
|
+
it("filters by transaction_type open and close", async () => {
|
|
214
|
+
const service = makeService()
|
|
215
|
+
const mockLogs = [
|
|
216
|
+
{ id: "pct_1", transaction_type: "open", store_id: "s1" },
|
|
217
|
+
{ id: "pct_2", transaction_type: "close", store_id: "s1" },
|
|
218
|
+
]
|
|
219
|
+
service.listPettyCashTransactions.mockResolvedValue(mockLogs)
|
|
220
|
+
|
|
221
|
+
const result = await service.getShiftLogs({ store_id: "s1" })
|
|
222
|
+
|
|
223
|
+
expect(service.listPettyCashTransactions).toHaveBeenCalledWith(
|
|
224
|
+
expect.objectContaining({ transaction_type: ["open", "close"], store_id: ["s1"] }),
|
|
225
|
+
expect.any(Object)
|
|
226
|
+
)
|
|
227
|
+
expect(result).toHaveLength(2)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("omits store_id filter when not provided", async () => {
|
|
231
|
+
const service = makeService()
|
|
232
|
+
service.listPettyCashTransactions.mockResolvedValue([])
|
|
233
|
+
|
|
234
|
+
await service.getShiftLogs({})
|
|
235
|
+
|
|
236
|
+
expect(service.listPettyCashTransactions).toHaveBeenCalledWith(
|
|
237
|
+
expect.not.objectContaining({ store_id: expect.anything() }),
|
|
238
|
+
expect.any(Object)
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it("passes custom limit to listPettyCashTransactions", async () => {
|
|
243
|
+
const service = makeService()
|
|
244
|
+
service.listPettyCashTransactions.mockResolvedValue([])
|
|
245
|
+
|
|
246
|
+
await service.getShiftLogs({ store_id: "s1", limit: 10 })
|
|
247
|
+
|
|
248
|
+
expect(service.listPettyCashTransactions).toHaveBeenCalledWith(
|
|
249
|
+
expect.any(Object),
|
|
250
|
+
expect.objectContaining({ take: 10 })
|
|
251
|
+
)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe("CmsModuleService.dayEnd - no prior accumulation", () => {
|
|
256
|
+
it("uses zero as current_cash when no accumulation exists", async () => {
|
|
257
|
+
const service = makeService()
|
|
258
|
+
service.listCmsAccumulations.mockResolvedValue([])
|
|
259
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_x" }])
|
|
260
|
+
|
|
261
|
+
await service.dayEnd({ store_id: "s1", employee_id: "emp_1", closing_amount: 300 })
|
|
262
|
+
|
|
263
|
+
expect(service.createPettyCashTransactions).toHaveBeenCalledWith([
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
opening_balance: 0,
|
|
266
|
+
closing_balance: 300,
|
|
267
|
+
difference_amount: 300,
|
|
268
|
+
}),
|
|
269
|
+
])
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe("CmsModuleService.handover - edge cases", () => {
|
|
274
|
+
it("creates new accumulation when none exists", async () => {
|
|
275
|
+
const service = makeService()
|
|
276
|
+
service.createCmsHandovers.mockResolvedValue([{ id: "cmsh_3", store_id: "s1", handover_amount: 100, type: "CR" }])
|
|
277
|
+
service.listCmsAccumulations.mockResolvedValue([])
|
|
278
|
+
service.createCmsAccumulations.mockResolvedValue([])
|
|
279
|
+
|
|
280
|
+
await service.handover({ store_id: "s1", employee_id: "emp_1", handover_amount: 100, type: "CR" })
|
|
281
|
+
|
|
282
|
+
expect(service.createCmsAccumulations).toHaveBeenCalledWith([
|
|
283
|
+
expect.objectContaining({ store_id: "s1", cash_in_store: 100 }),
|
|
284
|
+
])
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it("does not change cash when type is null", async () => {
|
|
288
|
+
const service = makeService()
|
|
289
|
+
service.createCmsHandovers.mockResolvedValue([{ id: "cmsh_4", store_id: "s1", handover_amount: 50, type: null }])
|
|
290
|
+
service.listCmsAccumulations.mockResolvedValue([{ id: "cmsa_1", store_id: "s1", cash_in_store: 1000 }])
|
|
291
|
+
service.updateCmsAccumulations.mockResolvedValue([])
|
|
292
|
+
|
|
293
|
+
await service.handover({ store_id: "s1", employee_id: "emp_1", handover_amount: 50, type: null })
|
|
294
|
+
|
|
295
|
+
expect(service.updateCmsAccumulations).toHaveBeenCalledWith([
|
|
296
|
+
expect.objectContaining({ cash_in_store: 1000 }),
|
|
297
|
+
])
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe("CmsModuleService.addPettyCash - update path", () => {
|
|
302
|
+
it("updates existing petty cash balance", async () => {
|
|
303
|
+
const service = makeService()
|
|
304
|
+
service.listPettyCashes.mockResolvedValue([{ id: "pc_1", store_id: "s1", balance: 200 }])
|
|
305
|
+
service.updatePettyCashes.mockResolvedValue([])
|
|
306
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_5" }])
|
|
307
|
+
|
|
308
|
+
await service.addPettyCash({ store_id: "s1", employee_id: "emp_1", amount: 300 })
|
|
309
|
+
|
|
310
|
+
expect(service.createPettyCashes).not.toHaveBeenCalled()
|
|
311
|
+
expect(service.updatePettyCashes).toHaveBeenCalledWith([
|
|
312
|
+
expect.objectContaining({ id: "pc_1", balance: 500 }),
|
|
313
|
+
])
|
|
314
|
+
expect(service.createPettyCashTransactions).toHaveBeenCalledWith([
|
|
315
|
+
expect.objectContaining({ opening_balance: 200, closing_balance: 500 }),
|
|
316
|
+
])
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe("CmsModuleService.addExpense - no existing petty cash", () => {
|
|
321
|
+
it("creates petty cash record when none exists", async () => {
|
|
322
|
+
const service = makeService()
|
|
323
|
+
service.listPettyCashes.mockResolvedValue([])
|
|
324
|
+
service.createPettyCashes.mockResolvedValue([{ id: "pc_new", store_id: "s1", balance: -100 }])
|
|
325
|
+
service.createPettyCashTransactions.mockResolvedValue([{ id: "pct_6" }])
|
|
326
|
+
|
|
327
|
+
await service.addExpense({ store_id: "s1", employee_id: "emp_1", amount: 100 })
|
|
328
|
+
|
|
329
|
+
expect(service.createPettyCashes).toHaveBeenCalledWith([
|
|
330
|
+
expect.objectContaining({ store_id: "s1", balance: -100 }),
|
|
331
|
+
])
|
|
332
|
+
})
|
|
333
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { CMS_PERMISSIONS } from "../permissions"
|
|
3
|
+
|
|
4
|
+
describe("CMS_PERMISSIONS", () => {
|
|
5
|
+
const EXPECTED_KEYS = [
|
|
6
|
+
"cms.handover.read",
|
|
7
|
+
"cms.handover.create",
|
|
8
|
+
"cms.shift.operate",
|
|
9
|
+
"cms.petty_cash.read",
|
|
10
|
+
"cms.petty_cash.operate",
|
|
11
|
+
"cms.accumulation.read",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
it("exports exactly 6 permission keys", () => {
|
|
15
|
+
expect(CMS_PERMISSIONS).toHaveLength(6)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("contains all required permission keys", () => {
|
|
19
|
+
const keys = CMS_PERMISSIONS.map((p) => p.key)
|
|
20
|
+
for (const expected of EXPECTED_KEYS) {
|
|
21
|
+
expect(keys).toContain(expected)
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("all permissions have registered_by set to 'cms'", () => {
|
|
26
|
+
for (const p of CMS_PERMISSIONS) {
|
|
27
|
+
expect(p.registered_by).toBe("cms")
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("all permissions have a non-empty description", () => {
|
|
32
|
+
for (const p of CMS_PERMISSIONS) {
|
|
33
|
+
expect(p.description.length).toBeGreaterThan(0)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CMS_MODULE = "cms"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Module } from "@medusajs/framework/utils"
|
|
2
|
+
import { CMS_MODULE } from "./constants"
|
|
3
|
+
import CmsModuleService from "./services/cms-module-service"
|
|
4
|
+
|
|
5
|
+
export default Module(CMS_MODULE, {
|
|
6
|
+
service: CmsModuleService,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
export { CMS_MODULE } from "./constants"
|
|
10
|
+
export { CmsModuleService } from "./services"
|
|
11
|
+
export type {
|
|
12
|
+
DayStartInput,
|
|
13
|
+
DayEndInput,
|
|
14
|
+
HandoverInput,
|
|
15
|
+
AddPettyCashInput,
|
|
16
|
+
AddExpenseInput,
|
|
17
|
+
ShiftLogFilters,
|
|
18
|
+
HandoverType,
|
|
19
|
+
TransactionType,
|
|
20
|
+
EntryType,
|
|
21
|
+
PettyCashSource,
|
|
22
|
+
} from "./services"
|
|
23
|
+
export { CMS_PERMISSIONS } from "./permissions"
|
|
24
|
+
export * as models from "./models"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
+
|
|
3
|
+
export class Migration20260622000000 extends Migration {
|
|
4
|
+
override async up(): Promise<void> {
|
|
5
|
+
this.addSql(
|
|
6
|
+
`create table if not exists "retailos_cms_handover" ("id" text not null, "store_id" text not null, "employee_id" text not null, "handover_id" text null, "handover_amount" integer null, "total_cash" integer null, "type" text null, "image_url" text null, "remark" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "retailos_cms_handover_pkey" primary key ("id"));`
|
|
7
|
+
)
|
|
8
|
+
this.addSql(
|
|
9
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_cms_handover_store_id" ON "retailos_cms_handover" ("store_id") WHERE deleted_at IS NULL;`
|
|
10
|
+
)
|
|
11
|
+
this.addSql(
|
|
12
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_cms_handover_employee_id" ON "retailos_cms_handover" ("employee_id") WHERE deleted_at IS NULL;`
|
|
13
|
+
)
|
|
14
|
+
this.addSql(
|
|
15
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_cms_handover_deleted_at" ON "retailos_cms_handover" ("deleted_at") WHERE deleted_at IS NULL;`
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
this.addSql(
|
|
19
|
+
`create table if not exists "retailos_cms_accumulation" ("id" text not null, "store_id" text not null, "cash_in_store" integer not null default 0, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "retailos_cms_accumulation_pkey" primary key ("id"));`
|
|
20
|
+
)
|
|
21
|
+
this.addSql(
|
|
22
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_retailos_cms_accumulation_store_id_unique" ON "retailos_cms_accumulation" ("store_id") WHERE deleted_at IS NULL;`
|
|
23
|
+
)
|
|
24
|
+
this.addSql(
|
|
25
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_cms_accumulation_deleted_at" ON "retailos_cms_accumulation" ("deleted_at") WHERE deleted_at IS NULL;`
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
this.addSql(
|
|
29
|
+
`create table if not exists "retailos_petty_cash" ("id" text not null, "store_id" text not null, "balance" integer not null default 0, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "retailos_petty_cash_pkey" primary key ("id"));`
|
|
30
|
+
)
|
|
31
|
+
this.addSql(
|
|
32
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_retailos_petty_cash_store_id_unique" ON "retailos_petty_cash" ("store_id") WHERE deleted_at IS NULL;`
|
|
33
|
+
)
|
|
34
|
+
this.addSql(
|
|
35
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_petty_cash_deleted_at" ON "retailos_petty_cash" ("deleted_at") WHERE deleted_at IS NULL;`
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
this.addSql(
|
|
39
|
+
`create table if not exists "retailos_petty_cash_transaction" ("id" text not null, "store_id" text not null, "employee_id" text not null, "transaction_type" text not null, "entry_type" text null, "source" text null, "amount" integer not null default 0, "opening_balance" integer null, "closing_balance" integer null, "expense_amount" integer not null default 0, "difference_amount" integer not null default 0, "category" text null, "sub_category" text null, "reason" text null, "image_url" text null, "date" timestamptz null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "retailos_petty_cash_transaction_pkey" primary key ("id"));`
|
|
40
|
+
)
|
|
41
|
+
this.addSql(
|
|
42
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_petty_cash_transaction_store_id" ON "retailos_petty_cash_transaction" ("store_id") WHERE deleted_at IS NULL;`
|
|
43
|
+
)
|
|
44
|
+
this.addSql(
|
|
45
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_petty_cash_transaction_type" ON "retailos_petty_cash_transaction" ("transaction_type") WHERE deleted_at IS NULL;`
|
|
46
|
+
)
|
|
47
|
+
this.addSql(
|
|
48
|
+
`CREATE INDEX IF NOT EXISTS "IDX_retailos_petty_cash_transaction_deleted_at" ON "retailos_petty_cash_transaction" ("deleted_at") WHERE deleted_at IS NULL;`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override async down(): Promise<void> {
|
|
53
|
+
this.addSql(`drop table if exists "retailos_petty_cash_transaction" cascade;`)
|
|
54
|
+
this.addSql(`drop table if exists "retailos_petty_cash" cascade;`)
|
|
55
|
+
this.addSql(`drop table if exists "retailos_cms_accumulation" cascade;`)
|
|
56
|
+
this.addSql(`drop table if exists "retailos_cms_handover" cascade;`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations"
|
|
2
|
+
|
|
3
|
+
export class Migration20260622000001 extends Migration {
|
|
4
|
+
override async up(): Promise<void> {
|
|
5
|
+
this.addSql(
|
|
6
|
+
`alter table if exists "retailos_cms_accumulation" add column if not exists "metadata" jsonb null;`
|
|
7
|
+
)
|
|
8
|
+
this.addSql(
|
|
9
|
+
`alter table if exists "retailos_petty_cash" add column if not exists "metadata" jsonb null;`
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async down(): Promise<void> {
|
|
14
|
+
this.addSql(`alter table if exists "retailos_cms_accumulation" drop column if exists "metadata";`)
|
|
15
|
+
this.addSql(`alter table if exists "retailos_petty_cash" drop column if exists "metadata";`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { model } from "@medusajs/framework/utils"
|
|
2
|
+
|
|
3
|
+
const CmsAccumulation = model.define("retailos_cms_accumulation", {
|
|
4
|
+
id: model.id({ prefix: "cmsa" }).primaryKey(),
|
|
5
|
+
store_id: model.text().searchable(),
|
|
6
|
+
cash_in_store: model.number(),
|
|
7
|
+
metadata: model.json().nullable(),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export default CmsAccumulation
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { model } from "@medusajs/framework/utils"
|
|
2
|
+
|
|
3
|
+
const CmsHandover = model.define("retailos_cms_handover", {
|
|
4
|
+
id: model.id({ prefix: "cmsh" }).primaryKey(),
|
|
5
|
+
store_id: model.text().searchable(),
|
|
6
|
+
employee_id: model.text().searchable(),
|
|
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
|
+
handover_amount: model.number().nullable(),
|
|
9
|
+
total_cash: model.number().nullable(),
|
|
10
|
+
type: model.text().nullable(),
|
|
11
|
+
image_url: model.text().nullable(),
|
|
12
|
+
remark: model.text().nullable(),
|
|
13
|
+
metadata: model.json().nullable(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default CmsHandover
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { model } from "@medusajs/framework/utils"
|
|
2
|
+
|
|
3
|
+
const PettyCashTransaction = model.define("retailos_petty_cash_transaction", {
|
|
4
|
+
id: model.id({ prefix: "pct" }).primaryKey(),
|
|
5
|
+
store_id: model.text().searchable(),
|
|
6
|
+
employee_id: model.text().searchable(),
|
|
7
|
+
transaction_type: model.text(),
|
|
8
|
+
entry_type: model.text().nullable(),
|
|
9
|
+
source: model.text().nullable(),
|
|
10
|
+
amount: model.number(),
|
|
11
|
+
opening_balance: model.number().nullable(),
|
|
12
|
+
closing_balance: model.number().nullable(),
|
|
13
|
+
expense_amount: model.number(),
|
|
14
|
+
difference_amount: model.number(),
|
|
15
|
+
category: model.text().nullable(),
|
|
16
|
+
sub_category: model.text().nullable(),
|
|
17
|
+
reason: model.text().nullable(),
|
|
18
|
+
image_url: model.text().nullable(),
|
|
19
|
+
date: model.dateTime().nullable(),
|
|
20
|
+
metadata: model.json().nullable(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export default PettyCashTransaction
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { model } from "@medusajs/framework/utils"
|
|
2
|
+
|
|
3
|
+
const PettyCash = model.define("retailos_petty_cash", {
|
|
4
|
+
id: model.id({ prefix: "pc" }).primaryKey(),
|
|
5
|
+
store_id: model.text().searchable(),
|
|
6
|
+
balance: model.number(),
|
|
7
|
+
metadata: model.json().nullable(),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export default PettyCash
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PermissionRegistration } from "@devx-retailos/core"
|
|
2
|
+
|
|
3
|
+
export const CMS_PERMISSIONS: PermissionRegistration[] = [
|
|
4
|
+
{
|
|
5
|
+
key: "cms.handover.read",
|
|
6
|
+
description: "View cash handover records",
|
|
7
|
+
registered_by: "cms",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
key: "cms.handover.create",
|
|
11
|
+
description: "Create a cash handover record",
|
|
12
|
+
registered_by: "cms",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: "cms.shift.operate",
|
|
16
|
+
description: "Open and close shift (day start / day end)",
|
|
17
|
+
registered_by: "cms",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: "cms.petty_cash.read",
|
|
21
|
+
description: "View petty cash balance and transactions",
|
|
22
|
+
registered_by: "cms",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "cms.petty_cash.operate",
|
|
26
|
+
description: "Add petty cash or record expenses",
|
|
27
|
+
registered_by: "cms",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "cms.accumulation.read",
|
|
31
|
+
description: "View current cash accumulation for a store",
|
|
32
|
+
registered_by: "cms",
|
|
33
|
+
},
|
|
34
|
+
]
|