@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.
Files changed (27) hide show
  1. package/.medusa/server/src/api/admin/retailos/cms/export/route.js +2 -2
  2. package/.medusa/server/src/api/admin/retailos/cms/handovers/[id]/route.js +2 -2
  3. package/.medusa/server/src/api/admin/retailos/cms/handovers/route.js +72 -7
  4. package/.medusa/server/src/api/admin/retailos/cms/petty-cash/route.js +35 -4
  5. package/.medusa/server/src/api/admin/retailos/cms/reconciliation/route.js +42 -0
  6. package/.medusa/server/src/modules/cms/migrations/Migration20260626000000.js +16 -0
  7. package/.medusa/server/src/modules/cms/models/cms-handover.js +3 -1
  8. package/.medusa/server/src/modules/cms/permissions.js +6 -1
  9. package/.medusa/server/src/modules/cms/services/cms-module-service.js +108 -38
  10. package/README.md +116 -0
  11. package/package.json +1 -1
  12. package/src/api/admin/retailos/cms/export/__tests__/route.test.ts +9 -9
  13. package/src/api/admin/retailos/cms/export/route.ts +1 -1
  14. package/src/api/admin/retailos/cms/handovers/[id]/__tests__/route.test.ts +5 -5
  15. package/src/api/admin/retailos/cms/handovers/[id]/route.ts +1 -1
  16. package/src/api/admin/retailos/cms/handovers/__tests__/route.test.ts +22 -11
  17. package/src/api/admin/retailos/cms/handovers/route.ts +78 -6
  18. package/src/api/admin/retailos/cms/petty-cash/__tests__/route.test.ts +18 -12
  19. package/src/api/admin/retailos/cms/petty-cash/route.ts +46 -4
  20. package/src/api/admin/retailos/cms/reconciliation/__tests__/route.test.ts +124 -0
  21. package/src/api/admin/retailos/cms/reconciliation/route.ts +47 -0
  22. package/src/modules/cms/__tests__/cms-module-service.test.ts +219 -78
  23. package/src/modules/cms/__tests__/permissions.test.ts +3 -2
  24. package/src/modules/cms/migrations/Migration20260626000000.ts +17 -0
  25. package/src/modules/cms/models/cms-handover.ts +2 -0
  26. package/src/modules/cms/permissions.ts +5 -0
  27. package/src/modules/cms/services/cms-module-service.ts +163 -37
package/README.md CHANGED
@@ -126,8 +126,21 @@ const logs = await cmsService.getShiftLogs({
126
126
  to: new Date("2024-01-31"),
127
127
  limit: 50,
128
128
  })
