@dainamite/billing 0.17.0 → 0.19.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 +99 -19
- package/dist/modules/billing/acl.js +9 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/balance/route.js +62 -0
- package/dist/modules/billing/api/accounts/[id]/balance/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js +56 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js +36 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +7 -0
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +48 -3
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/statements/generate/route.js +68 -0
- package/dist/modules/billing/api/statements/generate/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +110 -0
- package/dist/modules/billing/api/statements/route.js.map +7 -0
- package/dist/modules/billing/api/topups/route.js +139 -0
- package/dist/modules/billing/api/topups/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/adjust/route.js +71 -0
- package/dist/modules/billing/api/transactions/adjust/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/route.js +109 -0
- package/dist/modules/billing/api/transactions/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +30 -6
- package/dist/modules/billing/api/usage/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +16 -1
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +8 -1
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +26 -1
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +129 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.js +152 -0
- package/dist/modules/billing/backend/billing/statements/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js +90 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.js +147 -0
- package/dist/modules/billing/backend/billing/topups/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js +114 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.js +149 -0
- package/dist/modules/billing/backend/billing/transactions/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +26 -1
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +21 -1
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/statements.js +74 -0
- package/dist/modules/billing/commands/statements.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +139 -0
- package/dist/modules/billing/commands/topups.js.map +7 -0
- package/dist/modules/billing/commands/transactions.js +119 -0
- package/dist/modules/billing/commands/transactions.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +72 -0
- package/dist/modules/billing/commands/usage.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +53 -1
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/PrepaidPanel.js +314 -0
- package/dist/modules/billing/components/PrepaidPanel.js.map +7 -0
- package/dist/modules/billing/components/accountNames.js +20 -0
- package/dist/modules/billing/components/accountNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +290 -0
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +6 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +107 -2
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +52 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/en.json +112 -4
- package/dist/modules/billing/i18n/pl.json +112 -4
- package/dist/modules/billing/index.js +1 -1
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountStatus.js +106 -0
- package/dist/modules/billing/lib/accountStatus.js.map +7 -0
- package/dist/modules/billing/lib/balanceEvents.js +48 -0
- package/dist/modules/billing/lib/balanceEvents.js.map +7 -0
- package/dist/modules/billing/lib/balanceLedger.js +136 -0
- package/dist/modules/billing/lib/balanceLedger.js.map +7 -0
- package/dist/modules/billing/lib/balanceStatus.js +29 -0
- package/dist/modules/billing/lib/balanceStatus.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +40 -2
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/creditStatus.js +65 -0
- package/dist/modules/billing/lib/creditStatus.js.map +7 -0
- package/dist/modules/billing/lib/money.js +47 -0
- package/dist/modules/billing/lib/money.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +45 -0
- package/dist/modules/billing/lib/prepaidConfig.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConsume.js +214 -0
- package/dist/modules/billing/lib/prepaidConsume.js.map +7 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js +285 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +62 -0
- package/dist/modules/billing/lib/topupCapture.js.map +7 -0
- package/dist/modules/billing/lib/topupReceipt.js +97 -0
- package/dist/modules/billing/lib/topupReceipt.js.map +7 -0
- package/dist/modules/billing/lib/topupTransition.js +19 -0
- package/dist/modules/billing/lib/topupTransition.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js +110 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js.map +7 -0
- package/dist/modules/billing/setup.js +32 -1
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +87 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js.map +7 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js +50 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js.map +7 -0
- package/package.json +9 -9
- package/src/modules/billing/__tests__/commands/invoices.test.ts +3 -3
- package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
- package/src/modules/billing/__tests__/commands/transactions.test.ts +107 -0
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
- package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
- package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
- package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
- package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
- package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
- package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
- package/src/modules/billing/__tests__/setup.test.ts +28 -2
- package/src/modules/billing/acl.ts +8 -0
- package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
- package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
- package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
- package/src/modules/billing/api/accounts/route.ts +7 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
- package/src/modules/billing/api/statements/generate/route.ts +78 -0
- package/src/modules/billing/api/statements/route.ts +117 -0
- package/src/modules/billing/api/topups/route.ts +154 -0
- package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
- package/src/modules/billing/api/transactions/route.ts +116 -0
- package/src/modules/billing/api/usage/route.ts +36 -5
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
- package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
- package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
- package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
- package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
- package/src/modules/billing/commands/accounts.ts +42 -1
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +26 -2
- package/src/modules/billing/commands/statements.ts +101 -0
- package/src/modules/billing/commands/topups.ts +181 -0
- package/src/modules/billing/commands/transactions.ts +152 -0
- package/src/modules/billing/commands/usage.ts +114 -4
- package/src/modules/billing/components/AccountForm.tsx +66 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
- package/src/modules/billing/components/accountNames.ts +23 -0
- package/src/modules/billing/data/entities.ts +383 -0
- package/src/modules/billing/data/entityIds.ts +5 -0
- package/src/modules/billing/data/validators.ts +144 -0
- package/src/modules/billing/events.ts +52 -0
- package/src/modules/billing/i18n/en.json +112 -4
- package/src/modules/billing/i18n/pl.json +112 -4
- package/src/modules/billing/index.ts +1 -1
- package/src/modules/billing/lib/accountStatus.ts +155 -0
- package/src/modules/billing/lib/balanceEvents.ts +74 -0
- package/src/modules/billing/lib/balanceLedger.ts +258 -0
- package/src/modules/billing/lib/balanceStatus.ts +78 -0
- package/src/modules/billing/lib/billRunEngine.ts +66 -5
- package/src/modules/billing/lib/creditStatus.ts +153 -0
- package/src/modules/billing/lib/money.ts +73 -0
- package/src/modules/billing/lib/prepaidConfig.ts +58 -0
- package/src/modules/billing/lib/prepaidConsume.ts +299 -0
- package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
- package/src/modules/billing/lib/topupCapture.ts +117 -0
- package/src/modules/billing/lib/topupReceipt.ts +153 -0
- package/src/modules/billing/lib/topupTransition.ts +31 -0
- package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
- package/src/modules/billing/setup.ts +45 -0
- package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
- package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
- package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
|
|
3
|
+
import { BillingAccountTransaction } from "../../data/entities.js";
|
|
4
|
+
import { billingEntityIds } from "../../data/entityIds.js";
|
|
5
|
+
import { billingTransactionListQuerySchema } from "../../data/validators.js";
|
|
6
|
+
import {
|
|
7
|
+
createBillingCrudOpenApi,
|
|
8
|
+
createPagedListResponseSchema
|
|
9
|
+
} from "../openapi.js";
|
|
10
|
+
const routeMetadata = {
|
|
11
|
+
GET: { requireAuth: true, requireFeatures: ["billing.balance.view"] }
|
|
12
|
+
};
|
|
13
|
+
const metadata = routeMetadata;
|
|
14
|
+
const billingTransactionListItemSchema = z.object({
|
|
15
|
+
id: z.string().uuid(),
|
|
16
|
+
bill_account_id: z.string().uuid(),
|
|
17
|
+
type: z.string(),
|
|
18
|
+
amount: z.string(),
|
|
19
|
+
currency_code: z.string(),
|
|
20
|
+
balance_after: z.string(),
|
|
21
|
+
usage_id: z.string().uuid().nullable(),
|
|
22
|
+
billing_item_id: z.string().uuid().nullable(),
|
|
23
|
+
topup_id: z.string().uuid().nullable(),
|
|
24
|
+
statement_id: z.string().uuid().nullable(),
|
|
25
|
+
description: z.string(),
|
|
26
|
+
source_ref: z.string().nullable(),
|
|
27
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
28
|
+
user_id: z.string().uuid().nullable(),
|
|
29
|
+
organization_id: z.string().uuid(),
|
|
30
|
+
tenant_id: z.string().uuid(),
|
|
31
|
+
created_at: z.string()
|
|
32
|
+
});
|
|
33
|
+
const crud = makeCrudRoute({
|
|
34
|
+
metadata: routeMetadata,
|
|
35
|
+
orm: {
|
|
36
|
+
entity: BillingAccountTransaction,
|
|
37
|
+
idField: "id",
|
|
38
|
+
orgField: "organizationId",
|
|
39
|
+
tenantField: "tenantId",
|
|
40
|
+
softDeleteField: "deletedAt"
|
|
41
|
+
},
|
|
42
|
+
list: {
|
|
43
|
+
schema: billingTransactionListQuerySchema,
|
|
44
|
+
entityId: billingEntityIds.transaction,
|
|
45
|
+
fields: [
|
|
46
|
+
"id",
|
|
47
|
+
"bill_account_id",
|
|
48
|
+
"type",
|
|
49
|
+
"amount",
|
|
50
|
+
"currency_code",
|
|
51
|
+
"balance_after",
|
|
52
|
+
"usage_id",
|
|
53
|
+
"billing_item_id",
|
|
54
|
+
"topup_id",
|
|
55
|
+
"statement_id",
|
|
56
|
+
"description",
|
|
57
|
+
"source_ref",
|
|
58
|
+
"metadata",
|
|
59
|
+
"user_id",
|
|
60
|
+
"organization_id",
|
|
61
|
+
"tenant_id",
|
|
62
|
+
"created_at"
|
|
63
|
+
],
|
|
64
|
+
sortFieldMap: {
|
|
65
|
+
createdAt: "created_at"
|
|
66
|
+
},
|
|
67
|
+
buildFilters: async (query) => {
|
|
68
|
+
const filters = {};
|
|
69
|
+
if (typeof query.id === "string" && query.id) {
|
|
70
|
+
filters.id = { $eq: query.id };
|
|
71
|
+
}
|
|
72
|
+
if (typeof query.billAccountId === "string" && query.billAccountId) {
|
|
73
|
+
filters.bill_account_id = { $eq: query.billAccountId };
|
|
74
|
+
}
|
|
75
|
+
if (typeof query.type === "string" && query.type) {
|
|
76
|
+
filters.type = { $eq: query.type };
|
|
77
|
+
}
|
|
78
|
+
if (query.createdFrom instanceof Date) {
|
|
79
|
+
filters.created_at = Object.assign(
|
|
80
|
+
filters.created_at ?? {},
|
|
81
|
+
{ $gte: query.createdFrom }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (query.createdTo instanceof Date) {
|
|
85
|
+
filters.created_at = Object.assign(
|
|
86
|
+
filters.created_at ?? {},
|
|
87
|
+
{ $lte: query.createdTo }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return filters;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
// Append-only — no create/update/delete on this surface. Manual
|
|
94
|
+
// adjustments go through POST /api/billing/transactions/adjust.
|
|
95
|
+
actions: {}
|
|
96
|
+
});
|
|
97
|
+
const GET = crud.GET;
|
|
98
|
+
const openApi = createBillingCrudOpenApi({
|
|
99
|
+
resourceName: "BillingAccountTransaction",
|
|
100
|
+
pluralName: "BillingAccountTransactions",
|
|
101
|
+
querySchema: billingTransactionListQuerySchema,
|
|
102
|
+
listResponseSchema: createPagedListResponseSchema(billingTransactionListItemSchema)
|
|
103
|
+
});
|
|
104
|
+
export {
|
|
105
|
+
GET,
|
|
106
|
+
metadata,
|
|
107
|
+
openApi
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/billing/api/transactions/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { BillingAccountTransaction } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport { billingTransactionListQuerySchema } from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n} from '../openapi'\n\n/**\n * `GET /api/billing/transactions` \u2014 paged, append-only balance ledger\n * (SPEC-002). Filter by account, type, and a `created_at` window. The\n * write path is the command bus (real-time consume, top-up capture, period\n * close, manual adjustment) \u2014 there is no create/update/delete here.\n */\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.balance.view'] },\n}\n\nexport const metadata = routeMetadata\n\nconst billingTransactionListItemSchema = z.object({\n id: z.string().uuid(),\n bill_account_id: z.string().uuid(),\n type: z.string(),\n amount: z.string(),\n currency_code: z.string(),\n balance_after: z.string(),\n usage_id: z.string().uuid().nullable(),\n billing_item_id: z.string().uuid().nullable(),\n topup_id: z.string().uuid().nullable(),\n statement_id: z.string().uuid().nullable(),\n description: z.string(),\n source_ref: z.string().nullable(),\n metadata: z.record(z.string(), z.unknown()).nullable(),\n user_id: z.string().uuid().nullable(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n})\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccountTransaction,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingTransactionListQuerySchema,\n entityId: billingEntityIds.transaction,\n fields: [\n 'id',\n 'bill_account_id',\n 'type',\n 'amount',\n 'currency_code',\n 'balance_after',\n 'usage_id',\n 'billing_item_id',\n 'topup_id',\n 'statement_id',\n 'description',\n 'source_ref',\n 'metadata',\n 'user_id',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n if (typeof query.billAccountId === 'string' && query.billAccountId) {\n filters.bill_account_id = { $eq: query.billAccountId }\n }\n if (typeof query.type === 'string' && query.type) {\n filters.type = { $eq: query.type }\n }\n if (query.createdFrom instanceof Date) {\n filters.created_at = Object.assign(\n (filters.created_at as Record<string, unknown>) ?? {},\n { $gte: query.createdFrom },\n )\n }\n if (query.createdTo instanceof Date) {\n filters.created_at = Object.assign(\n (filters.created_at as Record<string, unknown>) ?? {},\n { $lte: query.createdTo },\n )\n }\n return filters\n },\n },\n // Append-only \u2014 no create/update/delete on this surface. Manual\n // adjustments go through POST /api/billing/transactions/adjust.\n actions: {},\n})\n\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingAccountTransaction',\n pluralName: 'BillingAccountTransactions',\n querySchema: billingTransactionListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingTransactionListItemSchema),\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -36,7 +36,16 @@ const billingUsageListItemSchema = z.object({
|
|
|
36
36
|
});
|
|
37
37
|
const usageCreateResponseSchema = z.object({
|
|
38
38
|
id: z.string().uuid().nullable(),
|
|
39
|
-
deduplicated: z.boolean()
|
|
39
|
+
deduplicated: z.boolean(),
|
|
40
|
+
// Prepaid accounts (SPEC-002) get the real-time balance body. Absent for
|
|
41
|
+
// postpaid accounts (the legacy append-only response is unchanged).
|
|
42
|
+
ratedAmount: z.string().optional(),
|
|
43
|
+
currencyCode: z.string().optional(),
|
|
44
|
+
balance: z.string().optional(),
|
|
45
|
+
balanceStatus: z.enum(["ok", "low", "exhausted"]).optional(),
|
|
46
|
+
creditStatus: z.enum(["within_limit", "near_limit", "over_limit"]).optional(),
|
|
47
|
+
tierBreakdown: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
48
|
+
warning: z.string().nullable().optional()
|
|
40
49
|
});
|
|
41
50
|
const rawBodySchema = z.object({}).passthrough();
|
|
42
51
|
const crud = makeCrudRoute({
|
|
@@ -107,10 +116,25 @@ const crud = makeCrudRoute({
|
|
|
107
116
|
const scoped = withScopedPayload(raw ?? {}, ctx, translate);
|
|
108
117
|
return billingUsageCreateSchema.parse(scoped);
|
|
109
118
|
},
|
|
110
|
-
response: ({ result }) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
response: ({ result }) => {
|
|
120
|
+
const base = {
|
|
121
|
+
id: result?.id ?? null,
|
|
122
|
+
deduplicated: Boolean(result?.deduplicated)
|
|
123
|
+
};
|
|
124
|
+
if (result && "balance" in result && result.balance !== void 0) {
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
ratedAmount: result.ratedAmount,
|
|
128
|
+
currencyCode: result.currencyCode,
|
|
129
|
+
balance: result.balance,
|
|
130
|
+
balanceStatus: result.balanceStatus,
|
|
131
|
+
creditStatus: result.creditStatus,
|
|
132
|
+
tierBreakdown: result.tierBreakdown ?? [],
|
|
133
|
+
warning: result.warning ?? null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return base;
|
|
137
|
+
},
|
|
114
138
|
status: 200
|
|
115
139
|
}
|
|
116
140
|
// Usage has no update or delete by design — see file header.
|
|
@@ -126,7 +150,7 @@ const openApi = createBillingCrudOpenApi({
|
|
|
126
150
|
create: {
|
|
127
151
|
schema: billingUsageCreateSchema,
|
|
128
152
|
responseSchema: usageCreateResponseSchema,
|
|
129
|
-
description: "Ingests a pre-aggregated usage record. Idempotent via `sourceRef`. Usage records are immutable once persisted \u2014 to correct a mis-aggregation, post a `one_time` Billing Item credit (the spec's refund model)."
|
|
153
|
+
description: "Ingests a pre-aggregated usage record. Idempotent via `sourceRef`. Usage records are immutable once persisted \u2014 to correct a mis-aggregation, post a `one_time` Billing Item credit (the spec's refund model). For a PREPAID account (SPEC-002) the upload is rated synchronously, debited from the balance, and the response carries `ratedAmount`, the remaining `balance`, `balanceStatus` (ok/low/exhausted), `creditStatus`, and the `tierBreakdown`. Usage is never rejected \u2014 the balance may go negative and the client is expected to throttle itself."
|
|
130
154
|
}
|
|
131
155
|
});
|
|
132
156
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/billing/api/usage/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingAccountUsage } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingUsageCreateSchema,\n billingUsageListQuerySchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \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\u2500\u2500\u2500\n//\n// Usage is append-only \u2014 no update or delete. The narrow scope here\n// lets us mint API keys for external metering systems with ONLY\n// `billing.usage.ingest`, eliminating an entire blast radius.\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.usage.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.usage.ingest'] },\n}\n\nexport const metadata = routeMetadata\n\nconst billingUsageListItemSchema = z.object({\n id: z.string().uuid(),\n billAccountId: z.string().uuid(),\n uomCode: z.string(),\n quantity: z.string(), // numeric(18,4) \u2192 string\n periodStart: z.string(),\n periodEnd: z.string(),\n lineDescription: z.string().nullable(),\n sourceRef: z.string().nullable(),\n ratedInBillRunId: z.string().uuid().nullable(),\n isActive: z.boolean(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst usageCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n deduplicated: z.boolean(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccountUsage,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingUsageListQuerySchema,\n entityId: billingEntityIds.usage,\n fields: [\n 'id',\n 'bill_account_id',\n 'uom_code',\n 'quantity',\n 'period_start',\n 'period_end',\n 'line_description',\n 'source_ref',\n 'rated_in_bill_run_id',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n periodStart: 'period_start',\n periodEnd: 'period_end',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.billAccountId === 'string' && query.billAccountId) {\n filters.bill_account_id = { $eq: query.billAccountId }\n }\n if (typeof query.uomCode === 'string' && query.uomCode) {\n filters.uom_code = { $eq: query.uomCode }\n }\n if (typeof query.rated === 'boolean') {\n filters.rated_in_bill_run_id = query.rated ? { $ne: null } : { $eq: null }\n }\n if (query.periodEndFrom instanceof Date) {\n filters.period_end = Object.assign(\n (filters.period_end as Record<string, unknown>) ?? {},\n { $gte: query.periodEndFrom },\n )\n }\n if (query.periodEndTo instanceof Date) {\n filters.period_end = Object.assign(\n (filters.period_end as Record<string, unknown>) ?? {},\n { $lte: query.periodEndTo },\n )\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.usage.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingUsageCreateSchema.parse(scoped)\n },\n response: ({ result }) =>
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAQP,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,oBAAoB,EAAE;AAAA,EAClE,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACvE;AAEO,MAAM,WAAW;AAExB,MAAM,6BAA6B,EAAE,OAAO;AAAA,EAC1C,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,EACpB,eAAe,EAAE,OAAO,EAAE,KAAK;AAAA,EAC/B,SAAS,EAAE,OAAO;AAAA,EAClB,UAAU,EAAE,OAAO;AAAA;AAAA,EACnB,aAAa,EAAE,OAAO;AAAA,EACtB,WAAW,EAAE,OAAO;AAAA,EACpB,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,EACrC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,kBAAkB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC7C,UAAU,EAAE,QAAQ;AAAA,EACpB,gBAAgB,EAAE,OAAO,EAAE,KAAK;AAAA,EAChC,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,WAAW,EAAE,OAAO;AAAA,EACpB,WAAW,EAAE,OAAO;AACtB,CAAC;AAED,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,cAAc,EAAE,QAAQ;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingAccountUsage } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingUsageCreateSchema,\n billingUsageListQuerySchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \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\u2500\u2500\u2500\n//\n// Usage is append-only \u2014 no update or delete. The narrow scope here\n// lets us mint API keys for external metering systems with ONLY\n// `billing.usage.ingest`, eliminating an entire blast radius.\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.usage.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.usage.ingest'] },\n}\n\nexport const metadata = routeMetadata\n\nconst billingUsageListItemSchema = z.object({\n id: z.string().uuid(),\n billAccountId: z.string().uuid(),\n uomCode: z.string(),\n quantity: z.string(), // numeric(18,4) \u2192 string\n periodStart: z.string(),\n periodEnd: z.string(),\n lineDescription: z.string().nullable(),\n sourceRef: z.string().nullable(),\n ratedInBillRunId: z.string().uuid().nullable(),\n isActive: z.boolean(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst usageCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n deduplicated: z.boolean(),\n // Prepaid accounts (SPEC-002) get the real-time balance body. Absent for\n // postpaid accounts (the legacy append-only response is unchanged).\n ratedAmount: z.string().optional(),\n currencyCode: z.string().optional(),\n balance: z.string().optional(),\n balanceStatus: z.enum(['ok', 'low', 'exhausted']).optional(),\n creditStatus: z.enum(['within_limit', 'near_limit', 'over_limit']).optional(),\n tierBreakdown: z.array(z.record(z.string(), z.unknown())).optional(),\n warning: z.string().nullable().optional(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccountUsage,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingUsageListQuerySchema,\n entityId: billingEntityIds.usage,\n fields: [\n 'id',\n 'bill_account_id',\n 'uom_code',\n 'quantity',\n 'period_start',\n 'period_end',\n 'line_description',\n 'source_ref',\n 'rated_in_bill_run_id',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n periodStart: 'period_start',\n periodEnd: 'period_end',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.billAccountId === 'string' && query.billAccountId) {\n filters.bill_account_id = { $eq: query.billAccountId }\n }\n if (typeof query.uomCode === 'string' && query.uomCode) {\n filters.uom_code = { $eq: query.uomCode }\n }\n if (typeof query.rated === 'boolean') {\n filters.rated_in_bill_run_id = query.rated ? { $ne: null } : { $eq: null }\n }\n if (query.periodEndFrom instanceof Date) {\n filters.period_end = Object.assign(\n (filters.period_end as Record<string, unknown>) ?? {},\n { $gte: query.periodEndFrom },\n )\n }\n if (query.periodEndTo instanceof Date) {\n filters.period_end = Object.assign(\n (filters.period_end as Record<string, unknown>) ?? {},\n { $lte: query.periodEndTo },\n )\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.usage.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingUsageCreateSchema.parse(scoped)\n },\n response: ({ result }) => {\n const base = {\n id: result?.id ?? null,\n deduplicated: Boolean(result?.deduplicated),\n }\n // Pass through the prepaid balance body when present (prepaid\n // accounts only); postpaid responses keep the legacy 2-field shape.\n if (result && 'balance' in result && result.balance !== undefined) {\n return {\n ...base,\n ratedAmount: result.ratedAmount,\n currencyCode: result.currencyCode,\n balance: result.balance,\n balanceStatus: result.balanceStatus,\n creditStatus: result.creditStatus,\n tierBreakdown: result.tierBreakdown ?? [],\n warning: result.warning ?? null,\n }\n }\n return base\n },\n status: 200,\n },\n // Usage has no update or delete by design \u2014 see file header.\n },\n})\n\nconst { POST } = crud\nexport { POST }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingUsageRecord',\n pluralName: 'BillingUsageRecords',\n querySchema: billingUsageListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingUsageListItemSchema),\n create: {\n schema: billingUsageCreateSchema,\n responseSchema: usageCreateResponseSchema,\n description:\n 'Ingests a pre-aggregated usage record. Idempotent via `sourceRef`. ' +\n 'Usage records are immutable once persisted \u2014 to correct a mis-aggregation, ' +\n \"post a `one_time` Billing Item credit (the spec's refund model). \" +\n 'For a PREPAID account (SPEC-002) the upload is rated synchronously, ' +\n 'debited from the balance, and the response carries `ratedAmount`, the ' +\n 'remaining `balance`, `balanceStatus` (ok/low/exhausted), `creditStatus`, ' +\n 'and the `tierBreakdown`. Usage is never rejected \u2014 the balance may go ' +\n 'negative and the client is expected to throttle itself.',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { normalizeCrudServerError } from "@open-mercato/ui/backend/utils/serverErrors";
|
|
21
21
|
import { AccountForm } from "../../../../components/AccountForm.js";
|
|
22
22
|
import { DetailCard, DetailField } from "../../../../components/DetailFields.js";
|
|
23
|
+
import { PrepaidPanel } from "../../../../components/PrepaidPanel.js";
|
|
23
24
|
function formatDate(value) {
|
|
24
25
|
if (!value) return "\u2014";
|
|
25
26
|
const parsed = new Date(value);
|
|
@@ -43,7 +44,12 @@ function toFormValues(row) {
|
|
|
43
44
|
country: row.invoice_address?.country ?? ""
|
|
44
45
|
},
|
|
45
46
|
nextBillDate: row.next_bill_date?.slice(0, 10) ?? "",
|
|
46
|
-
isActive: row.is_active
|
|
47
|
+
isActive: row.is_active,
|
|
48
|
+
billingMode: row.billing_mode,
|
|
49
|
+
creditLimit: row.credit_limit ?? "0",
|
|
50
|
+
// The threshold lives on the balance row, not the account row — blank
|
|
51
|
+
// means "leave unchanged" on save.
|
|
52
|
+
lowBalanceThreshold: ""
|
|
47
53
|
};
|
|
48
54
|
}
|
|
49
55
|
function AccountView({ row }) {
|
|
@@ -58,6 +64,8 @@ function AccountView({ row }) {
|
|
|
58
64
|
return /* @__PURE__ */ jsxs(DetailCard, { children: [
|
|
59
65
|
/* @__PURE__ */ jsx(DetailField, { label: t("billing.accounts.form.customer_id", "Customer ID"), children: /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: row.customer_id }) }),
|
|
60
66
|
/* @__PURE__ */ jsx(DetailField, { label: t("billing.accounts.columns.currency", "Currency"), children: row.currency_code }),
|
|
67
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.accounts.columns.mode", "Mode"), children: row.billing_mode === "prepaid" ? /* @__PURE__ */ jsx(Tag, { variant: "brand", children: t("billing.mode.prepaid", "Prepaid") }) : /* @__PURE__ */ jsx(Tag, { variant: "neutral", children: t("billing.mode.postpaid", "Postpaid") }) }),
|
|
68
|
+
/* @__PURE__ */ jsx(DetailField, { label: t("billing.accounts.columns.credit_limit", "Credit limit"), children: /* @__PURE__ */ jsx("span", { className: "font-mono tabular-nums", children: row.credit_limit }) }),
|
|
61
69
|
/* @__PURE__ */ jsx(DetailField, { label: t("billing.common.id", "ID"), children: /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: row.id }) }),
|
|
62
70
|
/* @__PURE__ */ jsxs(DetailField, { label: t("billing.accounts.columns.cycle", "Cycle"), children: [
|
|
63
71
|
row.bill_cycle,
|
|
@@ -281,6 +289,12 @@ function BillingAccountDetailPage(props) {
|
|
|
281
289
|
isActive: values.isActive
|
|
282
290
|
};
|
|
283
291
|
payload.taxId = values.taxId.trim() === "" ? null : values.taxId.trim();
|
|
292
|
+
const creditLimit = Number.parseFloat(values.creditLimit);
|
|
293
|
+
if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit;
|
|
294
|
+
if (row.billing_mode === "prepaid" && values.lowBalanceThreshold.trim()) {
|
|
295
|
+
const threshold = Number.parseFloat(values.lowBalanceThreshold);
|
|
296
|
+
if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold;
|
|
297
|
+
}
|
|
284
298
|
await apiCallOrThrow("/api/billing/accounts", {
|
|
285
299
|
method: "PUT",
|
|
286
300
|
headers: { "Content-Type": "application/json" },
|
|
@@ -390,6 +404,7 @@ function BillingAccountDetailPage(props) {
|
|
|
390
404
|
onCancel: () => setEditing(false)
|
|
391
405
|
}
|
|
392
406
|
) : /* @__PURE__ */ jsx(AccountView, { row }),
|
|
407
|
+
row.billing_mode === "prepaid" ? /* @__PURE__ */ jsx(PrepaidPanel, { accountId: row.id, currencyCode: row.currency_code }) : null,
|
|
393
408
|
/* @__PURE__ */ jsx(AccountItemsSection, { accountId: row.id }),
|
|
394
409
|
/* @__PURE__ */ jsx(AccountUsageSection, { accountId: row.id }),
|
|
395
410
|
ConfirmDialogElement
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/billing/backend/billing/accounts/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Pencil, Plus } from 'lucide-react'\nimport { useParams, useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\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 { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\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 { AccountForm, type AccountFormValues } from '../../../../components/AccountForm'\nimport { DetailCard, DetailField } from '../../../../components/DetailFields'\n\n/**\n * `/backend/billing/accounts/[id]` \u2014 Billing Account detail.\n *\n * Opens read-only (field grid + the account's items). The `Edit`\n * button swaps the grid for the `AccountForm`; Save / Cancel return\n * to the read-only view. Loads via `GET /api/billing/accounts?id=<id>`,\n * edits through `PUT`, soft-delete through `DELETE`.\n *\n * `customerId` + `currencyCode` are immutable on update \u2014 the form\n * disables both in `mode='edit'` and the schema rejects them per\n * `billingAccountUpdateSchema`.\n */\n\n// API list rows are snake_case (see `api/accounts/route.ts` `fields`).\ntype AccountRow = {\n id: string\n customer_id: string\n name: string\n currency_code: string\n bill_cycle: 'weekly' | 'monthly' | 'quarterly' | 'annually'\n bill_cycle_anchor: string\n invoice_email: string\n invoice_language: string\n tax_id: string | null\n invoice_address: {\n line1?: string\n line2?: string\n city?: string\n postal_code?: string\n country?: string\n } | null\n next_bill_date: string\n last_bill_date: string | null\n is_active: boolean\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = { items: AccountRow[] }\n\n// Minimal item shape for the embedded \"Items\" table.\ntype AccountItemRow = {\n id: string\n description: string\n type: 'one_time' | 'recurring' | 'usage'\n bill_start_date: string\n is_active: boolean\n}\n\n// Minimal usage shape for the embedded \"Usage\" table. Mirrors the\n// snake_case `fields` projection in `api/usage/route.ts`. Usage is\n// append-only \u2014 `rated_in_bill_run_id` is null until a Bill Run rates it.\ntype AccountUsageRow = {\n id: string\n uom_code: string\n quantity: string\n period_start: string\n period_end: string\n line_description: string | null\n rated_in_bill_run_id: string | null\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 toFormValues(row: AccountRow): AccountFormValues {\n return {\n customerId: row.customer_id,\n name: row.name,\n currencyCode: row.currency_code,\n billCycle: row.bill_cycle,\n billCycleAnchor: row.bill_cycle_anchor,\n invoiceEmail: row.invoice_email,\n invoiceLanguage: row.invoice_language,\n taxId: row.tax_id ?? '',\n invoiceAddress: {\n line1: row.invoice_address?.line1 ?? '',\n line2: row.invoice_address?.line2 ?? '',\n city: row.invoice_address?.city ?? '',\n postal_code: row.invoice_address?.postal_code ?? '',\n country: row.invoice_address?.country ?? '',\n },\n nextBillDate: row.next_bill_date?.slice(0, 10) ?? '',\n isActive: row.is_active,\n }\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 AccountView({ row }: { row: AccountRow }) {\n const t = useT()\n const addr = row.invoice_address\n const addressText = addr\n ? [\n addr.line1,\n addr.line2,\n [addr.postal_code, addr.city].filter(Boolean).join(' '),\n addr.country,\n ]\n .map((part) => (part ?? '').trim())\n .filter(Boolean)\n .join(', ')\n : ''\n return (\n <DetailCard>\n <DetailField label={t('billing.accounts.form.customer_id', 'Customer ID')}>\n <span className=\"font-mono text-xs\">{row.customer_id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.currency', 'Currency')}>\n {row.currency_code}\n </DetailField>\n <DetailField label={t('billing.common.id', 'ID')}>\n <span className=\"font-mono text-xs\">{row.id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.cycle', 'Cycle')}>\n {row.bill_cycle}\n <span className=\"text-muted-foreground\"> \u00B7 {row.bill_cycle_anchor}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.next_bill', 'Next bill')}>\n {formatDate(row.next_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.last_bill', 'Last bill')}>\n {formatDate(row.last_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_email', 'Invoice email')}>\n {row.invoice_email}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_language', 'Invoice language')}>\n {row.invoice_language}\n </DetailField>\n <DetailField label={t('billing.accounts.form.tax_id', 'Tax ID')}>\n {row.tax_id || '\u2014'}\n </DetailField>\n <DetailField\n label={t('billing.accounts.form.address.title', 'Invoice address')}\n fullWidth\n >\n {addressText || '\u2014'}\n </DetailField>\n </DetailCard>\n )\n}\n\n// \u2500\u2500\u2500 Embedded items table \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 AccountItemsSection({ accountId }: { accountId: string }) {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<AccountItemRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountItemRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/items?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=createdAt&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountItemRow>[]>(\n () => [\n {\n accessorKey: 'description',\n header: t('billing.items.columns.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {row.original.description}\n </span>\n ),\n },\n {\n accessorKey: 'type',\n header: t('billing.items.columns.type', 'Type'),\n cell: ({ row }) => <Tag variant=\"info\">{row.original.type}</Tag>,\n },\n {\n accessorKey: 'bill_start_date',\n header: t('billing.items.columns.start', 'Start'),\n cell: ({ row }) => formatDate(row.original.bill_start_date),\n },\n {\n accessorKey: 'is_active',\n header: t('billing.items.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.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 ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.items.title', 'Items')}\n columns={columns}\n data={rows}\n isLoading={loading}\n onRowClick={(row) => router.push(`/backend/billing/items/${row.id}`)}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.items.empty', 'No items for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Embedded usage table \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 AccountUsageSection({ accountId }: { accountId: string }) {\n const t = useT()\n const [rows, setRows] = React.useState<AccountUsageRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountUsageRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/usage?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=periodStart&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountUsageRow>[]>(\n () => [\n {\n accessorKey: 'uom_code',\n header: t('billing.usage.columns.uom', 'Unit'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">{row.original.uom_code}</span>\n ),\n },\n {\n accessorKey: 'quantity',\n header: t('billing.usage.columns.quantity', 'Quantity'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.quantity}</span>\n ),\n },\n {\n accessorKey: 'period_start',\n header: t('billing.usage.columns.period', 'Period'),\n cell: ({ row }) =>\n `${formatDate(row.original.period_start)} \u2013 ${formatDate(row.original.period_end)}`,\n },\n {\n accessorKey: 'line_description',\n header: t('billing.usage.columns.description', 'Description'),\n cell: ({ row }) => row.original.line_description || '\u2014',\n },\n {\n accessorKey: 'rated_in_bill_run_id',\n header: t('billing.usage.columns.rated', 'Rated'),\n cell: ({ row }) =>\n row.original.rated_in_bill_run_id ? (\n // Rated records carry the Bill Run that priced them \u2014 link to it.\n // Unrated records have no destination, so they stay non-interactive\n // (no misleading row-level click affordance).\n <Link\n href={`/backend/billing/runs/${row.original.rated_in_bill_run_id}`}\n className=\"rounded-sm hover:underline\"\n >\n <Tag variant=\"success\">{t('billing.usage.rated.yes', 'Rated')}</Tag>\n </Link>\n ) : (\n <Tag variant=\"default\">{t('billing.usage.rated.no', 'Unrated')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.usage.title', 'Usage')}\n columns={columns}\n data={rows}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.usage.empty', 'No usage records for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\nexport default function BillingAccountDetailPage(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 accountId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [row, setRow] = React.useState<AccountRow | 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 (!accountId) return\n setLoading(true)\n setError(null)\n try {\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/accounts?id=${accountId}&pageSize=1`,\n )\n const found = result.items?.[0]\n if (!found) {\n setError(t('billing.accounts.detail.error.not_found', 'Account not found'))\n return\n }\n setRow(found)\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setError(message || t('billing.accounts.detail.error.load', 'Failed to load account'))\n } finally {\n setLoading(false)\n }\n }, [accountId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const handleSubmit = React.useCallback(\n async (values: AccountFormValues) => {\n if (!row) return\n setSaving(true)\n try {\n const payload: Record<string, unknown> = {\n id: row.id,\n name: values.name.trim(),\n billCycle: values.billCycle,\n billCycleAnchor: values.billCycleAnchor.trim(),\n invoiceEmail: values.invoiceEmail.trim(),\n invoiceLanguage: values.invoiceLanguage.trim().toLowerCase(),\n invoiceAddress: {\n line1: values.invoiceAddress.line1.trim(),\n ...(values.invoiceAddress.line2.trim()\n ? { line2: values.invoiceAddress.line2.trim() }\n : {}),\n city: values.invoiceAddress.city.trim(),\n postal_code: values.invoiceAddress.postal_code.trim(),\n country: values.invoiceAddress.country.trim().toUpperCase(),\n },\n nextBillDate: values.nextBillDate,\n isActive: values.isActive,\n }\n payload.taxId = values.taxId.trim() === '' ? null : values.taxId.trim()\n\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n flash(t('billing.accounts.detail.save.success', 'Account updated'), 'success')\n setEditing(false)\n void load()\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message\n ? `${message} \u2014 ${fieldDetail}`\n : fieldDetail || message\n flash(detailed || t('billing.accounts.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.accounts.detail.delete.confirm.title', 'Delete account?'),\n text: t(\n 'billing.accounts.detail.delete.confirm.text',\n \"Soft-deletes the account. Its items + usage records stay in the database for audit; the Bill Run engine will stop picking it up immediately.\",\n ),\n confirmText: t('billing.accounts.detail.delete.confirm.button', 'Soft delete'),\n variant: 'destructive',\n })\n if (!ok) return\n setDeleting(true)\n try {\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('billing.accounts.detail.delete.success', 'Account deleted'), 'success')\n router.push('/backend/billing/accounts')\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.accounts.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.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n\n if (loading || !row) {\n return (\n <Page>\n <PageHeader title={t('billing.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <LoadingMessage\n label={t('billing.accounts.detail.loading', 'Loading account\u2026')}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/accounts\"\n entityTypeLabel={t('billing.accounts.detail.title', 'Billing Account')}\n title={row.name}\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 <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/items/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_item', 'Add item')}\n </Link>\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/usage/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_usage', 'Add usage')}\n </Link>\n </Button>\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.accounts.detail.delete.in_progress', 'Deleting\u2026')\n : t('billing.accounts.detail.delete.action', 'Soft delete')}\n </Button>\n </>\n }\n />\n <PageBody>\n {editing ? (\n <AccountForm\n mode=\"edit\"\n initial={toFormValues(row)}\n submitting={saving}\n onSubmit={handleSubmit}\n onCancel={() => setEditing(false)}\n />\n ) : (\n <AccountView row={row} />\n )}\n <AccountItemsSection accountId={row.id} />\n <AccountUsageSection accountId={row.id} />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Pencil, Plus } from 'lucide-react'\nimport { useParams, useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\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 { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\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 { AccountForm, type AccountFormValues } from '../../../../components/AccountForm'\nimport { DetailCard, DetailField } from '../../../../components/DetailFields'\nimport { PrepaidPanel } from '../../../../components/PrepaidPanel'\n\n/**\n * `/backend/billing/accounts/[id]` \u2014 Billing Account detail.\n *\n * Opens read-only (field grid + the account's items). The `Edit`\n * button swaps the grid for the `AccountForm`; Save / Cancel return\n * to the read-only view. Loads via `GET /api/billing/accounts?id=<id>`,\n * edits through `PUT`, soft-delete through `DELETE`.\n *\n * `customerId` + `currencyCode` are immutable on update \u2014 the form\n * disables both in `mode='edit'` and the schema rejects them per\n * `billingAccountUpdateSchema`.\n */\n\n// API list rows are snake_case (see `api/accounts/route.ts` `fields`).\ntype AccountRow = {\n id: string\n customer_id: string\n name: string\n currency_code: string\n bill_cycle: 'weekly' | 'monthly' | 'quarterly' | 'annually'\n bill_cycle_anchor: string\n invoice_email: string\n invoice_language: string\n tax_id: string | null\n invoice_address: {\n line1?: string\n line2?: string\n city?: string\n postal_code?: string\n country?: string\n } | null\n next_bill_date: string\n last_bill_date: string | null\n billing_mode: 'postpaid' | 'prepaid'\n credit_limit: string\n is_active: boolean\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = { items: AccountRow[] }\n\n// Minimal item shape for the embedded \"Items\" table.\ntype AccountItemRow = {\n id: string\n description: string\n type: 'one_time' | 'recurring' | 'usage'\n bill_start_date: string\n is_active: boolean\n}\n\n// Minimal usage shape for the embedded \"Usage\" table. Mirrors the\n// snake_case `fields` projection in `api/usage/route.ts`. Usage is\n// append-only \u2014 `rated_in_bill_run_id` is null until a Bill Run rates it.\ntype AccountUsageRow = {\n id: string\n uom_code: string\n quantity: string\n period_start: string\n period_end: string\n line_description: string | null\n rated_in_bill_run_id: string | null\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 toFormValues(row: AccountRow): AccountFormValues {\n return {\n customerId: row.customer_id,\n name: row.name,\n currencyCode: row.currency_code,\n billCycle: row.bill_cycle,\n billCycleAnchor: row.bill_cycle_anchor,\n invoiceEmail: row.invoice_email,\n invoiceLanguage: row.invoice_language,\n taxId: row.tax_id ?? '',\n invoiceAddress: {\n line1: row.invoice_address?.line1 ?? '',\n line2: row.invoice_address?.line2 ?? '',\n city: row.invoice_address?.city ?? '',\n postal_code: row.invoice_address?.postal_code ?? '',\n country: row.invoice_address?.country ?? '',\n },\n nextBillDate: row.next_bill_date?.slice(0, 10) ?? '',\n isActive: row.is_active,\n billingMode: row.billing_mode,\n creditLimit: row.credit_limit ?? '0',\n // The threshold lives on the balance row, not the account row \u2014 blank\n // means \"leave unchanged\" on save.\n lowBalanceThreshold: '',\n }\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 AccountView({ row }: { row: AccountRow }) {\n const t = useT()\n const addr = row.invoice_address\n const addressText = addr\n ? [\n addr.line1,\n addr.line2,\n [addr.postal_code, addr.city].filter(Boolean).join(' '),\n addr.country,\n ]\n .map((part) => (part ?? '').trim())\n .filter(Boolean)\n .join(', ')\n : ''\n return (\n <DetailCard>\n <DetailField label={t('billing.accounts.form.customer_id', 'Customer ID')}>\n <span className=\"font-mono text-xs\">{row.customer_id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.currency', 'Currency')}>\n {row.currency_code}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.mode', 'Mode')}>\n {row.billing_mode === 'prepaid' ? (\n <Tag variant=\"brand\">{t('billing.mode.prepaid', 'Prepaid')}</Tag>\n ) : (\n <Tag variant=\"neutral\">{t('billing.mode.postpaid', 'Postpaid')}</Tag>\n )}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.credit_limit', 'Credit limit')}>\n <span className=\"font-mono tabular-nums\">{row.credit_limit}</span>\n </DetailField>\n <DetailField label={t('billing.common.id', 'ID')}>\n <span className=\"font-mono text-xs\">{row.id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.cycle', 'Cycle')}>\n {row.bill_cycle}\n <span className=\"text-muted-foreground\"> \u00B7 {row.bill_cycle_anchor}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.next_bill', 'Next bill')}>\n {formatDate(row.next_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.last_bill', 'Last bill')}>\n {formatDate(row.last_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_email', 'Invoice email')}>\n {row.invoice_email}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_language', 'Invoice language')}>\n {row.invoice_language}\n </DetailField>\n <DetailField label={t('billing.accounts.form.tax_id', 'Tax ID')}>\n {row.tax_id || '\u2014'}\n </DetailField>\n <DetailField\n label={t('billing.accounts.form.address.title', 'Invoice address')}\n fullWidth\n >\n {addressText || '\u2014'}\n </DetailField>\n </DetailCard>\n )\n}\n\n// \u2500\u2500\u2500 Embedded items table \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 AccountItemsSection({ accountId }: { accountId: string }) {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<AccountItemRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountItemRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/items?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=createdAt&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountItemRow>[]>(\n () => [\n {\n accessorKey: 'description',\n header: t('billing.items.columns.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {row.original.description}\n </span>\n ),\n },\n {\n accessorKey: 'type',\n header: t('billing.items.columns.type', 'Type'),\n cell: ({ row }) => <Tag variant=\"info\">{row.original.type}</Tag>,\n },\n {\n accessorKey: 'bill_start_date',\n header: t('billing.items.columns.start', 'Start'),\n cell: ({ row }) => formatDate(row.original.bill_start_date),\n },\n {\n accessorKey: 'is_active',\n header: t('billing.items.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.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 ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.items.title', 'Items')}\n columns={columns}\n data={rows}\n isLoading={loading}\n onRowClick={(row) => router.push(`/backend/billing/items/${row.id}`)}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.items.empty', 'No items for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Embedded usage table \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 AccountUsageSection({ accountId }: { accountId: string }) {\n const t = useT()\n const [rows, setRows] = React.useState<AccountUsageRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountUsageRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/usage?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=periodStart&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountUsageRow>[]>(\n () => [\n {\n accessorKey: 'uom_code',\n header: t('billing.usage.columns.uom', 'Unit'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">{row.original.uom_code}</span>\n ),\n },\n {\n accessorKey: 'quantity',\n header: t('billing.usage.columns.quantity', 'Quantity'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.quantity}</span>\n ),\n },\n {\n accessorKey: 'period_start',\n header: t('billing.usage.columns.period', 'Period'),\n cell: ({ row }) =>\n `${formatDate(row.original.period_start)} \u2013 ${formatDate(row.original.period_end)}`,\n },\n {\n accessorKey: 'line_description',\n header: t('billing.usage.columns.description', 'Description'),\n cell: ({ row }) => row.original.line_description || '\u2014',\n },\n {\n accessorKey: 'rated_in_bill_run_id',\n header: t('billing.usage.columns.rated', 'Rated'),\n cell: ({ row }) =>\n row.original.rated_in_bill_run_id ? (\n // Rated records carry the Bill Run that priced them \u2014 link to it.\n // Unrated records have no destination, so they stay non-interactive\n // (no misleading row-level click affordance).\n <Link\n href={`/backend/billing/runs/${row.original.rated_in_bill_run_id}`}\n className=\"rounded-sm hover:underline\"\n >\n <Tag variant=\"success\">{t('billing.usage.rated.yes', 'Rated')}</Tag>\n </Link>\n ) : (\n <Tag variant=\"default\">{t('billing.usage.rated.no', 'Unrated')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.usage.title', 'Usage')}\n columns={columns}\n data={rows}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.usage.empty', 'No usage records for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\nexport default function BillingAccountDetailPage(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 accountId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [row, setRow] = React.useState<AccountRow | 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 (!accountId) return\n setLoading(true)\n setError(null)\n try {\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/accounts?id=${accountId}&pageSize=1`,\n )\n const found = result.items?.[0]\n if (!found) {\n setError(t('billing.accounts.detail.error.not_found', 'Account not found'))\n return\n }\n setRow(found)\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setError(message || t('billing.accounts.detail.error.load', 'Failed to load account'))\n } finally {\n setLoading(false)\n }\n }, [accountId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const handleSubmit = React.useCallback(\n async (values: AccountFormValues) => {\n if (!row) return\n setSaving(true)\n try {\n const payload: Record<string, unknown> = {\n id: row.id,\n name: values.name.trim(),\n billCycle: values.billCycle,\n billCycleAnchor: values.billCycleAnchor.trim(),\n invoiceEmail: values.invoiceEmail.trim(),\n invoiceLanguage: values.invoiceLanguage.trim().toLowerCase(),\n invoiceAddress: {\n line1: values.invoiceAddress.line1.trim(),\n ...(values.invoiceAddress.line2.trim()\n ? { line2: values.invoiceAddress.line2.trim() }\n : {}),\n city: values.invoiceAddress.city.trim(),\n postal_code: values.invoiceAddress.postal_code.trim(),\n country: values.invoiceAddress.country.trim().toUpperCase(),\n },\n nextBillDate: values.nextBillDate,\n isActive: values.isActive,\n }\n payload.taxId = values.taxId.trim() === '' ? null : values.taxId.trim()\n // billingMode is immutable on update; creditLimit + threshold are editable.\n const creditLimit = Number.parseFloat(values.creditLimit)\n if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit\n if (row.billing_mode === 'prepaid' && values.lowBalanceThreshold.trim()) {\n const threshold = Number.parseFloat(values.lowBalanceThreshold)\n if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold\n }\n\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n flash(t('billing.accounts.detail.save.success', 'Account updated'), 'success')\n setEditing(false)\n void load()\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message\n ? `${message} \u2014 ${fieldDetail}`\n : fieldDetail || message\n flash(detailed || t('billing.accounts.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.accounts.detail.delete.confirm.title', 'Delete account?'),\n text: t(\n 'billing.accounts.detail.delete.confirm.text',\n \"Soft-deletes the account. Its items + usage records stay in the database for audit; the Bill Run engine will stop picking it up immediately.\",\n ),\n confirmText: t('billing.accounts.detail.delete.confirm.button', 'Soft delete'),\n variant: 'destructive',\n })\n if (!ok) return\n setDeleting(true)\n try {\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('billing.accounts.detail.delete.success', 'Account deleted'), 'success')\n router.push('/backend/billing/accounts')\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.accounts.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.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n\n if (loading || !row) {\n return (\n <Page>\n <PageHeader title={t('billing.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <LoadingMessage\n label={t('billing.accounts.detail.loading', 'Loading account\u2026')}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/accounts\"\n entityTypeLabel={t('billing.accounts.detail.title', 'Billing Account')}\n title={row.name}\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 <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/items/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_item', 'Add item')}\n </Link>\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/usage/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_usage', 'Add usage')}\n </Link>\n </Button>\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.accounts.detail.delete.in_progress', 'Deleting\u2026')\n : t('billing.accounts.detail.delete.action', 'Soft delete')}\n </Button>\n </>\n }\n />\n <PageBody>\n {editing ? (\n <AccountForm\n mode=\"edit\"\n initial={toFormValues(row)}\n submitting={saving}\n onSubmit={handleSubmit}\n onCancel={() => setEditing(false)}\n />\n ) : (\n <AccountView row={row} />\n )}\n {row.billing_mode === 'prepaid' ? (\n <PrepaidPanel accountId={row.id} currencyCode={row.currency_code} />\n ) : null}\n <AccountItemsSection accountId={row.id} />\n <AccountUsageSection accountId={row.id} />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -31,9 +31,16 @@ function CreateBillingAccountPage() {
|
|
|
31
31
|
postal_code: values.invoiceAddress.postal_code.trim(),
|
|
32
32
|
country: values.invoiceAddress.country.trim().toUpperCase()
|
|
33
33
|
},
|
|
34
|
-
nextBillDate: values.nextBillDate
|
|
34
|
+
nextBillDate: values.nextBillDate,
|
|
35
|
+
billingMode: values.billingMode
|
|
35
36
|
};
|
|
36
37
|
if (values.taxId.trim()) payload.taxId = values.taxId.trim();
|
|
38
|
+
const creditLimit = Number.parseFloat(values.creditLimit);
|
|
39
|
+
if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit;
|
|
40
|
+
if (values.billingMode === "prepaid" && values.lowBalanceThreshold.trim()) {
|
|
41
|
+
const threshold = Number.parseFloat(values.lowBalanceThreshold);
|
|
42
|
+
if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold;
|
|
43
|
+
}
|
|
37
44
|
const call = await apiCallOrThrow("/api/billing/accounts", {
|
|
38
45
|
method: "POST",
|
|
39
46
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/billing/backend/billing/accounts/create/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { AccountForm, type AccountFormValues } from '../../../../components/AccountForm'\n\n/**\n * `/backend/billing/accounts/create` \u2014 create a Billing Account.\n *\n * POSTs `/api/billing/accounts`. On success, flash + navigate to the\n * new account's detail page (`/backend/billing/accounts/<id>`) so the\n * operator can continue editing if they need to tweak after-creation\n * details.\n */\nexport default function CreateBillingAccountPage() {\n const t = useT()\n const router = useRouter()\n const [submitting, setSubmitting] = React.useState(false)\n\n const handleSubmit = React.useCallback(\n async (values: AccountFormValues) => {\n setSubmitting(true)\n try {\n const payload: Record<string, unknown> = {\n customerId: values.customerId.trim(),\n name: values.name.trim(),\n currencyCode: values.currencyCode.trim().toUpperCase(),\n billCycle: values.billCycle,\n billCycleAnchor: values.billCycleAnchor.trim(),\n invoiceEmail: values.invoiceEmail.trim(),\n invoiceLanguage: values.invoiceLanguage.trim().toLowerCase(),\n invoiceAddress: {\n line1: values.invoiceAddress.line1.trim(),\n ...(values.invoiceAddress.line2.trim()\n ? { line2: values.invoiceAddress.line2.trim() }\n : {}),\n city: values.invoiceAddress.city.trim(),\n postal_code: values.invoiceAddress.postal_code.trim(),\n country: values.invoiceAddress.country.trim().toUpperCase(),\n },\n nextBillDate: values.nextBillDate,\n }\n if (values.taxId.trim()) payload.taxId = values.taxId.trim()\n\n const call = await apiCallOrThrow<{ id: string }>('/api/billing/accounts', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const id = call.result?.id\n flash(t('billing.accounts.create.success', 'Billing account created'), 'success')\n if (id) {\n router.push(`/backend/billing/accounts/${id}`)\n } else {\n router.push('/backend/billing/accounts')\n }\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message\n ? `${message} \u2014 ${fieldDetail}`\n : fieldDetail || message\n flash(\n detailed || t('billing.accounts.create.error', 'Failed to create account'),\n 'error',\n )\n } finally {\n setSubmitting(false)\n }\n },\n [router, t],\n )\n\n const handleCancel = React.useCallback(() => {\n router.push('/backend/billing/accounts')\n }, [router])\n\n return (\n <Page>\n <PageHeader title={t('billing.accounts.create.title', 'Create Billing Account')} />\n <PageBody>\n <AccountForm\n mode=\"create\"\n submitting={submitting}\n onSubmit={handleSubmit}\n onCancel={handleCancel}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { AccountForm, type AccountFormValues } from '../../../../components/AccountForm'\n\n/**\n * `/backend/billing/accounts/create` \u2014 create a Billing Account.\n *\n * POSTs `/api/billing/accounts`. On success, flash + navigate to the\n * new account's detail page (`/backend/billing/accounts/<id>`) so the\n * operator can continue editing if they need to tweak after-creation\n * details.\n */\nexport default function CreateBillingAccountPage() {\n const t = useT()\n const router = useRouter()\n const [submitting, setSubmitting] = React.useState(false)\n\n const handleSubmit = React.useCallback(\n async (values: AccountFormValues) => {\n setSubmitting(true)\n try {\n const payload: Record<string, unknown> = {\n customerId: values.customerId.trim(),\n name: values.name.trim(),\n currencyCode: values.currencyCode.trim().toUpperCase(),\n billCycle: values.billCycle,\n billCycleAnchor: values.billCycleAnchor.trim(),\n invoiceEmail: values.invoiceEmail.trim(),\n invoiceLanguage: values.invoiceLanguage.trim().toLowerCase(),\n invoiceAddress: {\n line1: values.invoiceAddress.line1.trim(),\n ...(values.invoiceAddress.line2.trim()\n ? { line2: values.invoiceAddress.line2.trim() }\n : {}),\n city: values.invoiceAddress.city.trim(),\n postal_code: values.invoiceAddress.postal_code.trim(),\n country: values.invoiceAddress.country.trim().toUpperCase(),\n },\n nextBillDate: values.nextBillDate,\n billingMode: values.billingMode,\n }\n if (values.taxId.trim()) payload.taxId = values.taxId.trim()\n const creditLimit = Number.parseFloat(values.creditLimit)\n if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit\n if (values.billingMode === 'prepaid' && values.lowBalanceThreshold.trim()) {\n const threshold = Number.parseFloat(values.lowBalanceThreshold)\n if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold\n }\n\n const call = await apiCallOrThrow<{ id: string }>('/api/billing/accounts', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const id = call.result?.id\n flash(t('billing.accounts.create.success', 'Billing account created'), 'success')\n if (id) {\n router.push(`/backend/billing/accounts/${id}`)\n } else {\n router.push('/backend/billing/accounts')\n }\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message\n ? `${message} \u2014 ${fieldDetail}`\n : fieldDetail || message\n flash(\n detailed || t('billing.accounts.create.error', 'Failed to create account'),\n 'error',\n )\n } finally {\n setSubmitting(false)\n }\n },\n [router, t],\n )\n\n const handleCancel = React.useCallback(() => {\n router.push('/backend/billing/accounts')\n }, [router])\n\n return (\n <Page>\n <PageHeader title={t('billing.accounts.create.title', 'Create Billing Account')} />\n <PageBody>\n <AccountForm\n mode=\"create\"\n submitting={submitting}\n onSubmit={handleSubmit}\n onCancel={handleCancel}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -44,6 +44,15 @@ function BillingAccountsListPage() {
|
|
|
44
44
|
label: t("billing.accounts.filters.currency", "Currency"),
|
|
45
45
|
type: "text"
|
|
46
46
|
},
|
|
47
|
+
{
|
|
48
|
+
id: "billingMode",
|
|
49
|
+
label: t("billing.accounts.columns.mode", "Mode"),
|
|
50
|
+
type: "select",
|
|
51
|
+
options: [
|
|
52
|
+
{ value: "postpaid", label: t("billing.mode.postpaid", "Postpaid") },
|
|
53
|
+
{ value: "prepaid", label: t("billing.mode.prepaid", "Prepaid") }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
47
56
|
{
|
|
48
57
|
id: "isActive",
|
|
49
58
|
label: t("billing.accounts.filters.active", "Active"),
|
|
@@ -72,6 +81,9 @@ function BillingAccountsListPage() {
|
|
|
72
81
|
if (typeof filters.currencyCode === "string" && filters.currencyCode.trim()) {
|
|
73
82
|
params.set("currencyCode", filters.currencyCode.trim().toUpperCase());
|
|
74
83
|
}
|
|
84
|
+
if (typeof filters.billingMode === "string" && filters.billingMode) {
|
|
85
|
+
params.set("billingMode", filters.billingMode);
|
|
86
|
+
}
|
|
75
87
|
if (filters.isActive === "true") params.set("isActive", "true");
|
|
76
88
|
if (filters.isActive === "false") params.set("isActive", "false");
|
|
77
89
|
const result = await readApiResultOrThrow(
|
|
@@ -83,7 +95,15 @@ function BillingAccountsListPage() {
|
|
|
83
95
|
} finally {
|
|
84
96
|
setLoading(false);
|
|
85
97
|
}
|
|
86
|
-
}, [
|
|
98
|
+
}, [
|
|
99
|
+
filters.billCycle,
|
|
100
|
+
filters.currencyCode,
|
|
101
|
+
filters.billingMode,
|
|
102
|
+
filters.isActive,
|
|
103
|
+
page,
|
|
104
|
+
pageSize,
|
|
105
|
+
search
|
|
106
|
+
]);
|
|
87
107
|
React.useEffect(() => {
|
|
88
108
|
void loadRows();
|
|
89
109
|
}, [loadRows]);
|
|
@@ -114,6 +134,11 @@ function BillingAccountsListPage() {
|
|
|
114
134
|
] })
|
|
115
135
|
] })
|
|
116
136
|
},
|
|
137
|
+
{
|
|
138
|
+
accessorKey: "billing_mode",
|
|
139
|
+
header: t("billing.accounts.columns.mode", "Mode"),
|
|
140
|
+
cell: ({ row }) => row.original.billing_mode === "prepaid" ? /* @__PURE__ */ jsx(Tag, { variant: "brand", children: t("billing.mode.prepaid", "Prepaid") }) : /* @__PURE__ */ jsx(Tag, { variant: "neutral", children: t("billing.mode.postpaid", "Postpaid") })
|
|
141
|
+
},
|
|
117
142
|
{
|
|
118
143
|
accessorKey: "next_bill_date",
|
|
119
144
|
header: t("billing.accounts.columns.next_bill", "Next bill"),
|