@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.
Files changed (56) hide show
  1. package/.medusa/server/src/admin/index.js +23 -0
  2. package/.medusa/server/src/admin/index.mjs +24 -0
  3. package/.medusa/server/src/api/admin/retailos/cms/accumulation/[storeId]/route.js +34 -0
  4. package/.medusa/server/src/api/admin/retailos/cms/export/route.js +52 -0
  5. package/.medusa/server/src/api/admin/retailos/cms/handovers/[id]/route.js +35 -0
  6. package/.medusa/server/src/api/admin/retailos/cms/handovers/route.js +78 -0
  7. package/.medusa/server/src/api/admin/retailos/cms/operation/route.js +124 -0
  8. package/.medusa/server/src/api/admin/retailos/cms/petty-cash/route.js +31 -0
  9. package/.medusa/server/src/api/admin/retailos/cms/shift-logs/route.js +36 -0
  10. package/.medusa/server/src/links/employee-to-cms-handover.js +13 -0
  11. package/.medusa/server/src/links/store-to-cms-handover.js +13 -0
  12. package/.medusa/server/src/modules/cms/constants.js +5 -0
  13. package/.medusa/server/src/modules/cms/index.js +53 -0
  14. package/.medusa/server/src/modules/cms/migrations/Migration20260622000000.js +30 -0
  15. package/.medusa/server/src/modules/cms/migrations/Migration20260622000001.js +16 -0
  16. package/.medusa/server/src/modules/cms/models/cms-accumulation.js +11 -0
  17. package/.medusa/server/src/modules/cms/models/cms-handover.js +17 -0
  18. package/.medusa/server/src/modules/cms/models/index.js +15 -0
  19. package/.medusa/server/src/modules/cms/models/petty-cash-transaction.js +24 -0
  20. package/.medusa/server/src/modules/cms/models/petty-cash.js +11 -0
  21. package/.medusa/server/src/modules/cms/permissions.js +36 -0
  22. package/.medusa/server/src/modules/cms/services/cms-module-service.js +210 -0
  23. package/.medusa/server/src/modules/cms/services/index.js +9 -0
  24. package/README.md +155 -0
  25. package/package.json +60 -0
  26. package/src/admin/.gitkeep +0 -0
  27. package/src/api/admin/retailos/cms/accumulation/[storeId]/__tests__/route.test.ts +68 -0
  28. package/src/api/admin/retailos/cms/accumulation/[storeId]/route.ts +35 -0
  29. package/src/api/admin/retailos/cms/export/__tests__/route.test.ts +126 -0
  30. package/src/api/admin/retailos/cms/export/route.ts +58 -0
  31. package/src/api/admin/retailos/cms/handovers/[id]/__tests__/route.test.ts +68 -0
  32. package/src/api/admin/retailos/cms/handovers/[id]/route.ts +36 -0
  33. package/src/api/admin/retailos/cms/handovers/__tests__/route.test.ts +104 -0
  34. package/src/api/admin/retailos/cms/handovers/route.ts +79 -0
  35. package/src/api/admin/retailos/cms/operation/__tests__/route.test.ts +169 -0
  36. package/src/api/admin/retailos/cms/operation/route.ts +130 -0
  37. package/src/api/admin/retailos/cms/petty-cash/__tests__/route.test.ts +73 -0
  38. package/src/api/admin/retailos/cms/petty-cash/route.ts +35 -0
  39. package/src/api/admin/retailos/cms/shift-logs/__tests__/route.test.ts +77 -0
  40. package/src/api/admin/retailos/cms/shift-logs/route.ts +38 -0
  41. package/src/links/employee-to-cms-handover.ts +11 -0
  42. package/src/links/store-to-cms-handover.ts +11 -0
  43. package/src/modules/cms/__tests__/cms-module-service.test.ts +333 -0
  44. package/src/modules/cms/__tests__/permissions.test.ts +36 -0
  45. package/src/modules/cms/constants.ts +1 -0
  46. package/src/modules/cms/index.ts +24 -0
  47. package/src/modules/cms/migrations/Migration20260622000000.ts +58 -0
  48. package/src/modules/cms/migrations/Migration20260622000001.ts +17 -0
  49. package/src/modules/cms/models/cms-accumulation.ts +10 -0
  50. package/src/modules/cms/models/cms-handover.ts +16 -0
  51. package/src/modules/cms/models/index.ts +4 -0
  52. package/src/modules/cms/models/petty-cash-transaction.ts +23 -0
  53. package/src/modules/cms/models/petty-cash.ts +10 -0
  54. package/src/modules/cms/permissions.ts +34 -0
  55. package/src/modules/cms/services/cms-module-service.ts +284 -0
  56. package/src/modules/cms/services/index.ts +13 -0
