@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
|
@@ -3,7 +3,7 @@ import { GET } from "../route"
|
|
|
3
3
|
|
|
4
4
|
function makeService() {
|
|
5
5
|
return {
|
|
6
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
48
|
-
const
|
|
49
|
-
|
|
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
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
+
listAndCountRetailosPettyCashTransactions: vi.fn().mockResolvedValue([[], 0]),
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
function makeReq(
|
|
10
|
+
function makeReq(query: Record<string, unknown> = {}, service = makeService()) {
|
|
11
11
|
return {
|
|
12
|
-
|
|
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.
|
|
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.
|
|
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({
|
|
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("
|
|
54
|
+
it("includes store_id in filter when provided", async () => {
|
|
49
55
|
const service = makeService()
|
|
50
|
-
service.
|
|
51
|
-
const req = makeReq({ store_id:
|
|
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.
|
|
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.
|
|
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
|
|
24
|
-
|
|
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:
|
|
35
|
+
{ take: limit, skip: offset, order: { created_at: "DESC" } } as any
|
|
27
36
|
)
|
|
28
|
-
|
|
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),
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest"
|
|
2
|
+
import { GET } from "../route"
|
|
3
|
+
|
|
4
|
+
function makeReport(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
date: "2026-06-23",
|
|
7
|
+
store_id: "s1",
|
|
8
|
+
opening_balance: 8200,
|
|
9
|
+
net_cash_sales: 28450,
|
|
10
|
+
total_handovers_in: 0,
|
|
11
|
+
total_handovers_out: 5000,
|
|
12
|
+
petty_cash_in: 0,
|
|
13
|
+
petty_cash_out: 1000,
|
|
14
|
+
expected_closing_balance: 30650,
|
|
15
|
+
closing_balance: 30650,
|
|
16
|
+
cms_balance: 30650,
|
|
17
|
+
variance: 0,
|
|
18
|
+
is_balanced: true,
|
|
19
|
+
handovers: [],
|
|
20
|
+
shift_logs: [],
|
|
21
|
+
...overrides,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeService(report = makeReport()) {
|
|
26
|
+
return { getReconciliationReport: vi.fn().mockResolvedValue(report) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeReq(query: Record<string, string> = {}, service = makeService()) {
|
|
30
|
+
return {
|
|
31
|
+
query,
|
|
32
|
+
scope: {
|
|
33
|
+
resolve: vi.fn().mockImplementation((key: string) => {
|
|
34
|
+
if (key === "cms") return service
|
|
35
|
+
throw new Error("not found")
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
_service: service,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeRes() {
|
|
43
|
+
return {
|
|
44
|
+
status: vi.fn().mockReturnThis(),
|
|
45
|
+
json: vi.fn().mockReturnThis(),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("GET /reconciliation", () => {
|
|
50
|
+
it("returns 400 when store_id is missing", async () => {
|
|
51
|
+
const req = makeReq({ date: "2026-06-23" })
|
|
52
|
+
const res = makeRes()
|
|
53
|
+
await GET(req as any, res as any)
|
|
54
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
55
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "BAD_REQUEST" }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("returns 400 when date is missing", async () => {
|
|
59
|
+
const req = makeReq({ store_id: "s1" })
|
|
60
|
+
const res = makeRes()
|
|
61
|
+
await GET(req as any, res as any)
|
|
62
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
63
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "BAD_REQUEST" }))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("returns 400 when date format is invalid", async () => {
|
|
67
|
+
const req = makeReq({ store_id: "s1", date: "23-06-2026" })
|
|
68
|
+
const res = makeRes()
|
|
69
|
+
await GET(req as any, res as any)
|
|
70
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
71
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "BAD_REQUEST" }))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("returns reconciliation report on success", async () => {
|
|
75
|
+
const report = makeReport()
|
|
76
|
+
const service = makeService(report)
|
|
77
|
+
const req = makeReq({ store_id: "s1", date: "2026-06-23" }, service)
|
|
78
|
+
const res = makeRes()
|
|
79
|
+
|
|
80
|
+
await GET(req as any, res as any)
|
|
81
|
+
|
|
82
|
+
expect(res.status).toHaveBeenCalledWith(200)
|
|
83
|
+
expect(res.json).toHaveBeenCalledWith({ data: report })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("passes net_cash_sales from query to service", async () => {
|
|
87
|
+
const service = makeService()
|
|
88
|
+
const req = makeReq({ store_id: "s1", date: "2026-06-23", net_cash_sales: "28450" }, service)
|
|
89
|
+
const res = makeRes()
|
|
90
|
+
|
|
91
|
+
await GET(req as any, res as any)
|
|
92
|
+
|
|
93
|
+
expect(service.getReconciliationReport).toHaveBeenCalledWith({
|
|
94
|
+
store_id: "s1",
|
|
95
|
+
date: "2026-06-23",
|
|
96
|
+
net_cash_sales: 28450,
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("defaults net_cash_sales to 0 when not provided", async () => {
|
|
101
|
+
const service = makeService()
|
|
102
|
+
const req = makeReq({ store_id: "s1", date: "2026-06-23" }, service)
|
|
103
|
+
const res = makeRes()
|
|
104
|
+
|
|
105
|
+
await GET(req as any, res as any)
|
|
106
|
+
|
|
107
|
+
expect(service.getReconciliationReport).toHaveBeenCalledWith({
|
|
108
|
+
store_id: "s1",
|
|
109
|
+
date: "2026-06-23",
|
|
110
|
+
net_cash_sales: 0,
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("returns 500 when service throws", async () => {
|
|
115
|
+
const service = { getReconciliationReport: vi.fn().mockRejectedValue(new Error("db error")) }
|
|
116
|
+
const req = makeReq({ store_id: "s1", date: "2026-06-23" }, service as any)
|
|
117
|
+
const res = makeRes()
|
|
118
|
+
|
|
119
|
+
await GET(req as any, res as any)
|
|
120
|
+
|
|
121
|
+
expect(res.status).toHaveBeenCalledWith(500)
|
|
122
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: "INTERNAL_ERROR" }))
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
|
|
21
|
+
|
|
22
|
+
export const GET = async (req: AuthenticatedMedusaRequest, res: MedusaResponse) => {
|
|
23
|
+
const query = req.query as Record<string, string>
|
|
24
|
+
const { store_id, date } = query
|
|
25
|
+
|
|
26
|
+
if (!store_id) {
|
|
27
|
+
return res.status(400).json({ code: "BAD_REQUEST", message: "store_id is required" })
|
|
28
|
+
}
|
|
29
|
+
if (!date || !DATE_RE.test(date)) {
|
|
30
|
+
return res.status(400).json({ code: "BAD_REQUEST", message: "date is required in YYYY-MM-DD format" })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const net_cash_sales = query.net_cash_sales !== undefined ? Number(query.net_cash_sales) : 0
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const service = getService(req)
|
|
37
|
+
const data = await service.getReconciliationReport({ store_id, date, net_cash_sales })
|
|
38
|
+
return res.status(200).json({ data })
|
|
39
|
+
} catch (err) {
|
|
40
|
+
getLogger(req).error("[retailos/cms] reconciliation report failed", {
|
|
41
|
+
store_id,
|
|
42
|
+
date,
|
|
43
|
+
error: err instanceof Error ? err.message : String(err),
|
|
44
|
+
})
|
|
45
|
+
return res.status(500).json({ code: "INTERNAL_ERROR", message: (err as Error).message })
|
|
46
|
+
}
|
|
47
|
+
}
|