129
+
130
+ // Daily cash reconciliation report
131
+ const report = await cmsService.getReconciliationReport({
132
+ store_id: "store_01",
133
+ date: "2026-06-23", // YYYY-MM-DD (UTC)
134
+ net_cash_sales: 28450, // optional — cash collected from orders (default 0)
135
+ })
136
+ // report.expected_closing_balance = opening + net_cash_sales
137
+ // + petty_cash_in - petty_cash_out + total_handovers_in - total_handovers_out
138
+ // report.variance = closing_balance - expected (null until day_end runs)
139
+ // report.is_balanced = variance === 0
129
140
  ```
130
141
 
142
+ > **`net_cash_sales`** comes from your order module. Pass it from the brand's order query or via `sdk-client`. When omitted the report uses 0 and computes everything from CMS data only.
143
+
131
144
  ---
132
145
 
133
146
  ## Permissions
@@ -140,6 +153,109 @@ const logs = await cmsService.getShiftLogs({
140
153
  | `cms.petty_cash.read` | View petty cash balance and transactions |
141
154
  | `cms.petty_cash.operate` | Add petty cash or record expenses |
142
155
  | `cms.accumulation.read` | View current cash accumulation for a store |
156
+ | `cms.reconciliation.read` | View daily cash reconciliation report with variance analysis |
157
+
158
+ ---
159
+
160
+ ## HTTP API
161
+
162
+ All routes are mounted under `/admin/retailos/cms` and require a valid Medusa admin session.
163
+
164
+ ### Handovers
165
+
166
+ | Method | Path | Description |
167
+ |--------|------|-------------|
168
+ | `GET` | `/admin/retailos/cms/handovers` | List handovers with filtering, pagination, and linked employee/store data |
169
+ | `POST` | `/admin/retailos/cms/handovers` | Create a cash handover |
170
+ | `GET` | `/admin/retailos/cms/handovers/:id` | Retrieve a single handover by ID |
171
+
172
+ **GET /admin/retailos/cms/handovers** query params:
173
+
174
+ | Param | Type | Description |
175
+ |-------|------|-------------|
176
+ | `store_id` | string | Filter by store |
177
+ | `employee_id` | string | Filter by employee |
178
+ | `type` | `CR` \| `DB` | Filter by direction |
179
+ | `handover_id` | string | Exact match on handover_id |
180
+ | `q` | string | Full-text search on handover_id (`ILIKE`) |
181
+ | `created_at` | string / object | Date range filter |
182
+ | `limit` | number | Max 500, default 20 |
183
+ | `offset` | number | Default 0 |
184
+ | `order` | string | Field name; prefix `-` for DESC (e.g. `-created_at`) |
185
+
186
+ Response includes `handovers[]` and `count`. Each handover carries an embedded `employee` (`id`, `first_name`, `last_name`) and `store` (`id`, `name`, `store_code`) when module links resolve.
187
+
188
+ **POST /admin/retailos/cms/handovers** body (Zod-validated):
189
+
190
+ ```json
191
+ {
192
+ "store_id": "store_01",
193
+ "employee_id": "emp_01",
194
+ "handover_amount": 2000,
195
+ "type": "CR",
196
+ "remark": "shift change",
197
+ "handover_id": null,
198
+ "image_url": null,
199
+ "metadata": null
200
+ }
201
+ ```
202
+
203
+ ---
204
+
205
+ ### Petty cash
206
+
207
+ | Method | Path | Description |
208
+ |--------|------|-------------|
209
+ | `GET` | `/admin/retailos/cms/petty-cash` | List petty cash transactions with employee enrichment |
210
+
211
+ Query params: `store_id`, `limit` (max 500, default 100), `offset`.
212
+
213
+ Each transaction includes an embedded `employee` object when the `employee` module is available.
214
+
215
+ ---
216
+
217
+ ### Reconciliation
218
+
219
+ | Method | Path | Description |
220
+ |--------|------|-------------|
221
+ | `GET` | `/admin/retailos/cms/reconciliation` | Daily cash reconciliation report |
222
+
223
+ Required params: `store_id`, `date` (YYYY-MM-DD). Optional: `net_cash_sales` (number).
224
+
225
+ ---
226
+
227
+ ### Export
228
+
229
+ | Method | Path | Description |
230
+ |--------|------|-------------|
231
+ | `GET` | `/admin/retailos/cms/export` | Download all handovers as a CSV file |
232
+
233
+ Query params: `store_id` (optional). Returns `text/csv` with `Content-Disposition: attachment`.
234
+
235
+ ---
236
+
237
+ ## sdk-client
238
+
239
+ The `cms` namespace on `RetailOSClient` exposes typed wrappers for all routes above. Key methods:
240
+
241
+ ```ts
242
+ // Reconciliation report
243
+ const report = await client.cms.getReconciliationReport({
244
+ store_id: "store_01",
245
+ date: "2026-06-26",
246
+ net_cash_sales: 28450,
247
+ })
248
+
249
+ // List handovers (paginated)
250
+ const { handovers, count } = await client.cms.listHandovers({
251
+ store_id: "store_01",
252
+ limit: 20,
253
+ offset: 0,
254
+ })
255
+
256
+ // Export CSV (returns raw text)
257
+ const csv = await client.cms.exportHandovers({ store_id: "store_01" })
258
+ ```
143
259
 
144
260
  ---
145
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devx-retailos/cms",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Cash Management System for retailOS: shift ops, handovers, petty cash, accumulation.",
5
5
  "license": "MIT",
6
6
  "main": "./.medusa/server/src/modules/cms/index.js",
@@ -3,7 +3,7 @@ import { GET } from "../route"
3
3
 
4
4
  function makeService() {
5
5
  return {
6
- listAndCountCmsHandovers: vi.fn().mockResolvedValue([[], 0]),
6
+ listAndCountRetailosCmsHandovers: vi.fn().mockResolvedValue([[], 0]),
7
7
  }
8
8
  }
9
9
 
@@ -34,7 +34,7 @@ function makeRes() {
34
34
  describe("GET /export", () => {
35
35
  it("returns CSV with correct headers row", async () => {
36
36
  const service = makeService()
37
- service.listAndCountCmsHandovers.mockResolvedValue([[], 0])
37
+ service.listAndCountRetailosCmsHandovers.mockResolvedValue([[], 0])
38
38
  const req = makeReq({}, service)
39
39
  const res = makeRes()
40
40
 
@@ -55,7 +55,7 @@ describe("GET /export", () => {
55
55
  const handovers = [
56
56
  { id: "cmsh_1", store_id: "s1", employee_id: "e1", handover_id: null, handover_amount: 500, total_cash: null, type: "CR", remark: null, created_at: "2026-06-22T00:00:00Z" },
57
57
  ]
58
- service.listAndCountCmsHandovers.mockResolvedValue([handovers, 1])
58
+ service.listAndCountRetailosCmsHandovers.mockResolvedValue([handovers, 1])
59
59
  const req = makeReq({}, service)
60
60
  const res = makeRes()
61
61
 
@@ -74,7 +74,7 @@ describe("GET /export", () => {
74
74
  const handovers = [
75
75
  { id: "cmsh_2", store_id: "s1", employee_id: "e1", handover_id: null, handover_amount: 100, total_cash: null, type: "DB", remark: 'cash, "extra"', created_at: "2026-06-22T00:00:00Z" },
76
76
  ]
77
- service.listAndCountCmsHandovers.mockResolvedValue([handovers, 1])
77
+ service.listAndCountRetailosCmsHandovers.mockResolvedValue([handovers, 1])
78
78
  const req = makeReq({}, service)
79
79
  const res = makeRes()
80
80
 
@@ -86,13 +86,13 @@ describe("GET /export", () => {
86
86
 
87
87
  it("filters by store_id when provided in query", async () => {
88
88
  const service = makeService()
89
- service.listAndCountCmsHandovers.mockResolvedValue([[], 0])
89
+ service.listAndCountRetailosCmsHandovers.mockResolvedValue([[], 0])
90
90
  const req = makeReq({ store_id: "s1" }, service)
91
91
  const res = makeRes()
92
92
 
93
93
  await GET(req as any, res as any)
94
94
 
95
- expect(service.listAndCountCmsHandovers).toHaveBeenCalledWith(
95
+ expect(service.listAndCountRetailosCmsHandovers).toHaveBeenCalledWith(
96
96
  expect.objectContaining({ store_id: ["s1"] }),
97
97
  expect.any(Object)
98
98
  )
@@ -100,13 +100,13 @@ describe("GET /export", () => {
100
100
 
101
101
  it("does not filter by store_id when not in query", async () => {
102
102
  const service = makeService()
103
- service.listAndCountCmsHandovers.mockResolvedValue([[], 0])
103
+ service.listAndCountRetailosCmsHandovers.mockResolvedValue([[], 0])
104
104
  const req = makeReq({}, service)
105
105
  const res = makeRes()
106
106
 
107
107
  await GET(req as any, res as any)
108
108
 
109
- expect(service.listAndCountCmsHandovers).toHaveBeenCalledWith(
109
+ expect(service.listAndCountRetailosCmsHandovers).toHaveBeenCalledWith(
110
110
  expect.not.objectContaining({ store_id: expect.anything() }),
111
111
  expect.any(Object)
112
112
  )
@@ -114,7 +114,7 @@ describe("GET /export", () => {
114
114
 
115
115
  it("returns 500 when service throws", async () => {
116
116
  const service = makeService()
117
- service.listAndCountCmsHandovers.mockRejectedValue(new Error("db error"))
117
+ service.listAndCountRetailosCmsHandovers.mockRejectedValue(new Error("db error"))
118
118
  const req = makeReq({}, service)
119
119
  const res = makeRes()
120
120
 
@@ -35,7 +35,7 @@ export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse)
35
35
  const filters: Record<string, unknown> = {}
36
36
  if (store_id) filters.store_id = [store_id]
37
37
 
38
- const [handovers] = await service.listAndCountCmsHandovers(filters, {
38
+ const [handovers] = await service.listAndCountRetailosCmsHandovers(filters, {
39
39
  take: 10000,
40
40
  order: { created_at: "DESC" },
41
41
  })
@@ -3,7 +3,7 @@ import { GET } from "../route"
3
3
 
4
4
  function makeService() {
5
5
  return {
6
- retrieveCmsHandover: vi.fn(),
6
+ retrieveRetailosCmsHandover: vi.fn(),
7
7
  }
8
8
  }
9
9
 
@@ -31,20 +31,20 @@ describe("GET /handovers/[id]", () => {
31
31
  it("returns handover when found", async () => {
32
32
  const service = makeService()
33
33
  const handover = { id: "cmsh_1", store_id: "s1", handover_amount: 500 }
34
- service.retrieveCmsHandover.mockResolvedValue(handover)
34
+ service.retrieveRetailosCmsHandover.mockResolvedValue(handover)
35
35
  const req = makeReq({ id: "cmsh_1" }, service)
36
36
  const res = makeRes()
37
37
 
38
38
  await GET(req as any, res as any)
39
39
 
40
- expect(service.retrieveCmsHandover).toHaveBeenCalledWith("cmsh_1")
40
+ expect(service.retrieveRetailosCmsHandover).toHaveBeenCalledWith("cmsh_1")
41
41
  expect(res.status).toHaveBeenCalledWith(200)
42
42
  expect(res.json).toHaveBeenCalledWith({ handover })
43
43
  })
44
44
 
45
45
  it("returns 404 when handover is null", async () => {
46
46
  const service = makeService()
47
- service.retrieveCmsHandover.mockResolvedValue(null)
47
+ service.retrieveRetailosCmsHandover.mockResolvedValue(null)
48
48
  const req = makeReq({ id: "cmsh_missing" }, service)
49
49
  const res = makeRes()
50
50
 
@@ -56,7 +56,7 @@ describe("GET /handovers/[id]", () => {
56
56
 
57
57
  it("returns 500 when service throws", async () => {
58
58
  const service = makeService()
59
- service.retrieveCmsHandover.mockRejectedValue(new Error("db error"))
59
+ service.retrieveRetailosCmsHandover.mockRejectedValue(new Error("db error"))
60
60
  const req = makeReq({ id: "cmsh_1" }, service)
61
61
  const res = makeRes()
62
62
 
@@ -21,7 +21,7 @@ export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse)
21
21
  const { id } = req.params
22
22
  try {
23
23
  const service = getService(req)
24
- const handover = await service.retrieveCmsHandover(id)
24
+ const handover = await service.retrieveRetailosCmsHandover(id)
25
25
  if (!handover) {
26
26
  return res.status(404).json({ code: "NOT_FOUND", message: "Handover not found" })
27
27
  }
@@ -3,18 +3,24 @@ import { GET, POST } from "../route"
3
3
 
4
4
  function makeService() {
5
5
  return {
6
- listAndCountCmsHandovers: vi.fn().mockResolvedValue([[], 0]),
7
6
  handover: vi.fn().mockResolvedValue({ id: "cmsh_1", store_id: "s1", handover_amount: 100 }),
8
7
  }
9
8
  }
10
9
 
11
- function makeReq(overrides: Record<string, unknown> = {}, service = makeService()) {
10
+ function makeQueryModule(data: unknown[] = [], count = 0) {
11
+ return {
12
+ graph: vi.fn().mockResolvedValue({ data, metadata: { count } }),
13
+ }
14
+ }
15
+
16
+ function makeReq(overrides: Record<string, unknown> = {}, service = makeService(), queryModule = makeQueryModule()) {
12
17
  return {
13
18
  body: {},
14
- filterableFields: {},
19
+ query: {},
15
20
  scope: {
16
21
  resolve: vi.fn().mockImplementation((key: string) => {
17
22
  if (key === "cms") return service
23
+ if (key === "query") return queryModule
18
24
  throw new Error("not found")
19
25
  }),
20
26
  },
@@ -32,22 +38,27 @@ function makeRes() {
32
38
 
33
39
  describe("GET /handovers", () => {
34
40
  it("returns handover list with count", async () => {
41
+ const data = [{ id: "cmsh_1" }, { id: "cmsh_2" }]
42
+ const queryModule = makeQueryModule(data, 2)
35
43
  const service = makeService()
36
- const handovers = [{ id: "cmsh_1" }, { id: "cmsh_2" }]
37
- service.listAndCountCmsHandovers.mockResolvedValue([handovers, 2])
38
- const req = makeReq({}, service)
44
+ const req = makeReq({}, service, queryModule)
39
45
  const res = makeRes()
40
46
 
41
47
  await GET(req as any, res as any)
42
48
 
43
49
  expect(res.status).toHaveBeenCalledWith(200)
44
- expect(res.json).toHaveBeenCalledWith({ handovers, count: 2 })
50
+ expect(res.json).toHaveBeenCalledWith({
51
+ handovers: [
52
+ { id: "cmsh_1", employee: null, store: null },
53
+ { id: "cmsh_2", employee: null, store: null },
54
+ ],
55
+ count: 2,
56
+ })
45
57
  })
46
58
 
47
- it("returns 500 when service throws", async () => {
48
- const service = makeService()
49
- service.listAndCountCmsHandovers.mockRejectedValue(new Error("db error"))
50
- const req = makeReq({}, service)
59
+ it("returns 500 when query.graph throws", async () => {
60
+ const queryModule = { graph: vi.fn().mockRejectedValue(new Error("db error")) }
61
+ const req = makeReq({}, makeService(), queryModule)
51
62
  const res = makeRes()
52
63
 
53
64
  await GET(req as any, res as any)
@@ -2,6 +2,7 @@ import type {
2
2
  AuthenticatedMedusaRequest,
3
3
  MedusaResponse,
4
4
  } from "@medusajs/framework"
5
+ import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
5
6
  import { z } from "zod"
6
7
  import { CMS_MODULE } from "../../../../../modules/cms/constants"
7
8
  import type CmsModuleService from "../../../../../modules/cms/services/cms-module-service"
@@ -32,13 +33,84 @@ function getLogger(req: AuthenticatedMedusaRequest) {
32
33
 
33
34
  export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
34
35
  try {
35
- const service = getService(req)
36
- const filters = (req.filterableFields ?? {}) as Record<string, unknown>
37
- const [data, count] = await service.listAndCountCmsHandovers(filters, {
38
- take: 100,
39
- order: { created_at: "DESC" },
36
+ const query = (req as any).scope.resolve(ContainerRegistrationKeys.QUERY)
37
+ const q = req.query as Record<string, any>
38
+
39
+ const filters: Record<string, unknown> = {}
40
+
41
+ // Direct equality filters from query params
42
+ const DIRECT = ["store_id", "employee_id", "type"] as const
43
+ for (const key of DIRECT) {
44
+ if (q[key] !== undefined && q[key] !== "") filters[key] = q[key]
45
+ }
46
+ if (q.handover_id) filters.handover_id = q.handover_id
47
+
48
+ // q: full-text search on handover_id
49
+ if (q.q) {
50
+ filters.handover_id = { $ilike: `%${q.q}%` }
51
+ }
52
+
53
+ // Date range filters
54
+ if (q.created_at) {
55
+ filters.created_at = typeof q.created_at === "object" ? q.created_at : { $gte: new Date(q.created_at as string) }
56
+ }
57
+ if (q.updated_at) {
58
+ filters.updated_at = typeof q.updated_at === "object" ? q.updated_at : { $gte: new Date(q.updated_at as string) }
59
+ }
60
+
61
+ // Pagination
62
+ const limit = q.limit ? Math.min(parseInt(q.limit as string, 10), 500) : 20
63
+ const offset = q.offset ? parseInt(q.offset as string, 10) : 0
64
+
65
+ // Ordering: "field" = ASC, "-field" = DESC
66
+ let order: Record<string, "ASC" | "DESC"> = { created_at: "DESC" }
67
+ if (q.order) {
68
+ const raw = q.order as string
69
+ const isDesc = raw.startsWith("-")
70
+ const field = isDesc ? raw.slice(1) : raw
71
+ const SORTABLE = ["created_at", "updated_at", "handover_amount", "total_cash"]
72
+ if (SORTABLE.includes(field)) {
73
+ order = { [field]: isDesc ? "DESC" : "ASC" }
74
+ }
75
+ }
76
+
77
+ const { data, metadata } = await query.graph({
78
+ entity: "retailos_cms_handover",
79
+ fields: [
80
+ "*",
81
+ "retailos_employee.id",
82
+ "retailos_employee.first_name",
83
+ "retailos_employee.last_name",
84
+ "retailos_employee.email",
85
+ "retailos_store.id",
86
+ "retailos_store.name",
87
+ "retailos_store.store_code",
88
+ ],
89
+ filters: filters as any,
90
+ pagination: { skip: offset, take: limit },
91
+ // @ts-ignore
92
+ order,
40
93
  })
41
- return res.status(200).json({ handovers: data, count })
94
+
95
+ const handovers = (data as any[]).map((h: any) => ({
96
+ ...h,
97
+ employee: h.retailos_employee
98
+ ? {
99
+ id: h.retailos_employee.id,
100
+ first_name: h.retailos_employee.first_name,
101
+ last_name: h.retailos_employee.last_name,
102
+ }
103
+ : null,
104
+ store: h.retailos_store
105
+ ? {
106
+ id: h.retailos_store.id,
107
+ name: h.retailos_store.name,
108
+ store_code: h.retailos_store.store_code,
109
+ }
110
+ : null,
111
+ }))
112
+
113
+ return res.status(200).json({ handovers, count: (metadata as any)?.count ?? handovers.length })
42
114
  } catch (err) {
43
115
  getLogger(req).error("[retailos/cms] list handovers failed", {
44
116
  error: err instanceof Error ? err.message : String(err),
@@ -3,13 +3,13 @@ import { GET } from "../route"
3
3
 
4
4
  function makeService() {
5
5
  return {
6
- listAndCountPettyCashTransactions: vi.fn().mockResolvedValue([[], 0]),
6
+ listAndCountRetailosPettyCashTransactions: vi.fn().mockResolvedValue([[], 0]),
7
7
  }
8
8
  }
9
9
 
10
- function makeReq(filterableFields: Record<string, unknown> = {}, service = makeService()) {
10
+ function makeReq(query: Record<string, unknown> = {}, service = makeService()) {
11
11
  return {
12
- filterableFields,
12
+ query,
13
13
  scope: {
14
14
  resolve: vi.fn().mockImplementation((key: string) => {
15
15
  if (key === "cms") return service
@@ -28,32 +28,38 @@ function makeRes() {
28
28
  }
29
29
 
30
30
  describe("GET /petty-cash", () => {
31
- it("returns transactions and count", async () => {
31
+ it("returns petty cash transactions and count", async () => {
32
32
  const service = makeService()
33
33
  const transactions = [{ id: "pct_1" }, { id: "pct_2" }]
34
- service.listAndCountPettyCashTransactions.mockResolvedValue([transactions, 2])
34
+ service.listAndCountRetailosPettyCashTransactions.mockResolvedValue([transactions, 2])
35
35
  const req = makeReq({}, service)
36
36
  const res = makeRes()
37
37
 
38
38
  await GET(req as any, res as any)
39
39
 
40
- expect(service.listAndCountPettyCashTransactions).toHaveBeenCalledWith(
40
+ expect(service.listAndCountRetailosPettyCashTransactions).toHaveBeenCalledWith(
41
41
  expect.objectContaining({ transaction_type: ["open", "close", "petty_cash"] }),
42
42
  expect.objectContaining({ take: 100 })
43
43
  )
44
44
  expect(res.status).toHaveBeenCalledWith(200)
45
- expect(res.json).toHaveBeenCalledWith({ transactions, count: 2 })
45
+ expect(res.json).toHaveBeenCalledWith({
46
+ petty_cash: [
47
+ { id: "pct_1", employee: null },
48
+ { id: "pct_2", employee: null },
49
+ ],
50
+ count: 2,
51
+ })
46
52
  })
47
53
 
48
- it("merges filterableFields with transaction_type filter", async () => {
54
+ it("includes store_id in filter when provided", async () => {
49
55
  const service = makeService()
50
- service.listAndCountPettyCashTransactions.mockResolvedValue([[], 0])
51
- const req = makeReq({ store_id: ["s1"] }, service)
56
+ service.listAndCountRetailosPettyCashTransactions.mockResolvedValue([[], 0])
57
+ const req = makeReq({ store_id: "s1" }, service)
52
58
  const res = makeRes()
53
59
 
54
60
  await GET(req as any, res as any)
55
61
 
56
- expect(service.listAndCountPettyCashTransactions).toHaveBeenCalledWith(
62
+ expect(service.listAndCountRetailosPettyCashTransactions).toHaveBeenCalledWith(
57
63
  expect.objectContaining({ store_id: ["s1"], transaction_type: ["open", "close", "petty_cash"] }),
58
64
  expect.any(Object)
59
65
  )
@@ -61,7 +67,7 @@ describe("GET /petty-cash", () => {
61
67
 
62
68
  it("returns 500 when service throws", async () => {
63
69
  const service = makeService()
64
- service.listAndCountPettyCashTransactions.mockRejectedValue(new Error("db error"))
70
+ service.listAndCountRetailosPettyCashTransactions.mockRejectedValue(new Error("db error"))
65
71
  const req = makeReq({}, service)
66
72
  const res = makeRes()
67
73
 
@@ -5,6 +5,8 @@ import type {
5
5
  import { CMS_MODULE } from "../../../../../modules/cms/constants"
6
6
  import type CmsModuleService from "../../../../../modules/cms/services/cms-module-service"
7
7
 
8
+ const EMPLOYEE_MODULE = "employee"
9
+
8
10
  function getService(req: AuthenticatedMedusaRequest): CmsModuleService {
9
11
  return (req as any).scope.resolve(CMS_MODULE)
10
12
  }
@@ -20,12 +22,52 @@ function getLogger(req: AuthenticatedMedusaRequest) {
20
22
  export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
21
23
  try {
22
24
  const service = getService(req)
23
- const filters = (req.filterableFields ?? {}) as Record<string, unknown>
24
- const [transactions, count] = await service.listAndCountPettyCashTransactions(
25
+ const q = req.query as Record<string, any>
26
+
27
+ const filters: Record<string, unknown> = {}
28
+ if (q.store_id) filters.store_id = [q.store_id]
29
+
30
+ const limit = q.limit ? Math.min(parseInt(q.limit as string, 10), 500) : 100
31
+ const offset = q.offset ? parseInt(q.offset as string, 10) : 0
32
+
33
+ const [transactions, count] = await service.listAndCountRetailosPettyCashTransactions(
25
34
  { ...filters, transaction_type: ["open", "close", "petty_cash"] },
26
- { take: 100, order: { created_at: "DESC" } }
35
+ { take: limit, skip: offset, order: { created_at: "DESC" } } as any
27
36
  )
28
- return res.status(200).json({ transactions, count })
37
+
38
+ // Enrich with employee data via bulk lookup
39
+ const employeeIds = [...new Set(
40
+ transactions.map((t: any) => t.employee_id).filter(Boolean)
41
+ )] as string[]
42
+
43
+ let employeeMap = new Map<string, { id: string; first_name: string; last_name: string }>()
44
+ if (employeeIds.length) {
45
+ try {
46
+ const employeeService = (req as any).scope.resolve(EMPLOYEE_MODULE)
47
+ const employees = await employeeService.listEmployees(
48
+ { id: employeeIds },
49
+ { select: ["id", "first_name", "last_name"] }
50
+ )
51
+ employeeMap = new Map(employees.map((e: any) => [e.id, e]))
52
+ } catch {
53
+ // employee module unavailable — return without enrichment
54
+ }
55
+ }
56
+
57
+ const petty_cash = transactions.map((t: any) => ({
58
+ ...t,
59
+ employee: t.employee_id
60
+ ? (employeeMap.get(t.employee_id)
61
+ ? {
62
+ id: employeeMap.get(t.employee_id)!.id,
63
+ first_name: employeeMap.get(t.employee_id)!.first_name,
64
+ last_name: employeeMap.get(t.employee_id)!.last_name,
65
+ }
66
+ : null)
67
+ : null,
68
+ }))
69
+
70
+ return res.status(200).json({ petty_cash, count })
29
71
  } catch (err) {
30
72
  getLogger(req).error("[retailos/cms] list petty cash failed", {
31
73
  error: err instanceof Error ? err.message : String(err),