@@ -0,0 +1,35 @@
1
+ import type {
2
+ AuthenticatedMedusaRequest,
3
+ MedusaResponse,
4
+ } from "@medusajs/framework"
5
+ import { CMS_MODULE } from "../../../../../../modules/cms/constants"
6
+ import type CmsModuleService from "../../../../../../modules/cms/services/cms-module-service"
7
+
8
+ function getService(req: AuthenticatedMedusaRequest): CmsModuleService {
9
+ return (req as any).scope.resolve(CMS_MODULE)
10
+ }
11
+
12
+ function getLogger(req: AuthenticatedMedusaRequest) {
13
+ try {
14
+ return (req as any).scope.resolve("logger")
15
+ } catch {
16
+ return { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
17
+ }
18
+ }
19
+
20
+ export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
21
+ const { storeId } = req.params
22
+ try {
23
+ const service = getService(req)
24
+ const accumulation = await service.getAccumulation(storeId)
25
+ return res.status(200).json({
26
+ accumulation: accumulation ?? { store_id: storeId, cash_in_store: 0 },
27
+ })
28
+ } catch (err) {
29
+ getLogger(req).error("[retailos/cms] get accumulation failed", {
30
+ store_id: storeId,
31
+ error: err instanceof Error ? err.message : String(err),
32
+ })
33
+ return res.status(500).json({ code: "INTERNAL_ERROR", message: (err as Error).message })
34
+ }
35
+ }
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect, vi } from "vitest"
2
+ import { GET } from "../route"
3
+
4
+ function makeService() {
5
+ return {
6
+ listAndCountCmsHandovers: vi.fn().mockResolvedValue([[], 0]),
7
+ }
8
+ }
9
+
10
+ function makeReq(query: Record<string, string> = {}, service = makeService()) {
11
+ return {
12
+ query,
13
+ scope: {
14
+ resolve: vi.fn().mockImplementation((key: string) => {
15
+ if (key === "cms") return service
16
+ throw new Error("not found")
17
+ }),
18
+ },
19
+ _service: service,
20
+ }
21
+ }
22
+
23
+ function makeRes() {
24
+ const headers: Record<string, string> = {}
25
+ return {
26
+ status: vi.fn().mockReturnThis(),
27
+ json: vi.fn().mockReturnThis(),
28
+ send: vi.fn().mockReturnThis(),
29
+ setHeader: vi.fn().mockImplementation((k: string, v: string) => { headers[k] = v }),
30
+ _headers: headers,
31
+ }
32
+ }
33
+
34
+ describe("GET /export", () => {
35
+ it("returns CSV with correct headers row", async () => {
36
+ const service = makeService()
37
+ service.listAndCountCmsHandovers.mockResolvedValue([[], 0])
38
+ const req = makeReq({}, service)
39
+ const res = makeRes()
40
+
41
+ await GET(req as any, res as any)
42
+
43
+ expect(res.status).toHaveBeenCalledWith(200)
44
+ expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "text/csv")
45
+ expect(res.setHeader).toHaveBeenCalledWith(
46
+ "Content-Disposition",
47
+ expect.stringContaining("cms-handovers-")
48
+ )
49
+ const csv = (res.send as ReturnType<typeof vi.fn>).mock.calls[0][0] as string
50
+ expect(csv).toContain("id,store_id,employee_id,handover_id,handover_amount,total_cash,type,remark,created_at")
51
+ })
52
+
53
+ it("includes a data row for each handover", async () => {
54
+ const service = makeService()
55
+ const handovers = [
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
+ ]
58
+ service.listAndCountCmsHandovers.mockResolvedValue([handovers, 1])
59
+ const req = makeReq({}, service)
60
+ const res = makeRes()
61
+
62
+ await GET(req as any, res as any)
63
+
64
+ const csv = (res.send as ReturnType<typeof vi.fn>).mock.calls[0][0] as string
65
+ const lines = csv.split("\n")
66
+ expect(lines).toHaveLength(2)
67
+ expect(lines[1]).toContain("cmsh_1")
68
+ expect(lines[1]).toContain("500")
69
+ expect(lines[1]).toContain("CR")
70
+ })
71
+
72
+ it("escapes commas and quotes in CSV values", async () => {
73
+ const service = makeService()
74
+ const handovers = [
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
+ ]
77
+ service.listAndCountCmsHandovers.mockResolvedValue([handovers, 1])
78
+ const req = makeReq({}, service)
79
+ const res = makeRes()
80
+
81
+ await GET(req as any, res as any)
82
+
83
+ const csv = (res.send as ReturnType<typeof vi.fn>).mock.calls[0][0] as string
84
+ expect(csv).toContain('"cash, ""extra"""')
85
+ })
86
+
87
+ it("filters by store_id when provided in query", async () => {
88
+ const service = makeService()
89
+ service.listAndCountCmsHandovers.mockResolvedValue([[], 0])
90
+ const req = makeReq({ store_id: "s1" }, service)
91
+ const res = makeRes()
92
+
93
+ await GET(req as any, res as any)
94
+
95
+ expect(service.listAndCountCmsHandovers).toHaveBeenCalledWith(
96
+ expect.objectContaining({ store_id: ["s1"] }),
97
+ expect.any(Object)
98
+ )
99
+ })
100
+
101
+ it("does not filter by store_id when not in query", async () => {
102
+ const service = makeService()
103
+ service.listAndCountCmsHandovers.mockResolvedValue([[], 0])
104
+ const req = makeReq({}, service)
105
+ const res = makeRes()
106
+
107
+ await GET(req as any, res as any)
108
+
109
+ expect(service.listAndCountCmsHandovers).toHaveBeenCalledWith(
110
+ expect.not.objectContaining({ store_id: expect.anything() }),
111
+ expect.any(Object)
112
+ )
113
+ })
114
+
115
+ it("returns 500 when service throws", async () => {
116
+ const service = makeService()
117
+ service.listAndCountCmsHandovers.mockRejectedValue(new Error("db error"))
118
+ const req = makeReq({}, service)
119
+ const res = makeRes()
120
+
121
+ await GET(req as any, res as any)
122
+
123
+ expect(res.status).toHaveBeenCalledWith(500)
124
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "INTERNAL_ERROR" }))
125
+ })
126
+ })
@@ -0,0 +1,58 @@
1
+ import type {
2
+ AuthenticatedMedusaRequest,
3
+ MedusaResponse,
4
+ } from "@medusajs/framework"
5
+ import { CMS_MODULE } from "../../../../../modules/cms/constants"
6
+ import type CmsModuleService from "../../../../../modules/cms/services/cms-module-service"
7
+
8
+ function getService(req: AuthenticatedMedusaRequest): CmsModuleService {
9
+ return (req as any).scope.resolve(CMS_MODULE)
10
+ }
11
+
12
+ function getLogger(req: AuthenticatedMedusaRequest) {
13
+ try {
14
+ return (req as any).scope.resolve("logger")
15
+ } catch {
16
+ return { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
17
+ }
18
+ }
19
+
20
+ function escapeCSV(val: unknown): string {
21
+ if (val == null) return ""
22
+ const s = String(val)
23
+ if (s.includes(",") || s.includes('"') || s.includes("\n")) {
24
+ return `"${s.replace(/"/g, '""')}"`
25
+ }
26
+ return s
27
+ }
28
+
29
+ export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
30
+ const query = req.query as Record<string, string>
31
+ const store_id = query.store_id
32
+
33
+ try {
34
+ const service = getService(req)
35
+ const filters: Record<string, unknown> = {}
36
+ if (store_id) filters.store_id = [store_id]
37
+
38
+ const [handovers] = await service.listAndCountCmsHandovers(filters, {
39
+ take: 10000,
40
+ order: { created_at: "DESC" },
41
+ })
42
+
43
+ const headers = ["id", "store_id", "employee_id", "handover_id", "handover_amount", "total_cash", "type", "remark", "created_at"]
44
+ const rows = (handovers as any[]).map((h) =>
45
+ headers.map((k) => escapeCSV((h as any)[k])).join(",")
46
+ )
47
+ const csv = [headers.join(","), ...rows].join("\n")
48
+
49
+ res.setHeader("Content-Type", "text/csv")
50
+ res.setHeader("Content-Disposition", `attachment; filename="cms-handovers-${Date.now()}.csv"`)
51
+ return res.status(200).send(csv)
52
+ } catch (err) {
53
+ getLogger(req).error("[retailos/cms] export failed", {
54
+ error: err instanceof Error ? err.message : String(err),
55
+ })
56
+ return res.status(500).json({ code: "INTERNAL_ERROR", message: (err as Error).message })
57
+ }
58
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, vi } from "vitest"
2
+ import { GET } from "../route"
3
+
4
+ function makeService() {
5
+ return {
6
+ retrieveCmsHandover: vi.fn(),
7
+ }
8
+ }
9
+
10
+ function makeReq(params: Record<string, string>, service = makeService()) {
11
+ return {
12
+ params,
13
+ scope: {
14
+ resolve: vi.fn().mockImplementation((key: string) => {
15
+ if (key === "cms") return service
16
+ throw new Error("not found")
17
+ }),
18
+ },
19
+ _service: service,
20
+ }
21
+ }
22
+
23
+ function makeRes() {
24
+ return {
25
+ status: vi.fn().mockReturnThis(),
26
+ json: vi.fn().mockReturnThis(),
27
+ }
28
+ }
29
+
30
+ describe("GET /handovers/[id]", () => {
31
+ it("returns handover when found", async () => {
32
+ const service = makeService()
33
+ const handover = { id: "cmsh_1", store_id: "s1", handover_amount: 500 }
34
+ service.retrieveCmsHandover.mockResolvedValue(handover)
35
+ const req = makeReq({ id: "cmsh_1" }, service)
36
+ const res = makeRes()
37
+
38
+ await GET(req as any, res as any)
39
+
40
+ expect(service.retrieveCmsHandover).toHaveBeenCalledWith("cmsh_1")
41
+ expect(res.status).toHaveBeenCalledWith(200)
42
+ expect(res.json).toHaveBeenCalledWith({ handover })
43
+ })
44
+
45
+ it("returns 404 when handover is null", async () => {
46
+ const service = makeService()
47
+ service.retrieveCmsHandover.mockResolvedValue(null)
48
+ const req = makeReq({ id: "cmsh_missing" }, service)
49
+ const res = makeRes()
50
+
51
+ await GET(req as any, res as any)
52
+
53
+ expect(res.status).toHaveBeenCalledWith(404)
54
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "NOT_FOUND" }))
55
+ })
56
+
57
+ it("returns 500 when service throws", async () => {
58
+ const service = makeService()
59
+ service.retrieveCmsHandover.mockRejectedValue(new Error("db error"))
60
+ const req = makeReq({ id: "cmsh_1" }, service)
61
+ const res = makeRes()
62
+
63
+ await GET(req as any, res as any)
64
+
65
+ expect(res.status).toHaveBeenCalledWith(500)
66
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "INTERNAL_ERROR" }))
67
+ })
68
+ })
@@ -0,0 +1,36 @@
1
+ import type {
2
+ AuthenticatedMedusaRequest,
3
+ MedusaResponse,
4
+ } from "@medusajs/framework"
5
+ import { CMS_MODULE } from "../../../../../../modules/cms/constants"
6
+ import type CmsModuleService from "../../../../../../modules/cms/services/cms-module-service"
7
+
8
+ function getService(req: AuthenticatedMedusaRequest): CmsModuleService {
9
+ return (req as any).scope.resolve(CMS_MODULE)
10
+ }
11
+
12
+ function getLogger(req: AuthenticatedMedusaRequest) {
13
+ try {
14
+ return (req as any).scope.resolve("logger")
15
+ } catch {
16
+ return { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
17
+ }
18
+ }
19
+
20
+ export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
21
+ const { id } = req.params
22
+ try {
23
+ const service = getService(req)
24
+ const handover = await service.retrieveCmsHandover(id)
25
+ if (!handover) {
26
+ return res.status(404).json({ code: "NOT_FOUND", message: "Handover not found" })
27
+ }
28
+ return res.status(200).json({ handover })
29
+ } catch (err) {
30
+ getLogger(req).error("[retailos/cms] retrieve handover failed", {
31
+ id,
32
+ error: err instanceof Error ? err.message : String(err),
33
+ })
34
+ return res.status(500).json({ code: "INTERNAL_ERROR", message: (err as Error).message })
35
+ }
36
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect, vi } from "vitest"
2
+ import { GET, POST } from "../route"
3
+
4
+ function makeService() {
5
+ return {
6
+ listAndCountCmsHandovers: vi.fn().mockResolvedValue([[], 0]),
7
+ handover: vi.fn().mockResolvedValue({ id: "cmsh_1", store_id: "s1", handover_amount: 100 }),
8
+ }
9
+ }
10
+
11
+ function makeReq(overrides: Record<string, unknown> = {}, service = makeService()) {
12
+ return {
13
+ body: {},
14
+ filterableFields: {},
15
+ scope: {
16
+ resolve: vi.fn().mockImplementation((key: string) => {
17
+ if (key === "cms") return service
18
+ throw new Error("not found")
19
+ }),
20
+ },
21
+ _service: service,
22
+ ...overrides,
23
+ }
24
+ }
25
+
26
+ function makeRes() {
27
+ return {
28
+ status: vi.fn().mockReturnThis(),
29
+ json: vi.fn().mockReturnThis(),
30
+ }
31
+ }
32
+
33
+ describe("GET /handovers", () => {
34
+ it("returns handover list with count", async () => {
35
+ const service = makeService()
36
+ const handovers = [{ id: "cmsh_1" }, { id: "cmsh_2" }]
37
+ service.listAndCountCmsHandovers.mockResolvedValue([handovers, 2])
38
+ const req = makeReq({}, service)
39
+ const res = makeRes()
40
+
41
+ await GET(req as any, res as any)
42
+
43
+ expect(res.status).toHaveBeenCalledWith(200)
44
+ expect(res.json).toHaveBeenCalledWith({ handovers, count: 2 })
45
+ })
46
+
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)
51
+ const res = makeRes()
52
+
53
+ await GET(req as any, res as any)
54
+
55
+ expect(res.status).toHaveBeenCalledWith(500)
56
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "INTERNAL_ERROR" }))
57
+ })
58
+ })
59
+
60
+ describe("POST /handovers", () => {
61
+ it("returns 400 for invalid body", async () => {
62
+ const req = makeReq({ body: { store_id: "s1" } })
63
+ const res = makeRes()
64
+
65
+ await POST(req as any, res as any)
66
+
67
+ expect(res.status).toHaveBeenCalledWith(400)
68
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "BAD_REQUEST" }))
69
+ })
70
+
71
+ it("creates handover and returns 201", async () => {
72
+ const service = makeService()
73
+ const req = makeReq({
74
+ body: {
75
+ store_id: "s1",
76
+ employee_id: "e1",
77
+ handover_amount: 200,
78
+ type: "CR",
79
+ },
80
+ }, service)
81
+ const res = makeRes()
82
+
83
+ await POST(req as any, res as any)
84
+
85
+ expect(service.handover).toHaveBeenCalledWith(
86
+ expect.objectContaining({ store_id: "s1", handover_amount: 200, type: "CR" })
87
+ )
88
+ expect(res.status).toHaveBeenCalledWith(201)
89
+ expect(res.json).toHaveBeenCalledWith({ handover: { id: "cmsh_1", store_id: "s1", handover_amount: 100 } })
90
+ })
91
+
92
+ it("returns 500 when service throws", async () => {
93
+ const service = makeService()
94
+ service.handover.mockRejectedValue(new Error("write failed"))
95
+ const req = makeReq({
96
+ body: { store_id: "s1", employee_id: "e1", handover_amount: 100 },
97
+ }, service)
98
+ const res = makeRes()
99
+
100
+ await POST(req as any, res as any)
101
+
102
+ expect(res.status).toHaveBeenCalledWith(500)
103
+ })
104
+ })
@@ -0,0 +1,79 @@
1
+ import type {
2
+ AuthenticatedMedusaRequest,
3
+ MedusaResponse,
4
+ } from "@medusajs/framework"
5
+ import { z } from "zod"
6
+ import { CMS_MODULE } from "../../../../../modules/cms/constants"
7
+ import type CmsModuleService from "../../../../../modules/cms/services/cms-module-service"
8
+
9
+ const createSchema = z.object({
10
+ store_id: z.string().min(1),
11
+ employee_id: z.string().min(1),
12
+ handover_id: z.string().nullable().optional(),
13
+ handover_amount: z.number().nonnegative(),
14
+ total_cash: z.number().nullable().optional(),
15
+ type: z.enum(["DB", "CR"]).nullable().optional(),
16
+ image_url: z.string().nullable().optional(),
17
+ remark: z.string().nullable().optional(),
18
+ metadata: z.record(z.string(), z.unknown()).nullable().optional(),
19
+ })
20
+
21
+ function getService(req: AuthenticatedMedusaRequest): CmsModuleService {
22
+ return (req as any).scope.resolve(CMS_MODULE)
23
+ }
24
+
25
+ function getLogger(req: AuthenticatedMedusaRequest) {
26
+ try {
27
+ return (req as any).scope.resolve("logger")
28
+ } catch {
29
+ return { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
30
+ }
31
+ }
32
+
33
+ export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
34
+ 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" },
40
+ })
41
+ return res.status(200).json({ handovers: data, count })
42
+ } catch (err) {
43
+ getLogger(req).error("[retailos/cms] list handovers failed", {
44
+ error: err instanceof Error ? err.message : String(err),
45
+ })
46
+ return res.status(500).json({ code: "INTERNAL_ERROR", message: (err as Error).message })
47
+ }
48
+ }
49
+
50
+ export const POST = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
51
+ const parsed = createSchema.safeParse(req.body)
52
+ if (!parsed.success) {
53
+ return res.status(400).json({
54
+ code: "BAD_REQUEST",
55
+ message: "Invalid handover input",
56
+ details: parsed.error.flatten(),
57
+ })
58
+ }
59
+ try {
60
+ const service = getService(req)
61
+ const created = await service.handover({
62
+ store_id: parsed.data.store_id,
63
+ employee_id: parsed.data.employee_id,
64
+ handover_id: parsed.data.handover_id ?? null,
65
+ handover_amount: parsed.data.handover_amount,
66
+ total_cash: parsed.data.total_cash ?? null,
67
+ type: parsed.data.type ?? null,
68
+ image_url: parsed.data.image_url ?? null,
69
+ remark: parsed.data.remark ?? null,
70
+ metadata: parsed.data.metadata ?? null,
71
+ })
72
+ return res.status(201).json({ handover: created })
73
+ } catch (err) {
74
+ getLogger(req).error("[retailos/cms] create handover failed", {
75
+ error: err instanceof Error ? err.message : String(err),
76
+ })
77
+ return res.status(500).json({ code: "INTERNAL_ERROR", message: (err as Error).message })
78
+ }
79
+ }
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { POST } from "../route"
3
+
4
+ function makeService() {
5
+ return {
6
+ dayStart: vi.fn().mockResolvedValue(undefined),
7
+ dayEnd: vi.fn().mockResolvedValue(undefined),
8
+ handover: vi.fn().mockResolvedValue({ id: "cmsh_1" }),
9
+ addPettyCash: vi.fn().mockResolvedValue({ id: "pct_1" }),
10
+ addExpense: vi.fn().mockResolvedValue({ id: "pct_2" }),
11
+ }
12
+ }
13
+
14
+ function makeReq(body: unknown, service = makeService()) {
15
+ return {
16
+ body,
17
+ scope: {
18
+ resolve: vi.fn().mockImplementation((key: string) => {
19
+ if (key === "cms") return service
20
+ throw new Error("not found")
21
+ }),
22
+ },
23
+ _service: service,
24
+ }
25
+ }
26
+
27
+ function makeRes() {
28
+ const res = {
29
+ status: vi.fn().mockReturnThis(),
30
+ json: vi.fn().mockReturnThis(),
31
+ }
32
+ return res
33
+ }
34
+
35
+ describe("POST /operation - validation", () => {
36
+ it("returns 400 for invalid body", async () => {
37
+ const req = makeReq({ operation: "unknown_op", store_id: "s1", employee_id: "e1" })
38
+ const res = makeRes()
39
+ await POST(req as any, res as any)
40
+ expect(res.status).toHaveBeenCalledWith(400)
41
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "BAD_REQUEST" }))
42
+ })
43
+
44
+ it("returns 400 when store_id is missing", async () => {
45
+ const req = makeReq({ operation: "day_start", employee_id: "e1", opening_amount: 100 })
46
+ const res = makeRes()
47
+ await POST(req as any, res as any)
48
+ expect(res.status).toHaveBeenCalledWith(400)
49
+ })
50
+ })
51
+
52
+ describe("POST /operation - day_start", () => {
53
+ it("returns 400 when opening_amount is missing", async () => {
54
+ const req = makeReq({ operation: "day_start", store_id: "s1", employee_id: "e1" })
55
+ const res = makeRes()
56
+ await POST(req as any, res as any)
57
+ expect(res.status).toHaveBeenCalledWith(400)
58
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ message: "opening_amount is required for day_start" }))
59
+ })
60
+
61
+ it("calls service.dayStart and returns 200", async () => {
62
+ const service = makeService()
63
+ const req = makeReq({ operation: "day_start", store_id: "s1", employee_id: "e1", opening_amount: 500 }, service)
64
+ const res = makeRes()
65
+ await POST(req as any, res as any)
66
+ expect(service.dayStart).toHaveBeenCalledWith({ store_id: "s1", employee_id: "e1", opening_amount: 500 })
67
+ expect(res.status).toHaveBeenCalledWith(200)
68
+ expect(res.json).toHaveBeenCalledWith({ operation: "day_start", store_id: "s1" })
69
+ })
70
+ })
71
+
72
+ describe("POST /operation - day_end", () => {
73
+ it("returns 400 when closing_amount is missing", async () => {
74
+ const req = makeReq({ operation: "day_end", store_id: "s1", employee_id: "e1" })
75
+ const res = makeRes()
76
+ await POST(req as any, res as any)
77
+ expect(res.status).toHaveBeenCalledWith(400)
78
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ message: "closing_amount is required for day_end" }))
79
+ })
80
+
81
+ it("calls service.dayEnd and returns 200", async () => {
82
+ const service = makeService()
83
+ const req = makeReq({ operation: "day_end", store_id: "s1", employee_id: "e1", closing_amount: 800 }, service)
84
+ const res = makeRes()
85
+ await POST(req as any, res as any)
86
+ expect(service.dayEnd).toHaveBeenCalledWith({ store_id: "s1", employee_id: "e1", closing_amount: 800 })
87
+ expect(res.status).toHaveBeenCalledWith(200)
88
+ })
89
+ })
90
+
91
+ describe("POST /operation - handover", () => {
92
+ it("returns 400 when handover_amount is missing", async () => {
93
+ const req = makeReq({ operation: "handover", store_id: "s1", employee_id: "e1" })
94
+ const res = makeRes()
95
+ await POST(req as any, res as any)
96
+ expect(res.status).toHaveBeenCalledWith(400)
97
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ message: "handover_amount is required for handover" }))
98
+ })
99
+
100
+ it("calls service.handover and returns 201", async () => {
101
+ const service = makeService()
102
+ const req = makeReq({ operation: "handover", store_id: "s1", employee_id: "e1", handover_amount: 200, type: "CR" }, service)
103
+ const res = makeRes()
104
+ await POST(req as any, res as any)
105
+ expect(service.handover).toHaveBeenCalledWith(expect.objectContaining({ handover_amount: 200, type: "CR" }))
106
+ expect(res.status).toHaveBeenCalledWith(201)
107
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ operation: "handover", handover: { id: "cmsh_1" } }))
108
+ })
109
+ })
110
+
111
+ describe("POST /operation - add_petty_cash", () => {
112
+ it("returns 400 when amount is missing", async () => {
113
+ const req = makeReq({ operation: "add_petty_cash", store_id: "s1", employee_id: "e1" })
114
+ const res = makeRes()
115
+ await POST(req as any, res as any)
116
+ expect(res.status).toHaveBeenCalledWith(400)
117
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ message: "amount is required for add_petty_cash" }))
118
+ })
119
+
120
+ it("calls service.addPettyCash and returns 201", async () => {
121
+ const service = makeService()
122
+ const req = makeReq({ operation: "add_petty_cash", store_id: "s1", employee_id: "e1", amount: 300 }, service)
123
+ const res = makeRes()
124
+ await POST(req as any, res as any)
125
+ expect(service.addPettyCash).toHaveBeenCalledWith(expect.objectContaining({ store_id: "s1", amount: 300 }))
126
+ expect(res.status).toHaveBeenCalledWith(201)
127
+ })
128
+ })
129
+
130
+ describe("POST /operation - add_expense", () => {
131
+ it("returns 400 when amount is missing", async () => {
132
+ const req = makeReq({ operation: "add_expense", store_id: "s1", employee_id: "e1" })
133
+ const res = makeRes()
134
+ await POST(req as any, res as any)
135
+ expect(res.status).toHaveBeenCalledWith(400)
136
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ message: "amount is required for add_expense" }))
137
+ })
138
+
139
+ it("calls service.addExpense with optional fields and returns 201", async () => {
140
+ const service = makeService()
141
+ const req = makeReq({
142
+ operation: "add_expense",
143
+ store_id: "s1",
144
+ employee_id: "e1",
145
+ amount: 50,
146
+ category: "office",
147
+ sub_category: "stationery",
148
+ reason: "pens",
149
+ }, service)
150
+ const res = makeRes()
151
+ await POST(req as any, res as any)
152
+ expect(service.addExpense).toHaveBeenCalledWith(
153
+ expect.objectContaining({ amount: 50, category: "office", sub_category: "stationery", reason: "pens" })
154
+ )
155
+ expect(res.status).toHaveBeenCalledWith(201)
156
+ })
157
+ })
158
+
159
+ describe("POST /operation - service error", () => {
160
+ it("returns 500 when service throws", async () => {
161
+ const service = makeService()
162
+ service.dayStart.mockRejectedValue(new Error("db error"))
163
+ const req = makeReq({ operation: "day_start", store_id: "s1", employee_id: "e1", opening_amount: 100 }, service)
164
+ const res = makeRes()
165
+ await POST(req as any, res as any)
166
+ expect(res.status).toHaveBeenCalledWith(500)
167
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "INTERNAL_ERROR", message: "db error" }))
168
+ })
169
+ })