@dainamite/billing 0.17.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/CHANGELOG.md +658 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/modules/billing/acl.js +20 -0
- package/dist/modules/billing/acl.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +182 -0
- package/dist/modules/billing/api/accounts/route.js.map +7 -0
- package/dist/modules/billing/api/export/account/[id]/route.js +170 -0
- package/dist/modules/billing/api/export/account/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +85 -0
- package/dist/modules/billing/api/invoices/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/add-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +78 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/post/route.js +74 -0
- package/dist/modules/billing/api/invoices/post/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/route.js +109 -0
- package/dist/modules/billing/api/invoices/route.js.map +7 -0
- package/dist/modules/billing/api/items/bulk/route.js +74 -0
- package/dist/modules/billing/api/items/bulk/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +224 -0
- package/dist/modules/billing/api/items/route.js.map +7 -0
- package/dist/modules/billing/api/openapi.js +25 -0
- package/dist/modules/billing/api/openapi.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +122 -0
- package/dist/modules/billing/api/run-outcomes/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +70 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +7 -0
- package/dist/modules/billing/api/runs/route.js +143 -0
- package/dist/modules/billing/api/runs/route.js.map +7 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js +65 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +138 -0
- package/dist/modules/billing/api/usage/route.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +402 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js +82 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.js +173 -0
- package/dist/modules/billing/backend/billing/accounts/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +500 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.js +187 -0
- package/dist/modules/billing/backend/billing/invoices/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js +265 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.js +90 -0
- package/dist/modules/billing/backend/billing/items/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.js +208 -0
- package/dist/modules/billing/backend/billing/items/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +293 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.js +341 -0
- package/dist/modules/billing/backend/billing/runs/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js +100 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js.map +7 -0
- package/dist/modules/billing/ce.js +56 -0
- package/dist/modules/billing/ce.js.map +7 -0
- package/dist/modules/billing/cli.js +67 -0
- package/dist/modules/billing/cli.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +157 -0
- package/dist/modules/billing/commands/accounts.js.map +7 -0
- package/dist/modules/billing/commands/index.js +6 -0
- package/dist/modules/billing/commands/index.js.map +7 -0
- package/dist/modules/billing/commands/invoices.js +401 -0
- package/dist/modules/billing/commands/invoices.js.map +7 -0
- package/dist/modules/billing/commands/items.js +317 -0
- package/dist/modules/billing/commands/items.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +123 -0
- package/dist/modules/billing/commands/runs.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +88 -0
- package/dist/modules/billing/commands/usage.js.map +7 -0
- package/dist/modules/billing/components/AccountForm.js +420 -0
- package/dist/modules/billing/components/AccountForm.js.map +7 -0
- package/dist/modules/billing/components/DetailFields.js +37 -0
- package/dist/modules/billing/components/DetailFields.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +440 -0
- package/dist/modules/billing/components/ItemForm.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +195 -0
- package/dist/modules/billing/components/LineFormDialog.js.map +7 -0
- package/dist/modules/billing/components/UsageForm.js +262 -0
- package/dist/modules/billing/components/UsageForm.js.map +7 -0
- package/dist/modules/billing/data/entities.js +433 -0
- package/dist/modules/billing/data/entities.js.map +7 -0
- package/dist/modules/billing/data/entityIds.js +12 -0
- package/dist/modules/billing/data/entityIds.js.map +7 -0
- package/dist/modules/billing/data/validators.js +396 -0
- package/dist/modules/billing/data/validators.js.map +7 -0
- package/dist/modules/billing/di.js +6 -0
- package/dist/modules/billing/di.js.map +7 -0
- package/dist/modules/billing/events.js +35 -0
- package/dist/modules/billing/events.js.map +7 -0
- package/dist/modules/billing/i18n/en.json +336 -0
- package/dist/modules/billing/i18n/pl.json +336 -0
- package/dist/modules/billing/index.js +17 -0
- package/dist/modules/billing/index.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +101 -0
- package/dist/modules/billing/lib/billPeriod.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +256 -0
- package/dist/modules/billing/lib/billRunEngine.js.map +7 -0
- package/dist/modules/billing/lib/billRunLabel.js +8 -0
- package/dist/modules/billing/lib/billRunLabel.js.map +7 -0
- package/dist/modules/billing/lib/idempotency.js +13 -0
- package/dist/modules/billing/lib/idempotency.js.map +7 -0
- package/dist/modules/billing/lib/invoiceStatus.js +38 -0
- package/dist/modules/billing/lib/invoiceStatus.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +153 -0
- package/dist/modules/billing/lib/invoiceWriter.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js +77 -0
- package/dist/modules/billing/lib/itemSelector.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +130 -0
- package/dist/modules/billing/lib/seeds/demo.js.map +7 -0
- package/dist/modules/billing/lib/tenantLock.js +32 -0
- package/dist/modules/billing/lib/tenantLock.js.map +7 -0
- package/dist/modules/billing/lib/usageRater.js +150 -0
- package/dist/modules/billing/lib/usageRater.js.map +7 -0
- package/dist/modules/billing/lib/usageRunner.js +82 -0
- package/dist/modules/billing/lib/usageRunner.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js +144 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js.map +7 -0
- package/dist/modules/billing/setup.js +190 -0
- package/dist/modules/billing/setup.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +47 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +7 -0
- package/dist/modules/billing/translations.js +5 -0
- package/dist/modules/billing/translations.js.map +7 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js +31 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +7 -0
- package/package.json +148 -0
- package/src/modules/billing/__integration__/TC-BILL-001.spec.ts +116 -0
- package/src/modules/billing/__integration__/TC-BILL-002.spec.ts +169 -0
- package/src/modules/billing/__integration__/TC-BILL-003.spec.ts +28 -0
- package/src/modules/billing/__integration__/meta.ts +4 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +464 -0
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +284 -0
- package/src/modules/billing/__tests__/lib/billPeriod.test.ts +131 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +561 -0
- package/src/modules/billing/__tests__/lib/idempotency.test.ts +81 -0
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +119 -0
- package/src/modules/billing/__tests__/lib/itemSelector.test.ts +192 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/tenantLock.test.ts +126 -0
- package/src/modules/billing/__tests__/lib/usageRater.test.ts +190 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +223 -0
- package/src/modules/billing/__tests__/setup.test.ts +107 -0
- package/src/modules/billing/__tests__/validators.test.ts +594 -0
- package/src/modules/billing/acl.ts +16 -0
- package/src/modules/billing/api/accounts/route.ts +198 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +233 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +143 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +74 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +97 -0
- package/src/modules/billing/api/invoices/post/route.ts +97 -0
- package/src/modules/billing/api/invoices/remove-line/route.ts +75 -0
- package/src/modules/billing/api/invoices/route.ts +167 -0
- package/src/modules/billing/api/items/bulk/route.ts +92 -0
- package/src/modules/billing/api/items/route.ts +255 -0
- package/src/modules/billing/api/openapi.ts +33 -0
- package/src/modules/billing/api/run-outcomes/route.ts +149 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +88 -0
- package/src/modules/billing/api/runs/route.ts +170 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +87 -0
- package/src/modules/billing/api/usage/route.ts +148 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +574 -0
- package/src/modules/billing/backend/billing/accounts/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +101 -0
- package/src/modules/billing/backend/billing/accounts/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +222 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +613 -0
- package/src/modules/billing/backend/billing/invoices/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/invoices/page.tsx +245 -0
- package/src/modules/billing/backend/billing/items/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +366 -0
- package/src/modules/billing/backend/billing/items/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/create/page.tsx +110 -0
- package/src/modules/billing/backend/billing/items/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/items/page.tsx +272 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +404 -0
- package/src/modules/billing/backend/billing/runs/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/runs/page.tsx +426 -0
- package/src/modules/billing/backend/billing/usage/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/usage/create/page.tsx +120 -0
- package/src/modules/billing/ce.ts +74 -0
- package/src/modules/billing/cli.ts +89 -0
- package/src/modules/billing/commands/accounts.ts +205 -0
- package/src/modules/billing/commands/index.ts +13 -0
- package/src/modules/billing/commands/invoices.ts +542 -0
- package/src/modules/billing/commands/items.ts +438 -0
- package/src/modules/billing/commands/runs.ts +166 -0
- package/src/modules/billing/commands/usage.ts +118 -0
- package/src/modules/billing/components/AccountForm.tsx +517 -0
- package/src/modules/billing/components/DetailFields.tsx +60 -0
- package/src/modules/billing/components/ItemForm.tsx +558 -0
- package/src/modules/billing/components/LineFormDialog.tsx +253 -0
- package/src/modules/billing/components/UsageForm.tsx +326 -0
- package/src/modules/billing/data/entities.ts +533 -0
- package/src/modules/billing/data/entityIds.ts +25 -0
- package/src/modules/billing/data/validators.ts +562 -0
- package/src/modules/billing/di.ts +14 -0
- package/src/modules/billing/events.ts +47 -0
- package/src/modules/billing/i18n/en.json +336 -0
- package/src/modules/billing/i18n/pl.json +336 -0
- package/src/modules/billing/index.ts +22 -0
- package/src/modules/billing/lib/billPeriod.ts +181 -0
- package/src/modules/billing/lib/billRunEngine.ts +416 -0
- package/src/modules/billing/lib/billRunLabel.ts +14 -0
- package/src/modules/billing/lib/idempotency.ts +46 -0
- package/src/modules/billing/lib/invoiceStatus.ts +61 -0
- package/src/modules/billing/lib/invoiceWriter.ts +260 -0
- package/src/modules/billing/lib/itemSelector.ts +154 -0
- package/src/modules/billing/lib/seeds/demo.ts +199 -0
- package/src/modules/billing/lib/tenantLock.ts +87 -0
- package/src/modules/billing/lib/usageRater.ts +250 -0
- package/src/modules/billing/lib/usageRunner.ts +189 -0
- package/src/modules/billing/migrations/Migration20260519000000_billing.ts +175 -0
- package/src/modules/billing/setup.ts +252 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +89 -0
- package/src/modules/billing/translations.ts +9 -0
- package/src/modules/billing/workers/reap-stale-bill-runs.ts +57 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
|
+
import { Page, PageBody } from "@open-mercato/ui/backend/Page";
|
|
7
|
+
import { DataTable } from "@open-mercato/ui/backend/DataTable";
|
|
8
|
+
import { Tag } from "@open-mercato/ui/primitives/tag";
|
|
9
|
+
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
10
|
+
function formatDate(value) {
|
|
11
|
+
if (!value) return "\u2014";
|
|
12
|
+
const parsed = new Date(value);
|
|
13
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString();
|
|
14
|
+
}
|
|
15
|
+
function statusVariant(status) {
|
|
16
|
+
switch (status) {
|
|
17
|
+
case "paid":
|
|
18
|
+
return "success";
|
|
19
|
+
case "posted":
|
|
20
|
+
return "warning";
|
|
21
|
+
case "void":
|
|
22
|
+
return "error";
|
|
23
|
+
case "draft":
|
|
24
|
+
default:
|
|
25
|
+
return "default";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function formatMoney(value, currency) {
|
|
29
|
+
const n = Number.parseFloat(value);
|
|
30
|
+
if (!Number.isFinite(n)) return value;
|
|
31
|
+
try {
|
|
32
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(n);
|
|
33
|
+
} catch {
|
|
34
|
+
return `${value} ${currency}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function BillingInvoicesListPage() {
|
|
38
|
+
const t = useT();
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
const [rows, setRows] = React.useState([]);
|
|
41
|
+
const [page, setPage] = React.useState(1);
|
|
42
|
+
const [pageSize] = React.useState(25);
|
|
43
|
+
const [filters, setFilters] = React.useState({});
|
|
44
|
+
const [search, setSearch] = React.useState("");
|
|
45
|
+
const [total, setTotal] = React.useState(0);
|
|
46
|
+
const [totalPages, setTotalPages] = React.useState(1);
|
|
47
|
+
const [loading, setLoading] = React.useState(true);
|
|
48
|
+
const filterDefs = React.useMemo(
|
|
49
|
+
() => [
|
|
50
|
+
{
|
|
51
|
+
id: "status",
|
|
52
|
+
label: t("billing.invoices.filters.status", "Status"),
|
|
53
|
+
type: "select",
|
|
54
|
+
options: [
|
|
55
|
+
{ value: "draft", label: t("billing.invoices.status.draft", "Draft") },
|
|
56
|
+
{ value: "posted", label: t("billing.invoices.status.posted", "Posted") },
|
|
57
|
+
{ value: "paid", label: t("billing.invoices.status.paid", "Paid") },
|
|
58
|
+
{ value: "void", label: t("billing.invoices.status.void", "Void") }
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "testRun",
|
|
63
|
+
label: t("billing.invoices.filters.test_run", "Test runs"),
|
|
64
|
+
type: "select",
|
|
65
|
+
options: [
|
|
66
|
+
{ value: "true", label: t("billing.invoices.filters.test_run.only", "Only test") },
|
|
67
|
+
{
|
|
68
|
+
value: "false",
|
|
69
|
+
label: t("billing.invoices.filters.test_run.exclude", "Exclude test")
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
[t]
|
|
75
|
+
);
|
|
76
|
+
const loadRows = React.useCallback(async () => {
|
|
77
|
+
setLoading(true);
|
|
78
|
+
try {
|
|
79
|
+
const params = new URLSearchParams({
|
|
80
|
+
page: String(page),
|
|
81
|
+
pageSize: String(pageSize)
|
|
82
|
+
});
|
|
83
|
+
if (search.trim()) params.set("search", search.trim());
|
|
84
|
+
if (typeof filters.status === "string" && filters.status) {
|
|
85
|
+
params.set("status", filters.status);
|
|
86
|
+
}
|
|
87
|
+
if (filters.testRun === "true" || filters.testRun === "false") {
|
|
88
|
+
params.set("testRun", String(filters.testRun));
|
|
89
|
+
}
|
|
90
|
+
const result = await readApiResultOrThrow(
|
|
91
|
+
`/api/billing/invoices?${params.toString()}`
|
|
92
|
+
);
|
|
93
|
+
setRows(result.items ?? []);
|
|
94
|
+
setTotal(result.total ?? 0);
|
|
95
|
+
setTotalPages(result.totalPages ?? 1);
|
|
96
|
+
} finally {
|
|
97
|
+
setLoading(false);
|
|
98
|
+
}
|
|
99
|
+
}, [filters.status, filters.testRun, page, pageSize, search]);
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
void loadRows();
|
|
102
|
+
}, [loadRows]);
|
|
103
|
+
const columns = React.useMemo(
|
|
104
|
+
() => [
|
|
105
|
+
{
|
|
106
|
+
accessorKey: "invoice_number",
|
|
107
|
+
header: t("billing.invoices.columns.number", "Number"),
|
|
108
|
+
cell: ({ row }) => {
|
|
109
|
+
const isTest = row.original.metadata?.test_run === true;
|
|
110
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
111
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-primary", children: row.original.invoice_number }),
|
|
112
|
+
isTest ? /* @__PURE__ */ jsx(Tag, { variant: "warning", children: "TEST" }) : null
|
|
113
|
+
] });
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
accessorKey: "status",
|
|
118
|
+
header: t("billing.invoices.columns.status", "Status"),
|
|
119
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(Tag, { variant: statusVariant(row.original.status), children: row.original.status ?? "\u2014" })
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "period",
|
|
123
|
+
header: t("billing.invoices.columns.period", "Period"),
|
|
124
|
+
cell: ({ row }) => {
|
|
125
|
+
const start = row.original.metadata?.bill_period_start;
|
|
126
|
+
const end = row.original.metadata?.bill_period_end;
|
|
127
|
+
if (!start || !end) return "\u2014";
|
|
128
|
+
return /* @__PURE__ */ jsxs("span", { className: "text-xs", children: [
|
|
129
|
+
start,
|
|
130
|
+
" \u2192 ",
|
|
131
|
+
end
|
|
132
|
+
] });
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "total",
|
|
137
|
+
header: t("billing.invoices.columns.total", "Total"),
|
|
138
|
+
cell: ({ row }) => formatMoney(row.original.grand_total_gross_amount, row.original.currency_code)
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
accessorKey: "outstanding_amount",
|
|
142
|
+
header: t("billing.invoices.columns.outstanding", "Outstanding"),
|
|
143
|
+
cell: ({ row }) => formatMoney(row.original.outstanding_amount, row.original.currency_code)
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
accessorKey: "issue_date",
|
|
147
|
+
header: t("billing.invoices.columns.issued", "Issued"),
|
|
148
|
+
cell: ({ row }) => formatDate(row.original.issue_date)
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
[t]
|
|
152
|
+
);
|
|
153
|
+
return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
|
|
154
|
+
DataTable,
|
|
155
|
+
{
|
|
156
|
+
title: t("billing.invoices.title", "Billing Invoices"),
|
|
157
|
+
refreshButton: {
|
|
158
|
+
label: t("billing.common.refresh", "Refresh"),
|
|
159
|
+
onRefresh: loadRows,
|
|
160
|
+
isRefreshing: loading
|
|
161
|
+
},
|
|
162
|
+
columnChooser: { auto: true },
|
|
163
|
+
perspective: { tableId: "billing-invoices" },
|
|
164
|
+
columns,
|
|
165
|
+
data: rows,
|
|
166
|
+
onRowClick: (row) => router.push(`/backend/billing/invoices/${row.id}`),
|
|
167
|
+
isLoading: loading,
|
|
168
|
+
pagination: {
|
|
169
|
+
page,
|
|
170
|
+
pageSize,
|
|
171
|
+
total,
|
|
172
|
+
totalPages,
|
|
173
|
+
onPageChange: setPage
|
|
174
|
+
},
|
|
175
|
+
filters: filterDefs,
|
|
176
|
+
filterValues: filters,
|
|
177
|
+
onFiltersApply: setFilters,
|
|
178
|
+
onFiltersClear: () => setFilters({}),
|
|
179
|
+
searchValue: search,
|
|
180
|
+
onSearchChange: setSearch
|
|
181
|
+
}
|
|
182
|
+
) }) });
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
BillingInvoicesListPage as default
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/billing/backend/billing/invoices/page.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\n/**\n * Billing Invoices list \u2014 what the operator actually opens every\n * morning to \"review and post drafts\".\n *\n * Scope: only invoices the billing engine emitted (`metadata.bill_run_id`\n * set). Plain `core/sales` invoices (created by sales orders, etc.)\n * are NOT shown here \u2014 billing has its own surface so the operator's\n * review queue is small and focused.\n */\n\ntype BillingInvoiceRow = {\n id: string\n invoice_number: string\n status: 'draft' | 'posted' | 'paid' | 'void' | null\n currency_code: string\n issue_date: string | null\n due_date: string | null\n subtotal_net_amount: string\n grand_total_net_amount: string\n grand_total_gross_amount: string\n paid_total_amount: string\n outstanding_amount: string\n metadata: {\n bill_run_id?: string\n bill_account_id?: string\n bill_period_start?: string\n bill_period_end?: string\n test_run?: boolean\n } | null\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = {\n items: BillingInvoiceRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction statusVariant(\n status: BillingInvoiceRow['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'paid':\n return 'success'\n case 'posted':\n return 'warning'\n case 'void':\n return 'error'\n case 'draft':\n default:\n return 'default'\n }\n}\n\nfunction formatMoney(value: string, currency: string): string {\n const n = Number.parseFloat(value)\n if (!Number.isFinite(n)) return value\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n } catch {\n return `${value} ${currency}`\n }\n}\n\nexport default function BillingInvoicesListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<BillingInvoiceRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.invoices.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'draft', label: t('billing.invoices.status.draft', 'Draft') },\n { value: 'posted', label: t('billing.invoices.status.posted', 'Posted') },\n { value: 'paid', label: t('billing.invoices.status.paid', 'Paid') },\n { value: 'void', label: t('billing.invoices.status.void', 'Void') },\n ],\n },\n {\n id: 'testRun',\n label: t('billing.invoices.filters.test_run', 'Test runs'),\n type: 'select',\n options: [\n { value: 'true', label: t('billing.invoices.filters.test_run.only', 'Only test') },\n {\n value: 'false',\n label: t('billing.invoices.filters.test_run.exclude', 'Exclude test'),\n },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.status === 'string' && filters.status) {\n params.set('status', filters.status)\n }\n if (filters.testRun === 'true' || filters.testRun === 'false') {\n params.set('testRun', String(filters.testRun))\n }\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/invoices?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [filters.status, filters.testRun, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<BillingInvoiceRow>[]>(\n () => [\n {\n accessorKey: 'invoice_number',\n header: t('billing.invoices.columns.number', 'Number'),\n cell: ({ row }) => {\n const isTest = row.original.metadata?.test_run === true\n return (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-primary\">\n {row.original.invoice_number}\n </span>\n {isTest ? <Tag variant=\"warning\">TEST</Tag> : null}\n </div>\n )\n },\n },\n {\n accessorKey: 'status',\n header: t('billing.invoices.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={statusVariant(row.original.status)}>{row.original.status ?? '\u2014'}</Tag>\n ),\n },\n {\n id: 'period',\n header: t('billing.invoices.columns.period', 'Period'),\n cell: ({ row }) => {\n const start = row.original.metadata?.bill_period_start\n const end = row.original.metadata?.bill_period_end\n if (!start || !end) return '\u2014'\n return (\n <span className=\"text-xs\">\n {start} \u2192 {end}\n </span>\n )\n },\n },\n {\n id: 'total',\n header: t('billing.invoices.columns.total', 'Total'),\n cell: ({ row }) =>\n formatMoney(row.original.grand_total_gross_amount, row.original.currency_code),\n },\n {\n accessorKey: 'outstanding_amount',\n header: t('billing.invoices.columns.outstanding', 'Outstanding'),\n cell: ({ row }) =>\n formatMoney(row.original.outstanding_amount, row.original.currency_code),\n },\n {\n accessorKey: 'issue_date',\n header: t('billing.invoices.columns.issued', 'Issued'),\n cell: ({ row }) => formatDate(row.original.issue_date),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.invoices.title', 'Billing Invoices')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-invoices' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/invoices/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FileText } from "lucide-react";
|
|
3
|
+
const invoicesIcon = React.createElement(FileText, { size: 16 });
|
|
4
|
+
const metadata = {
|
|
5
|
+
requireAuth: true,
|
|
6
|
+
requireFeatures: ["billing.invoice.view"],
|
|
7
|
+
pageTitle: "Billing Invoices",
|
|
8
|
+
pageTitleKey: "billing.invoices.title",
|
|
9
|
+
pageGroup: "Billing",
|
|
10
|
+
pageGroupKey: "billing.nav.group",
|
|
11
|
+
pageOrder: 40,
|
|
12
|
+
icon: invoicesIcon,
|
|
13
|
+
breadcrumb: [{ label: "Billing Invoices", labelKey: "billing.invoices.title" }]
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
metadata
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=page.meta.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/billing/backend/billing/invoices/page.meta.ts"],
|
|
4
|
+
"sourcesContent": ["import React from 'react'\nimport { FileText } from 'lucide-react'\n\nconst invoicesIcon = React.createElement(FileText, { size: 16 })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['billing.invoice.view'],\n pageTitle: 'Billing Invoices',\n pageTitleKey: 'billing.invoices.title',\n pageGroup: 'Billing',\n pageGroupKey: 'billing.nav.group',\n pageOrder: 40,\n icon: invoicesIcon,\n breadcrumb: [{ label: 'Billing Invoices', labelKey: 'billing.invoices.title' }],\n} as const\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,WAAW;AAClB,SAAS,gBAAgB;AAEzB,MAAM,eAAe,MAAM,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAExD,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,sBAAsB;AAAA,EACxC,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY,CAAC,EAAE,OAAO,oBAAoB,UAAU,yBAAyB,CAAC;AAChF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Pencil } from "lucide-react";
|
|
6
|
+
import { useParams, useRouter } from "next/navigation";
|
|
7
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
8
|
+
import { Page, PageBody, PageHeader } from "@open-mercato/ui/backend/Page";
|
|
9
|
+
import { FormHeader } from "@open-mercato/ui/backend/forms";
|
|
10
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
11
|
+
import { Tag } from "@open-mercato/ui/primitives/tag";
|
|
12
|
+
import { LoadingMessage, ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
13
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
14
|
+
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
15
|
+
import {
|
|
16
|
+
apiCallOrThrow,
|
|
17
|
+
readApiResultOrThrow
|
|
18
|
+
} from "@open-mercato/ui/backend/utils/apiCall";
|
|
19
|
+
import { normalizeCrudServerError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
20
|
+
import {
|
|
21
|
+
ItemForm
|
|
22
|
+
} from "../../../../components/ItemForm.js";
|
|
23
|
+
import { DetailCard, DetailField } from "../../../../components/DetailFields.js";
|
|
24
|
+
function formatDate(value) {
|
|
25
|
+
if (!value) return "\u2014";
|
|
26
|
+
const parsed = new Date(value);
|
|
27
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString();
|
|
28
|
+
}
|
|
29
|
+
function formatRate(row) {
|
|
30
|
+
const rate = row.rate_json ?? {};
|
|
31
|
+
if (row.type === "one_time" && typeof rate.amount === "number") {
|
|
32
|
+
return String(rate.amount);
|
|
33
|
+
}
|
|
34
|
+
if (row.type === "recurring" && typeof rate.unit_price === "number") {
|
|
35
|
+
return String(rate.unit_price);
|
|
36
|
+
}
|
|
37
|
+
if (row.type === "usage") {
|
|
38
|
+
if ("model" in rate && "tiers" in rate) {
|
|
39
|
+
return `tiered (${String(rate.model)})`;
|
|
40
|
+
}
|
|
41
|
+
if (typeof rate.unit_price === "number") {
|
|
42
|
+
return String(rate.unit_price);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return "\u2014";
|
|
46
|
+
}
|
|
47
|
+
function toFormValues(row) {
|
|
48
|
+
const rate = row.rate_json ?? {};
|
|
49
|
+
const out = {
|
|
50
|
+
billAccountId: row.bill_account_id,
|
|
51
|
+
type: row.type,
|
|
52
|
+
billStartDate: row.bill_start_date?.slice(0, 10) ?? "",
|
|
53
|
+
billEndDate: row.bill_end_date?.slice(0, 10) ?? "",
|
|
54
|
+
description: row.description,
|
|
55
|
+
uomCode: row.uom_code ?? "",
|
|
56
|
+
subscriptionId: row.subscription_id ?? "",
|
|
57
|
+
subscriptionItemId: row.subscription_item_id ?? "",
|
|
58
|
+
isActive: row.is_active
|
|
59
|
+
};
|
|
60
|
+
if (row.type === "one_time" && typeof rate.amount === "number") {
|
|
61
|
+
out.oneTimeAmount = String(rate.amount);
|
|
62
|
+
} else if (row.type === "recurring" && typeof rate.unit_price === "number") {
|
|
63
|
+
out.recurringUnitPrice = String(rate.unit_price);
|
|
64
|
+
} else if (row.type === "usage") {
|
|
65
|
+
if ("model" in rate && "tiers" in rate) {
|
|
66
|
+
out.usageMode = "tiered";
|
|
67
|
+
out.usageTieredJson = JSON.stringify(rate, null, 2);
|
|
68
|
+
} else if (typeof rate.unit_price === "number") {
|
|
69
|
+
out.usageMode = "simple";
|
|
70
|
+
out.usageUnitPrice = String(rate.unit_price);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function ItemView({ row }) {
|
|
76
|
+
const t = useT();
|
|
77
|
+
return /* @__PURE__ */ jsxs(DetailCard, { children: [
|
|
78
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.detail.field.account", "Account"), children: /* @__PURE__ */ jsx(
|
|
79
|
+
Link,
|
|
80
|
+
{
|
|
81
|
+
href: `/backend/billing/accounts/${row.bill_account_id}`,
|
|
82
|
+
className: "text-primary hover:underline",
|
|
83
|
+
children: row.bill_account_name ?? row.bill_account_id
|
|
84
|
+
}
|
|
85
|
+
) }),
|
|
86
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.columns.type", "Type"), children: /* @__PURE__ */ jsx(Tag, { variant: "info", children: row.type }) }),
|
|
87
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.columns.start", "Start"), children: formatDate(row.bill_start_date) }),
|
|
88
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.columns.end", "End"), children: formatDate(row.bill_end_date) }),
|
|
89
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.form.rate.title", "Rate"), children: formatRate(row) }),
|
|
90
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.columns.uom", "UoM"), children: row.uom_code || "\u2014" }),
|
|
91
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.form.subscription_id", "Subscription ID"), children: row.subscription_id || "\u2014" }),
|
|
92
|
+
/* @__PURE__ */ jsx(
|
|
93
|
+
DetailField,
|
|
94
|
+
{
|
|
95
|
+
label: t("billing.items.form.subscription_item_id", "Subscription item ID"),
|
|
96
|
+
children: row.subscription_item_id || "\u2014"
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.detail.field.source_ref", "Source ref"), children: row.source_ref ? /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: row.source_ref }) : "\u2014" }),
|
|
100
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.detail.field.billed_to", "Billed through"), children: row.billed_to_date ? formatDate(row.billed_to_date) : "\u2014" }),
|
|
101
|
+
row.currency_mismatch ? /* @__PURE__ */ jsx(
|
|
102
|
+
DetailField,
|
|
103
|
+
{
|
|
104
|
+
label: t("billing.items.detail.currency_mismatch", "Currency mismatch"),
|
|
105
|
+
children: /* @__PURE__ */ jsx(Tag, { variant: "warning", children: t("billing.common.yes", "Yes") })
|
|
106
|
+
}
|
|
107
|
+
) : null,
|
|
108
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.items.columns.status", "Status"), children: row.is_active ? /* @__PURE__ */ jsx(Tag, { variant: "success", children: t("billing.common.active", "Active") }) : /* @__PURE__ */ jsx(Tag, { variant: "default", children: t("billing.common.inactive", "Inactive") }) }),
|
|
109
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.common.id", "ID"), children: /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: row.id }) })
|
|
110
|
+
] });
|
|
111
|
+
}
|
|
112
|
+
function BillingItemDetailPage(props) {
|
|
113
|
+
const t = useT();
|
|
114
|
+
const router = useRouter();
|
|
115
|
+
const urlParams = useParams();
|
|
116
|
+
const itemId = props.params?.id ?? urlParams?.id ?? "";
|
|
117
|
+
const [row, setRow] = React.useState(null);
|
|
118
|
+
const [loading, setLoading] = React.useState(true);
|
|
119
|
+
const [error, setError] = React.useState(null);
|
|
120
|
+
const [editing, setEditing] = React.useState(false);
|
|
121
|
+
const [saving, setSaving] = React.useState(false);
|
|
122
|
+
const [deleting, setDeleting] = React.useState(false);
|
|
123
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
124
|
+
const load = React.useCallback(async () => {
|
|
125
|
+
if (!itemId) return;
|
|
126
|
+
setLoading(true);
|
|
127
|
+
setError(null);
|
|
128
|
+
try {
|
|
129
|
+
const result = await readApiResultOrThrow(
|
|
130
|
+
`/api/billing/items?id=${itemId}&pageSize=1`
|
|
131
|
+
);
|
|
132
|
+
const found = result.items?.[0];
|
|
133
|
+
if (!found) {
|
|
134
|
+
setError(t("billing.items.detail.error.not_found", "Item not found"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setRow(found);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const { message } = normalizeCrudServerError(err);
|
|
140
|
+
setError(message || t("billing.items.detail.error.load", "Failed to load item"));
|
|
141
|
+
} finally {
|
|
142
|
+
setLoading(false);
|
|
143
|
+
}
|
|
144
|
+
}, [itemId, t]);
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
void load();
|
|
147
|
+
}, [load]);
|
|
148
|
+
const handleSubmit = React.useCallback(
|
|
149
|
+
async (assembled) => {
|
|
150
|
+
if (!row) return;
|
|
151
|
+
setSaving(true);
|
|
152
|
+
try {
|
|
153
|
+
const payload = {
|
|
154
|
+
id: row.id,
|
|
155
|
+
billStartDate: assembled.billStartDate,
|
|
156
|
+
description: assembled.description,
|
|
157
|
+
rateJson: assembled.rateJson,
|
|
158
|
+
isActive: assembled.isActive ?? row.is_active
|
|
159
|
+
};
|
|
160
|
+
payload.billEndDate = assembled.billEndDate ?? null;
|
|
161
|
+
payload.uomCode = assembled.uomCode ?? null;
|
|
162
|
+
payload.subscriptionId = assembled.subscriptionId ?? null;
|
|
163
|
+
payload.subscriptionItemId = assembled.subscriptionItemId ?? null;
|
|
164
|
+
await apiCallOrThrow("/api/billing/items", {
|
|
165
|
+
method: "PUT",
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify(payload)
|
|
168
|
+
});
|
|
169
|
+
flash(t("billing.items.detail.save.success", "Item updated"), "success");
|
|
170
|
+
setEditing(false);
|
|
171
|
+
void load();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const { message } = normalizeCrudServerError(err);
|
|
174
|
+
flash(message || t("billing.items.detail.save.error", "Save failed"), "error");
|
|
175
|
+
} finally {
|
|
176
|
+
setSaving(false);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[load, row, t]
|
|
180
|
+
);
|
|
181
|
+
const handleDelete = React.useCallback(async () => {
|
|
182
|
+
if (!row) return;
|
|
183
|
+
const ok = await confirm({
|
|
184
|
+
title: t("billing.items.detail.delete.confirm.title", "Delete item?"),
|
|
185
|
+
text: t(
|
|
186
|
+
"billing.items.detail.delete.confirm.text",
|
|
187
|
+
"Soft-deletes the item. Its row stays in the database for audit; the Bill Run engine will stop including it immediately."
|
|
188
|
+
),
|
|
189
|
+
confirmText: t("billing.items.detail.delete.confirm.button", "Soft delete"),
|
|
190
|
+
variant: "destructive"
|
|
191
|
+
});
|
|
192
|
+
if (!ok) return;
|
|
193
|
+
setDeleting(true);
|
|
194
|
+
try {
|
|
195
|
+
await apiCallOrThrow("/api/billing/items", {
|
|
196
|
+
method: "DELETE",
|
|
197
|
+
headers: { "Content-Type": "application/json" },
|
|
198
|
+
body: JSON.stringify({ id: row.id })
|
|
199
|
+
});
|
|
200
|
+
flash(t("billing.items.detail.delete.success", "Item deleted"), "success");
|
|
201
|
+
router.push("/backend/billing/items");
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const { message } = normalizeCrudServerError(err);
|
|
204
|
+
flash(message || t("billing.items.detail.delete.error", "Delete failed"), "error");
|
|
205
|
+
setDeleting(false);
|
|
206
|
+
}
|
|
207
|
+
}, [confirm, router, row, t]);
|
|
208
|
+
if (error) {
|
|
209
|
+
return /* @__PURE__ */ jsxs(Page, { children: [
|
|
210
|
+
/* @__PURE__ */ jsx(PageHeader, { title: t("billing.items.detail.title", "Billing Item") }),
|
|
211
|
+
/* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(ErrorMessage, { label: error }) })
|
|
212
|
+
] });
|
|
213
|
+
}
|
|
214
|
+
if (loading || !row) {
|
|
215
|
+
return /* @__PURE__ */ jsxs(Page, { children: [
|
|
216
|
+
/* @__PURE__ */ jsx(PageHeader, { title: t("billing.items.detail.title", "Billing Item") }),
|
|
217
|
+
/* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(LoadingMessage, { label: t("billing.items.detail.loading", "Loading item\u2026") }) })
|
|
218
|
+
] });
|
|
219
|
+
}
|
|
220
|
+
return /* @__PURE__ */ jsxs(Page, { children: [
|
|
221
|
+
/* @__PURE__ */ jsx(
|
|
222
|
+
FormHeader,
|
|
223
|
+
{
|
|
224
|
+
mode: "detail",
|
|
225
|
+
backHref: "/backend/billing/items",
|
|
226
|
+
entityTypeLabel: t("billing.items.detail.title", "Billing Item"),
|
|
227
|
+
title: row.description,
|
|
228
|
+
statusBadge: row.is_active ? /* @__PURE__ */ jsx(Tag, { variant: "success", children: t("billing.common.active", "Active") }) : /* @__PURE__ */ jsx(Tag, { variant: "default", children: t("billing.common.inactive", "Inactive") }),
|
|
229
|
+
actionsContent: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
230
|
+
!editing ? /* @__PURE__ */ jsxs(Button, { type: "button", onClick: () => setEditing(true), children: [
|
|
231
|
+
/* @__PURE__ */ jsx(Pencil, { size: 16 }),
|
|
232
|
+
t("billing.common.edit", "Edit")
|
|
233
|
+
] }) : null,
|
|
234
|
+
/* @__PURE__ */ jsx(
|
|
235
|
+
Button,
|
|
236
|
+
{
|
|
237
|
+
type: "button",
|
|
238
|
+
variant: "outline",
|
|
239
|
+
onClick: handleDelete,
|
|
240
|
+
disabled: deleting,
|
|
241
|
+
children: deleting ? t("billing.items.detail.delete.in_progress", "Deleting\u2026") : t("billing.items.detail.delete.action", "Soft delete")
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
] })
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
/* @__PURE__ */ jsxs(PageBody, { children: [
|
|
248
|
+
editing ? /* @__PURE__ */ jsx(
|
|
249
|
+
ItemForm,
|
|
250
|
+
{
|
|
251
|
+
mode: "edit",
|
|
252
|
+
initial: toFormValues(row),
|
|
253
|
+
submitting: saving,
|
|
254
|
+
onSubmit: handleSubmit,
|
|
255
|
+
onCancel: () => setEditing(false)
|
|
256
|
+
}
|
|
257
|
+
) : /* @__PURE__ */ jsx(ItemView, { row }),
|
|
258
|
+
ConfirmDialogElement
|
|
259
|
+
] })
|
|
260
|
+
] });
|
|
261
|
+
}
|
|
262
|
+
export {
|
|
263
|
+
BillingItemDetailPage as default
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=page.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/billing/backend/billing/items/%5Bid%5D/page.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Pencil } from 'lucide-react'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n apiCallOrThrow,\n readApiResultOrThrow,\n} from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport {\n ItemForm,\n type AssembledItemPayload,\n type ItemFormValues,\n} from '../../../../components/ItemForm'\nimport { DetailCard, DetailField } from '../../../../components/DetailFields'\n\n/**\n * `/backend/billing/items/[id]` \u2014 Billing Item detail.\n *\n * Opens read-only (field grid). The `Edit` button swaps the grid for\n * the `ItemForm`; Save / Cancel return to the read-only view. Loads\n * via the list endpoint (`?id=<id>`), edits through `PUT`, soft-delete\n * via `DELETE` with a `ConfirmDialog` gate.\n *\n * Two read-only badges at the top: `currencyMismatch` and\n * `billedToDate` (Bill Run engine-managed columns the operator\n * cannot edit but needs to see for troubleshooting).\n */\n\n// API list rows are snake_case (see `api/items/route.ts` `fields`).\n// `bill_account_name` is resolved by the route's `afterList` hook.\ntype BillingItemRow = {\n id: string\n bill_account_id: string\n bill_account_name: string | null\n type: 'one_time' | 'recurring' | 'usage'\n bill_start_date: string\n bill_end_date: string | null\n description: string\n rate_json: Record<string, unknown> | null\n uom_code: string | null\n subscription_id: string | null\n subscription_item_id: string | null\n source_ref: string | null\n currency_mismatch: boolean\n billed_to_date: string | null\n is_active: boolean\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = { items: BillingItemRow[] }\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction formatRate(row: BillingItemRow): string {\n const rate = (row.rate_json ?? {}) as Record<string, unknown>\n if (row.type === 'one_time' && typeof rate.amount === 'number') {\n return String(rate.amount)\n }\n if (row.type === 'recurring' && typeof rate.unit_price === 'number') {\n return String(rate.unit_price)\n }\n if (row.type === 'usage') {\n if ('model' in rate && 'tiers' in rate) {\n return `tiered (${String(rate.model)})`\n }\n if (typeof rate.unit_price === 'number') {\n return String(rate.unit_price)\n }\n }\n return '\u2014'\n}\n\nfunction toFormValues(row: BillingItemRow): Partial<ItemFormValues> {\n const rate = (row.rate_json ?? {}) as Record<string, unknown>\n const out: Partial<ItemFormValues> = {\n billAccountId: row.bill_account_id,\n type: row.type,\n billStartDate: row.bill_start_date?.slice(0, 10) ?? '',\n billEndDate: row.bill_end_date?.slice(0, 10) ?? '',\n description: row.description,\n uomCode: row.uom_code ?? '',\n subscriptionId: row.subscription_id ?? '',\n subscriptionItemId: row.subscription_item_id ?? '',\n isActive: row.is_active,\n }\n if (row.type === 'one_time' && typeof rate.amount === 'number') {\n out.oneTimeAmount = String(rate.amount)\n } else if (row.type === 'recurring' && typeof rate.unit_price === 'number') {\n out.recurringUnitPrice = String(rate.unit_price)\n } else if (row.type === 'usage') {\n if ('model' in rate && 'tiers' in rate) {\n out.usageMode = 'tiered'\n out.usageTieredJson = JSON.stringify(rate, null, 2)\n } else if (typeof rate.unit_price === 'number') {\n out.usageMode = 'simple'\n out.usageUnitPrice = String(rate.unit_price)\n }\n }\n return out\n}\n\n// \u2500\u2500\u2500 Read-only view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction ItemView({ row }: { row: BillingItemRow }) {\n const t = useT()\n return (\n <DetailCard>\n <DetailField label={t('billing.items.detail.field.account', 'Account')}>\n <Link\n href={`/backend/billing/accounts/${row.bill_account_id}`}\n className=\"text-primary hover:underline\"\n >\n {row.bill_account_name ?? row.bill_account_id}\n </Link>\n </DetailField>\n <DetailField label={t('billing.items.columns.type', 'Type')}>\n <Tag variant=\"info\">{row.type}</Tag>\n </DetailField>\n <DetailField label={t('billing.items.columns.start', 'Start')}>\n {formatDate(row.bill_start_date)}\n </DetailField>\n <DetailField label={t('billing.items.columns.end', 'End')}>\n {formatDate(row.bill_end_date)}\n </DetailField>\n <DetailField label={t('billing.items.form.rate.title', 'Rate')}>\n {formatRate(row)}\n </DetailField>\n <DetailField label={t('billing.items.columns.uom', 'UoM')}>\n {row.uom_code || '\u2014'}\n </DetailField>\n <DetailField label={t('billing.items.form.subscription_id', 'Subscription ID')}>\n {row.subscription_id || '\u2014'}\n </DetailField>\n <DetailField\n label={t('billing.items.form.subscription_item_id', 'Subscription item ID')}\n >\n {row.subscription_item_id || '\u2014'}\n </DetailField>\n <DetailField label={t('billing.items.detail.field.source_ref', 'Source ref')}>\n {row.source_ref ? (\n <span className=\"font-mono text-xs\">{row.source_ref}</span>\n ) : (\n '\u2014'\n )}\n </DetailField>\n <DetailField label={t('billing.items.detail.field.billed_to', 'Billed through')}>\n {row.billed_to_date ? formatDate(row.billed_to_date) : '\u2014'}\n </DetailField>\n {row.currency_mismatch ? (\n <DetailField\n label={t('billing.items.detail.currency_mismatch', 'Currency mismatch')}\n >\n <Tag variant=\"warning\">{t('billing.common.yes', 'Yes')}</Tag>\n </DetailField>\n ) : null}\n <DetailField label={t('billing.items.columns.status', 'Status')}>\n {row.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n )}\n </DetailField>\n <DetailField label={t('billing.common.id', 'ID')}>\n <span className=\"font-mono text-xs\">{row.id}</span>\n </DetailField>\n </DetailCard>\n )\n}\n\nexport default function BillingItemDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n const router = useRouter()\n // OM serves backend pages through a catch-all route \u2014 the dynamic\n // `[id]` segment arrives as a page prop; `useParams()` is the fallback.\n const urlParams = useParams<{ id: string }>()\n const itemId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [row, setRow] = React.useState<BillingItemRow | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [editing, setEditing] = React.useState(false)\n const [saving, setSaving] = React.useState(false)\n const [deleting, setDeleting] = React.useState(false)\n\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const load = React.useCallback(async () => {\n if (!itemId) return\n setLoading(true)\n setError(null)\n try {\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/items?id=${itemId}&pageSize=1`,\n )\n const found = result.items?.[0]\n if (!found) {\n setError(t('billing.items.detail.error.not_found', 'Item not found'))\n return\n }\n setRow(found)\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setError(message || t('billing.items.detail.error.load', 'Failed to load item'))\n } finally {\n setLoading(false)\n }\n }, [itemId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const handleSubmit = React.useCallback(\n async (assembled: AssembledItemPayload) => {\n if (!row) return\n setSaving(true)\n try {\n const payload: Record<string, unknown> = {\n id: row.id,\n billStartDate: assembled.billStartDate,\n description: assembled.description,\n rateJson: assembled.rateJson,\n isActive: assembled.isActive ?? row.is_active,\n }\n // Optional fields \u2014 submit null when the operator cleared them\n // so the column actually resets (the schema validators accept\n // null for nullable columns).\n payload.billEndDate = assembled.billEndDate ?? null\n payload.uomCode = assembled.uomCode ?? null\n payload.subscriptionId = assembled.subscriptionId ?? null\n payload.subscriptionItemId = assembled.subscriptionItemId ?? null\n\n await apiCallOrThrow('/api/billing/items', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n flash(t('billing.items.detail.save.success', 'Item updated'), 'success')\n setEditing(false)\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.items.detail.save.error', 'Save failed'), 'error')\n } finally {\n setSaving(false)\n }\n },\n [load, row, t],\n )\n\n const handleDelete = React.useCallback(async () => {\n if (!row) return\n const ok = await confirm({\n title: t('billing.items.detail.delete.confirm.title', 'Delete item?'),\n text: t(\n 'billing.items.detail.delete.confirm.text',\n 'Soft-deletes the item. Its row stays in the database for audit; the Bill Run engine will stop including it immediately.',\n ),\n confirmText: t('billing.items.detail.delete.confirm.button', 'Soft delete'),\n variant: 'destructive',\n })\n if (!ok) return\n setDeleting(true)\n try {\n await apiCallOrThrow('/api/billing/items', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('billing.items.detail.delete.success', 'Item deleted'), 'success')\n router.push('/backend/billing/items')\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.items.detail.delete.error', 'Delete failed'), 'error')\n setDeleting(false)\n }\n }, [confirm, router, row, t])\n\n if (error) {\n return (\n <Page>\n <PageHeader title={t('billing.items.detail.title', 'Billing Item')} />\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n if (loading || !row) {\n return (\n <Page>\n <PageHeader title={t('billing.items.detail.title', 'Billing Item')} />\n <PageBody>\n <LoadingMessage label={t('billing.items.detail.loading', 'Loading item\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/items\"\n entityTypeLabel={t('billing.items.detail.title', 'Billing Item')}\n title={row.description}\n statusBadge={\n row.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n )\n }\n actionsContent={\n <>\n {!editing ? (\n <Button type=\"button\" onClick={() => setEditing(true)}>\n <Pencil size={16} />\n {t('billing.common.edit', 'Edit')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleDelete}\n disabled={deleting}\n >\n {deleting\n ? t('billing.items.detail.delete.in_progress', 'Deleting\u2026')\n : t('billing.items.detail.delete.action', 'Soft delete')}\n </Button>\n </>\n }\n />\n <PageBody>\n {editing ? (\n <ItemForm\n mode=\"edit\"\n initial={toFormValues(row)}\n submitting={saving}\n onSubmit={handleSubmit}\n onCancel={() => setEditing(false)}\n />\n ) : (\n <ItemView row={row} />\n )}\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Receipt } from "lucide-react";
|
|
3
|
+
const itemIcon = React.createElement(Receipt, { size: 16 });
|
|
4
|
+
const metadata = {
|
|
5
|
+
requireAuth: true,
|
|
6
|
+
requireFeatures: ["billing.item.view"],
|
|
7
|
+
pageTitle: "Billing Item",
|
|
8
|
+
pageTitleKey: "billing.items.detail.title",
|
|
9
|
+
pageGroup: "Billing",
|
|
10
|
+
pageGroupKey: "billing.nav.group",
|
|
11
|
+
navHidden: true,
|
|
12
|
+
icon: itemIcon,
|
|
13
|
+
breadcrumb: [
|
|
14
|
+
{
|
|
15
|
+
label: "Billing Items",
|
|
16
|
+
labelKey: "billing.items.title",
|
|
17
|
+
href: "/backend/billing/items"
|
|
18
|
+
},
|
|
19
|
+
{ label: "Item", labelKey: "billing.items.detail.title" }
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
export {
|
|
23
|
+
metadata
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=page.meta.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../src/modules/billing/backend/billing/items/%5Bid%5D/page.meta.ts"],
|
|
4
|
+
"sourcesContent": ["import React from 'react'\nimport { Receipt } from 'lucide-react'\n\nconst itemIcon = React.createElement(Receipt, { size: 16 })\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['billing.item.view'],\n pageTitle: 'Billing Item',\n pageTitleKey: 'billing.items.detail.title',\n pageGroup: 'Billing',\n pageGroupKey: 'billing.nav.group',\n navHidden: true,\n icon: itemIcon,\n breadcrumb: [\n {\n label: 'Billing Items',\n labelKey: 'billing.items.title',\n href: '/backend/billing/items',\n },\n { label: 'Item', labelKey: 'billing.items.detail.title' },\n ],\n} as const\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,WAAW;AAClB,SAAS,eAAe;AAExB,MAAM,WAAW,MAAM,cAAc,SAAS,EAAE,MAAM,GAAG,CAAC;AAEnD,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,mBAAmB;AAAA,EACrC,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY;AAAA,IACV;AAAA,MACE,OAAO;AAAA,MACP,UAAU;AAAA,MACV,MAAM;AAAA,IACR;AAAA,IACA,EAAE,OAAO,QAAQ,UAAU,6BAA6B;AAAA,EAC1D;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|