@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,117 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import { BillingStatement } from '../../data/entities'
|
|
4
|
+
import { billingEntityIds } from '../../data/entityIds'
|
|
5
|
+
import { billingStatementListQuerySchema } from '../../data/validators'
|
|
6
|
+
import {
|
|
7
|
+
createBillingCrudOpenApi,
|
|
8
|
+
createPagedListResponseSchema,
|
|
9
|
+
} from '../openapi'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `GET /api/billing/statements` — paged consumption statements (SPEC-002 P3).
|
|
13
|
+
* Filter by account, status, and a `period_end` window. `?id=` narrows to one
|
|
14
|
+
* row for the detail page. Generation goes through
|
|
15
|
+
* POST /api/billing/statements/generate.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const routeMetadata = {
|
|
19
|
+
GET: { requireAuth: true, requireFeatures: ['billing.statement.view'] },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const metadata = routeMetadata
|
|
23
|
+
|
|
24
|
+
const billingStatementListItemSchema = z.object({
|
|
25
|
+
id: z.string().uuid(),
|
|
26
|
+
bill_account_id: z.string().uuid(),
|
|
27
|
+
bill_run_id: z.string().uuid().nullable(),
|
|
28
|
+
period_start: z.string(),
|
|
29
|
+
period_end: z.string(),
|
|
30
|
+
currency_code: z.string(),
|
|
31
|
+
opening_balance: z.string(),
|
|
32
|
+
total_topups: z.string(),
|
|
33
|
+
total_usage: z.string(),
|
|
34
|
+
total_recurring: z.string(),
|
|
35
|
+
total_adjustments: z.string(),
|
|
36
|
+
closing_balance: z.string(),
|
|
37
|
+
debit_breakdown: z.record(z.string(), z.unknown()).nullable(),
|
|
38
|
+
status: z.string(),
|
|
39
|
+
organization_id: z.string().uuid(),
|
|
40
|
+
tenant_id: z.string().uuid(),
|
|
41
|
+
created_at: z.string(),
|
|
42
|
+
updated_at: z.string(),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const crud = makeCrudRoute({
|
|
46
|
+
metadata: routeMetadata,
|
|
47
|
+
orm: {
|
|
48
|
+
entity: BillingStatement,
|
|
49
|
+
idField: 'id',
|
|
50
|
+
orgField: 'organizationId',
|
|
51
|
+
tenantField: 'tenantId',
|
|
52
|
+
},
|
|
53
|
+
list: {
|
|
54
|
+
schema: billingStatementListQuerySchema,
|
|
55
|
+
entityId: billingEntityIds.statement,
|
|
56
|
+
fields: [
|
|
57
|
+
'id',
|
|
58
|
+
'bill_account_id',
|
|
59
|
+
'bill_run_id',
|
|
60
|
+
'period_start',
|
|
61
|
+
'period_end',
|
|
62
|
+
'currency_code',
|
|
63
|
+
'opening_balance',
|
|
64
|
+
'total_topups',
|
|
65
|
+
'total_usage',
|
|
66
|
+
'total_recurring',
|
|
67
|
+
'total_adjustments',
|
|
68
|
+
'closing_balance',
|
|
69
|
+
'debit_breakdown',
|
|
70
|
+
'status',
|
|
71
|
+
'organization_id',
|
|
72
|
+
'tenant_id',
|
|
73
|
+
'created_at',
|
|
74
|
+
'updated_at',
|
|
75
|
+
],
|
|
76
|
+
sortFieldMap: {
|
|
77
|
+
periodEnd: 'period_end',
|
|
78
|
+
periodStart: 'period_start',
|
|
79
|
+
createdAt: 'created_at',
|
|
80
|
+
},
|
|
81
|
+
buildFilters: async (query) => {
|
|
82
|
+
const filters: Record<string, unknown> = {}
|
|
83
|
+
if (typeof query.id === 'string' && query.id) {
|
|
84
|
+
filters.id = { $eq: query.id }
|
|
85
|
+
}
|
|
86
|
+
if (typeof query.billAccountId === 'string' && query.billAccountId) {
|
|
87
|
+
filters.bill_account_id = { $eq: query.billAccountId }
|
|
88
|
+
}
|
|
89
|
+
if (typeof query.status === 'string' && query.status) {
|
|
90
|
+
filters.status = { $eq: query.status }
|
|
91
|
+
}
|
|
92
|
+
if (query.periodEndFrom instanceof Date) {
|
|
93
|
+
filters.period_end = Object.assign(
|
|
94
|
+
(filters.period_end as Record<string, unknown>) ?? {},
|
|
95
|
+
{ $gte: query.periodEndFrom },
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
if (query.periodEndTo instanceof Date) {
|
|
99
|
+
filters.period_end = Object.assign(
|
|
100
|
+
(filters.period_end as Record<string, unknown>) ?? {},
|
|
101
|
+
{ $lte: query.periodEndTo },
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
return filters
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
actions: {},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export const GET = crud.GET
|
|
111
|
+
|
|
112
|
+
export const openApi = createBillingCrudOpenApi({
|
|
113
|
+
resourceName: 'BillingStatement',
|
|
114
|
+
pluralName: 'BillingStatements',
|
|
115
|
+
querySchema: billingStatementListQuerySchema,
|
|
116
|
+
listResponseSchema: createPagedListResponseSchema(billingStatementListItemSchema),
|
|
117
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { BillingTopup } from '../../data/entities'
|
|
6
|
+
import { billingEntityIds } from '../../data/entityIds'
|
|
7
|
+
import {
|
|
8
|
+
billingTopupCreateSchema,
|
|
9
|
+
billingTopupListQuerySchema,
|
|
10
|
+
} from '../../data/validators'
|
|
11
|
+
import {
|
|
12
|
+
createBillingCrudOpenApi,
|
|
13
|
+
createPagedListResponseSchema,
|
|
14
|
+
} from '../openapi'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `/api/billing/topups` — GET (list registered top-ups) + POST (initiate).
|
|
18
|
+
*
|
|
19
|
+
* The nested, spec-faithful `POST /api/billing/accounts/{id}/topups` is a thin
|
|
20
|
+
* wrapper that injects the path id as `billAccountId` and delegates here, so
|
|
21
|
+
* all the RBAC / command-bus / side-effect plumbing lives in one place.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const routeMetadata = {
|
|
25
|
+
GET: { requireAuth: true, requireFeatures: ['billing.topup.view'] },
|
|
26
|
+
POST: { requireAuth: true, requireFeatures: ['billing.topup.create'] },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const metadata = routeMetadata
|
|
30
|
+
|
|
31
|
+
const billingTopupListItemSchema = z.object({
|
|
32
|
+
id: z.string().uuid(),
|
|
33
|
+
bill_account_id: z.string().uuid(),
|
|
34
|
+
status: z.string(),
|
|
35
|
+
amount: z.string(),
|
|
36
|
+
currency_code: z.string(),
|
|
37
|
+
provider_key: z.string(),
|
|
38
|
+
payment_id: z.string().uuid(),
|
|
39
|
+
gateway_transaction_id: z.string().uuid().nullable(),
|
|
40
|
+
transaction_id: z.string().uuid().nullable(),
|
|
41
|
+
receipt_invoice_id: z.string().uuid().nullable(),
|
|
42
|
+
source_ref: z.string().nullable(),
|
|
43
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
44
|
+
organization_id: z.string().uuid(),
|
|
45
|
+
tenant_id: z.string().uuid(),
|
|
46
|
+
created_at: z.string(),
|
|
47
|
+
updated_at: z.string(),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const topupCreateResponseSchema = z.object({
|
|
51
|
+
topupId: z.string().uuid(),
|
|
52
|
+
paymentId: z.string().uuid(),
|
|
53
|
+
status: z.string(),
|
|
54
|
+
transactionId: z.string().uuid().nullable(),
|
|
55
|
+
redirectUrl: z.string().nullable(),
|
|
56
|
+
clientSecret: z.string().nullable(),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const rawBodySchema = z.object({}).passthrough()
|
|
60
|
+
|
|
61
|
+
const crud = makeCrudRoute({
|
|
62
|
+
metadata: routeMetadata,
|
|
63
|
+
orm: {
|
|
64
|
+
entity: BillingTopup,
|
|
65
|
+
idField: 'id',
|
|
66
|
+
orgField: 'organizationId',
|
|
67
|
+
tenantField: 'tenantId',
|
|
68
|
+
softDeleteField: 'deletedAt',
|
|
69
|
+
},
|
|
70
|
+
list: {
|
|
71
|
+
schema: billingTopupListQuerySchema,
|
|
72
|
+
entityId: billingEntityIds.topup,
|
|
73
|
+
fields: [
|
|
74
|
+
'id',
|
|
75
|
+
'bill_account_id',
|
|
76
|
+
'status',
|
|
77
|
+
'amount',
|
|
78
|
+
'currency_code',
|
|
79
|
+
'provider_key',
|
|
80
|
+
'payment_id',
|
|
81
|
+
'gateway_transaction_id',
|
|
82
|
+
'transaction_id',
|
|
83
|
+
'receipt_invoice_id',
|
|
84
|
+
'source_ref',
|
|
85
|
+
'metadata',
|
|
86
|
+
'organization_id',
|
|
87
|
+
'tenant_id',
|
|
88
|
+
'created_at',
|
|
89
|
+
'updated_at',
|
|
90
|
+
],
|
|
91
|
+
sortFieldMap: {
|
|
92
|
+
createdAt: 'created_at',
|
|
93
|
+
updatedAt: 'updated_at',
|
|
94
|
+
amount: 'amount',
|
|
95
|
+
},
|
|
96
|
+
buildFilters: async (query) => {
|
|
97
|
+
const filters: Record<string, unknown> = {}
|
|
98
|
+
if (typeof query.id === 'string' && query.id) {
|
|
99
|
+
filters.id = { $eq: query.id }
|
|
100
|
+
}
|
|
101
|
+
if (typeof query.billAccountId === 'string' && query.billAccountId) {
|
|
102
|
+
filters.bill_account_id = { $eq: query.billAccountId }
|
|
103
|
+
}
|
|
104
|
+
if (typeof query.status === 'string' && query.status) {
|
|
105
|
+
filters.status = { $eq: query.status }
|
|
106
|
+
}
|
|
107
|
+
if (typeof query.search === 'string' && /^[0-9a-fA-F-]{36}$/.test(query.search.trim())) {
|
|
108
|
+
filters.id = { $eq: query.search.trim() }
|
|
109
|
+
}
|
|
110
|
+
return filters
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
actions: {
|
|
114
|
+
create: {
|
|
115
|
+
commandId: 'billing.topups.create',
|
|
116
|
+
schema: rawBodySchema,
|
|
117
|
+
mapInput: async ({ raw, ctx }) => {
|
|
118
|
+
const { translate } = await resolveTranslations()
|
|
119
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
120
|
+
return billingTopupCreateSchema.parse(scoped)
|
|
121
|
+
},
|
|
122
|
+
response: ({ result }) => ({
|
|
123
|
+
topupId: result?.topupId ?? null,
|
|
124
|
+
paymentId: result?.paymentId ?? null,
|
|
125
|
+
status: result?.status ?? null,
|
|
126
|
+
transactionId: result?.transactionId ?? null,
|
|
127
|
+
redirectUrl: result?.redirectUrl ?? null,
|
|
128
|
+
clientSecret: result?.clientSecret ?? null,
|
|
129
|
+
}),
|
|
130
|
+
status: 201,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const { POST } = crud
|
|
136
|
+
export { POST }
|
|
137
|
+
export const GET = crud.GET
|
|
138
|
+
|
|
139
|
+
export const openApi = createBillingCrudOpenApi({
|
|
140
|
+
resourceName: 'BillingTopup',
|
|
141
|
+
pluralName: 'BillingTopups',
|
|
142
|
+
querySchema: billingTopupListQuerySchema,
|
|
143
|
+
listResponseSchema: createPagedListResponseSchema(billingTopupListItemSchema),
|
|
144
|
+
create: {
|
|
145
|
+
schema: billingTopupCreateSchema,
|
|
146
|
+
responseSchema: topupCreateResponseSchema,
|
|
147
|
+
description:
|
|
148
|
+
'Registers a prepaid top-up (status=pending) and opens a payment ' +
|
|
149
|
+
'session via core/payment_gateways. The balance is credited only on ' +
|
|
150
|
+
'`payment_gateways.payment.captured` (the capture subscriber issues the ' +
|
|
151
|
+
'VAT receipt). Returns the topup id, the payment session redirect URL / ' +
|
|
152
|
+
'client secret, and the gateway transaction id.',
|
|
153
|
+
},
|
|
154
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
5
|
+
import { BillingAccountTransaction } from '../../../data/entities'
|
|
6
|
+
import { billingEntityIds } from '../../../data/entityIds'
|
|
7
|
+
import { billingTransactionAdjustSchema } from '../../../data/validators'
|
|
8
|
+
import { createBillingCrudOpenApi } from '../../openapi'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `POST /api/billing/transactions/adjust` (SPEC-002 P5) — manual balance
|
|
12
|
+
* credit/debit (`type='adjustment'`), audited with the operator's `user_id`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const routeMetadata = {
|
|
16
|
+
POST: { requireAuth: true, requireFeatures: ['billing.balance.adjust'] },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const metadata = routeMetadata
|
|
20
|
+
|
|
21
|
+
const adjustResponseSchema = z.object({
|
|
22
|
+
transactionId: z.string().uuid(),
|
|
23
|
+
balance: z.string(),
|
|
24
|
+
balanceStatus: z.enum(['ok', 'low', 'exhausted']),
|
|
25
|
+
deduplicated: z.boolean(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const rawBodySchema = z.object({}).passthrough()
|
|
29
|
+
|
|
30
|
+
const crud = makeCrudRoute({
|
|
31
|
+
metadata: routeMetadata,
|
|
32
|
+
orm: {
|
|
33
|
+
entity: BillingAccountTransaction,
|
|
34
|
+
idField: 'id',
|
|
35
|
+
orgField: 'organizationId',
|
|
36
|
+
tenantField: 'tenantId',
|
|
37
|
+
softDeleteField: 'deletedAt',
|
|
38
|
+
},
|
|
39
|
+
list: {
|
|
40
|
+
schema: z.object({}).passthrough(),
|
|
41
|
+
entityId: billingEntityIds.transaction,
|
|
42
|
+
fields: ['id'],
|
|
43
|
+
buildFilters: async () => ({}),
|
|
44
|
+
},
|
|
45
|
+
actions: {
|
|
46
|
+
create: {
|
|
47
|
+
commandId: 'billing.transactions.adjust',
|
|
48
|
+
schema: rawBodySchema,
|
|
49
|
+
mapInput: async ({ raw, ctx }) => {
|
|
50
|
+
const { translate } = await resolveTranslations()
|
|
51
|
+
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
52
|
+
return billingTransactionAdjustSchema.parse(scoped)
|
|
53
|
+
},
|
|
54
|
+
response: ({ result }) => ({
|
|
55
|
+
transactionId: result?.transactionId ?? null,
|
|
56
|
+
balance: result?.balance ?? null,
|
|
57
|
+
balanceStatus: result?.balanceStatus ?? null,
|
|
58
|
+
deduplicated: Boolean(result?.deduplicated),
|
|
59
|
+
}),
|
|
60
|
+
status: 201,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export const POST = crud.POST
|
|
66
|
+
|
|
67
|
+
export const openApi = createBillingCrudOpenApi({
|
|
68
|
+
resourceName: 'BillingBalanceAdjustment',
|
|
69
|
+
pluralName: 'BillingBalanceAdjustments',
|
|
70
|
+
querySchema: z.object({}).passthrough(),
|
|
71
|
+
listResponseSchema: z.object({ items: z.array(z.unknown()) }),
|
|
72
|
+
create: {
|
|
73
|
+
schema: billingTransactionAdjustSchema,
|
|
74
|
+
responseSchema: adjustResponseSchema,
|
|
75
|
+
description:
|
|
76
|
+
'Manually credits (amount > 0) or debits (amount < 0) a prepaid ' +
|
|
77
|
+
"account's balance. Audited with the operator's user id. Non-fiscal — " +
|
|
78
|
+
'no receipt is issued. Returns the new balance + status.',
|
|
79
|
+
},
|
|
80
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
|
+
import { BillingAccountTransaction } from '../../data/entities'
|
|
4
|
+
import { billingEntityIds } from '../../data/entityIds'
|
|
5
|
+
import { billingTransactionListQuerySchema } from '../../data/validators'
|
|
6
|
+
import {
|
|
7
|
+
createBillingCrudOpenApi,
|
|
8
|
+
createPagedListResponseSchema,
|
|
9
|
+
} from '../openapi'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `GET /api/billing/transactions` — paged, append-only balance ledger
|
|
13
|
+
* (SPEC-002). Filter by account, type, and a `created_at` window. The
|
|
14
|
+
* write path is the command bus (real-time consume, top-up capture, period
|
|
15
|
+
* close, manual adjustment) — there is no create/update/delete here.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const routeMetadata = {
|
|
19
|
+
GET: { requireAuth: true, requireFeatures: ['billing.balance.view'] },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const metadata = routeMetadata
|
|
23
|
+
|
|
24
|
+
const billingTransactionListItemSchema = z.object({
|
|
25
|
+
id: z.string().uuid(),
|
|
26
|
+
bill_account_id: z.string().uuid(),
|
|
27
|
+
type: z.string(),
|
|
28
|
+
amount: z.string(),
|
|
29
|
+
currency_code: z.string(),
|
|
30
|
+
balance_after: z.string(),
|
|
31
|
+
usage_id: z.string().uuid().nullable(),
|
|
32
|
+
billing_item_id: z.string().uuid().nullable(),
|
|
33
|
+
topup_id: z.string().uuid().nullable(),
|
|
34
|
+
statement_id: z.string().uuid().nullable(),
|
|
35
|
+
description: z.string(),
|
|
36
|
+
source_ref: z.string().nullable(),
|
|
37
|
+
metadata: z.record(z.string(), z.unknown()).nullable(),
|
|
38
|
+
user_id: z.string().uuid().nullable(),
|
|
39
|
+
organization_id: z.string().uuid(),
|
|
40
|
+
tenant_id: z.string().uuid(),
|
|
41
|
+
created_at: z.string(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const crud = makeCrudRoute({
|
|
45
|
+
metadata: routeMetadata,
|
|
46
|
+
orm: {
|
|
47
|
+
entity: BillingAccountTransaction,
|
|
48
|
+
idField: 'id',
|
|
49
|
+
orgField: 'organizationId',
|
|
50
|
+
tenantField: 'tenantId',
|
|
51
|
+
softDeleteField: 'deletedAt',
|
|
52
|
+
},
|
|
53
|
+
list: {
|
|
54
|
+
schema: billingTransactionListQuerySchema,
|
|
55
|
+
entityId: billingEntityIds.transaction,
|
|
56
|
+
fields: [
|
|
57
|
+
'id',
|
|
58
|
+
'bill_account_id',
|
|
59
|
+
'type',
|
|
60
|
+
'amount',
|
|
61
|
+
'currency_code',
|
|
62
|
+
'balance_after',
|
|
63
|
+
'usage_id',
|
|
64
|
+
'billing_item_id',
|
|
65
|
+
'topup_id',
|
|
66
|
+
'statement_id',
|
|
67
|
+
'description',
|
|
68
|
+
'source_ref',
|
|
69
|
+
'metadata',
|
|
70
|
+
'user_id',
|
|
71
|
+
'organization_id',
|
|
72
|
+
'tenant_id',
|
|
73
|
+
'created_at',
|
|
74
|
+
],
|
|
75
|
+
sortFieldMap: {
|
|
76
|
+
createdAt: 'created_at',
|
|
77
|
+
},
|
|
78
|
+
buildFilters: async (query) => {
|
|
79
|
+
const filters: Record<string, unknown> = {}
|
|
80
|
+
if (typeof query.id === 'string' && query.id) {
|
|
81
|
+
filters.id = { $eq: query.id }
|
|
82
|
+
}
|
|
83
|
+
if (typeof query.billAccountId === 'string' && query.billAccountId) {
|
|
84
|
+
filters.bill_account_id = { $eq: query.billAccountId }
|
|
85
|
+
}
|
|
86
|
+
if (typeof query.type === 'string' && query.type) {
|
|
87
|
+
filters.type = { $eq: query.type }
|
|
88
|
+
}
|
|
89
|
+
if (query.createdFrom instanceof Date) {
|
|
90
|
+
filters.created_at = Object.assign(
|
|
91
|
+
(filters.created_at as Record<string, unknown>) ?? {},
|
|
92
|
+
{ $gte: query.createdFrom },
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
if (query.createdTo instanceof Date) {
|
|
96
|
+
filters.created_at = Object.assign(
|
|
97
|
+
(filters.created_at as Record<string, unknown>) ?? {},
|
|
98
|
+
{ $lte: query.createdTo },
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return filters
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
// Append-only — no create/update/delete on this surface. Manual
|
|
105
|
+
// adjustments go through POST /api/billing/transactions/adjust.
|
|
106
|
+
actions: {},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
export const GET = crud.GET
|
|
110
|
+
|
|
111
|
+
export const openApi = createBillingCrudOpenApi({
|
|
112
|
+
resourceName: 'BillingAccountTransaction',
|
|
113
|
+
pluralName: 'BillingAccountTransactions',
|
|
114
|
+
querySchema: billingTransactionListQuerySchema,
|
|
115
|
+
listResponseSchema: createPagedListResponseSchema(billingTransactionListItemSchema),
|
|
116
|
+
})
|
|
@@ -46,6 +46,15 @@ const billingUsageListItemSchema = z.object({
|
|
|
46
46
|
const usageCreateResponseSchema = z.object({
|
|
47
47
|
id: z.string().uuid().nullable(),
|
|
48
48
|
deduplicated: z.boolean(),
|
|
49
|
+
// Prepaid accounts (SPEC-002) get the real-time balance body. Absent for
|
|
50
|
+
// postpaid accounts (the legacy append-only response is unchanged).
|
|
51
|
+
ratedAmount: z.string().optional(),
|
|
52
|
+
currencyCode: z.string().optional(),
|
|
53
|
+
balance: z.string().optional(),
|
|
54
|
+
balanceStatus: z.enum(['ok', 'low', 'exhausted']).optional(),
|
|
55
|
+
creditStatus: z.enum(['within_limit', 'near_limit', 'over_limit']).optional(),
|
|
56
|
+
tierBreakdown: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
57
|
+
warning: z.string().nullable().optional(),
|
|
49
58
|
})
|
|
50
59
|
|
|
51
60
|
const rawBodySchema = z.object({}).passthrough()
|
|
@@ -118,10 +127,27 @@ const crud = makeCrudRoute({
|
|
|
118
127
|
const scoped = withScopedPayload(raw ?? {}, ctx, translate)
|
|
119
128
|
return billingUsageCreateSchema.parse(scoped)
|
|
120
129
|
},
|
|
121
|
-
response: ({ result }) =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
response: ({ result }) => {
|
|
131
|
+
const base = {
|
|
132
|
+
id: result?.id ?? null,
|
|
133
|
+
deduplicated: Boolean(result?.deduplicated),
|
|
134
|
+
}
|
|
135
|
+
// Pass through the prepaid balance body when present (prepaid
|
|
136
|
+
// accounts only); postpaid responses keep the legacy 2-field shape.
|
|
137
|
+
if (result && 'balance' in result && result.balance !== undefined) {
|
|
138
|
+
return {
|
|
139
|
+
...base,
|
|
140
|
+
ratedAmount: result.ratedAmount,
|
|
141
|
+
currencyCode: result.currencyCode,
|
|
142
|
+
balance: result.balance,
|
|
143
|
+
balanceStatus: result.balanceStatus,
|
|
144
|
+
creditStatus: result.creditStatus,
|
|
145
|
+
tierBreakdown: result.tierBreakdown ?? [],
|
|
146
|
+
warning: result.warning ?? null,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return base
|
|
150
|
+
},
|
|
125
151
|
status: 200,
|
|
126
152
|
},
|
|
127
153
|
// Usage has no update or delete by design — see file header.
|
|
@@ -143,6 +169,11 @@ export const openApi = createBillingCrudOpenApi({
|
|
|
143
169
|
description:
|
|
144
170
|
'Ingests a pre-aggregated usage record. Idempotent via `sourceRef`. ' +
|
|
145
171
|
'Usage records are immutable once persisted — to correct a mis-aggregation, ' +
|
|
146
|
-
"post a `one_time` Billing Item credit (the spec's refund model)."
|
|
172
|
+
"post a `one_time` Billing Item credit (the spec's refund model). " +
|
|
173
|
+
'For a PREPAID account (SPEC-002) the upload is rated synchronously, ' +
|
|
174
|
+
'debited from the balance, and the response carries `ratedAmount`, the ' +
|
|
175
|
+
'remaining `balance`, `balanceStatus` (ok/low/exhausted), `creditStatus`, ' +
|
|
176
|
+
'and the `tierBreakdown`. Usage is never rejected — the balance may go ' +
|
|
177
|
+
'negative and the client is expected to throttle itself.',
|
|
147
178
|
},
|
|
148
179
|
})
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
21
21
|
import { AccountForm, type AccountFormValues } from '../../../../components/AccountForm'
|
|
22
22
|
import { DetailCard, DetailField } from '../../../../components/DetailFields'
|
|
23
|
+
import { PrepaidPanel } from '../../../../components/PrepaidPanel'
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* `/backend/billing/accounts/[id]` — Billing Account detail.
|
|
@@ -54,6 +55,8 @@ type AccountRow = {
|
|
|
54
55
|
} | null
|
|
55
56
|
next_bill_date: string
|
|
56
57
|
last_bill_date: string | null
|
|
58
|
+
billing_mode: 'postpaid' | 'prepaid'
|
|
59
|
+
credit_limit: string
|
|
57
60
|
is_active: boolean
|
|
58
61
|
created_at: string
|
|
59
62
|
updated_at: string
|
|
@@ -108,6 +111,11 @@ function toFormValues(row: AccountRow): AccountFormValues {
|
|
|
108
111
|
},
|
|
109
112
|
nextBillDate: row.next_bill_date?.slice(0, 10) ?? '',
|
|
110
113
|
isActive: row.is_active,
|
|
114
|
+
billingMode: row.billing_mode,
|
|
115
|
+
creditLimit: row.credit_limit ?? '0',
|
|
116
|
+
// The threshold lives on the balance row, not the account row — blank
|
|
117
|
+
// means "leave unchanged" on save.
|
|
118
|
+
lowBalanceThreshold: '',
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
121
|
|
|
@@ -135,6 +143,16 @@ function AccountView({ row }: { row: AccountRow }) {
|
|
|
135
143
|
<DetailField label={t('billing.accounts.columns.currency', 'Currency')}>
|
|
136
144
|
{row.currency_code}
|
|
137
145
|
</DetailField>
|
|
146
|
+
<DetailField label={t('billing.accounts.columns.mode', 'Mode')}>
|
|
147
|
+
{row.billing_mode === 'prepaid' ? (
|
|
148
|
+
<Tag variant="brand">{t('billing.mode.prepaid', 'Prepaid')}</Tag>
|
|
149
|
+
) : (
|
|
150
|
+
<Tag variant="neutral">{t('billing.mode.postpaid', 'Postpaid')}</Tag>
|
|
151
|
+
)}
|
|
152
|
+
</DetailField>
|
|
153
|
+
<DetailField label={t('billing.accounts.columns.credit_limit', 'Credit limit')}>
|
|
154
|
+
<span className="font-mono tabular-nums">{row.credit_limit}</span>
|
|
155
|
+
</DetailField>
|
|
138
156
|
<DetailField label={t('billing.common.id', 'ID')}>
|
|
139
157
|
<span className="font-mono text-xs">{row.id}</span>
|
|
140
158
|
</DetailField>
|
|
@@ -425,6 +443,13 @@ export default function BillingAccountDetailPage(props: { params?: { id?: string
|
|
|
425
443
|
isActive: values.isActive,
|
|
426
444
|
}
|
|
427
445
|
payload.taxId = values.taxId.trim() === '' ? null : values.taxId.trim()
|
|
446
|
+
// billingMode is immutable on update; creditLimit + threshold are editable.
|
|
447
|
+
const creditLimit = Number.parseFloat(values.creditLimit)
|
|
448
|
+
if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit
|
|
449
|
+
if (row.billing_mode === 'prepaid' && values.lowBalanceThreshold.trim()) {
|
|
450
|
+
const threshold = Number.parseFloat(values.lowBalanceThreshold)
|
|
451
|
+
if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold
|
|
452
|
+
}
|
|
428
453
|
|
|
429
454
|
await apiCallOrThrow('/api/billing/accounts', {
|
|
430
455
|
method: 'PUT',
|
|
@@ -565,6 +590,9 @@ export default function BillingAccountDetailPage(props: { params?: { id?: string
|
|
|
565
590
|
) : (
|
|
566
591
|
<AccountView row={row} />
|
|
567
592
|
)}
|
|
593
|
+
{row.billing_mode === 'prepaid' ? (
|
|
594
|
+
<PrepaidPanel accountId={row.id} currencyCode={row.currency_code} />
|
|
595
|
+
) : null}
|
|
568
596
|
<AccountItemsSection accountId={row.id} />
|
|
569
597
|
<AccountUsageSection accountId={row.id} />
|
|
570
598
|
{ConfirmDialogElement}
|
|
@@ -43,8 +43,15 @@ export default function CreateBillingAccountPage() {
|
|
|
43
43
|
country: values.invoiceAddress.country.trim().toUpperCase(),
|
|
44
44
|
},
|
|
45
45
|
nextBillDate: values.nextBillDate,
|
|
46
|
+
billingMode: values.billingMode,
|
|
46
47
|
}
|
|
47
48
|
if (values.taxId.trim()) payload.taxId = values.taxId.trim()
|
|
49
|
+
const creditLimit = Number.parseFloat(values.creditLimit)
|
|
50
|
+
if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit
|
|
51
|
+
if (values.billingMode === 'prepaid' && values.lowBalanceThreshold.trim()) {
|
|
52
|
+
const threshold = Number.parseFloat(values.lowBalanceThreshold)
|
|
53
|
+
if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold
|
|
54
|
+
}
|
|
48
55
|
|
|
49
56
|
const call = await apiCallOrThrow<{ id: string }>('/api/billing/accounts', {
|
|
50
57
|
method: 'POST',
|
|
@@ -32,6 +32,7 @@ type BillingAccountRow = {
|
|
|
32
32
|
invoice_email: string
|
|
33
33
|
next_bill_date: string
|
|
34
34
|
last_bill_date: string | null
|
|
35
|
+
billing_mode: 'postpaid' | 'prepaid'
|
|
35
36
|
is_active: boolean
|
|
36
37
|
created_at: string
|
|
37
38
|
}
|
|
@@ -78,6 +79,15 @@ export default function BillingAccountsListPage() {
|
|
|
78
79
|
label: t('billing.accounts.filters.currency', 'Currency'),
|
|
79
80
|
type: 'text',
|
|
80
81
|
},
|
|
82
|
+
{
|
|
83
|
+
id: 'billingMode',
|
|
84
|
+
label: t('billing.accounts.columns.mode', 'Mode'),
|
|
85
|
+
type: 'select',
|
|
86
|
+
options: [
|
|
87
|
+
{ value: 'postpaid', label: t('billing.mode.postpaid', 'Postpaid') },
|
|
88
|
+
{ value: 'prepaid', label: t('billing.mode.prepaid', 'Prepaid') },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
81
91
|
{
|
|
82
92
|
id: 'isActive',
|
|
83
93
|
label: t('billing.accounts.filters.active', 'Active'),
|
|
@@ -107,6 +117,9 @@ export default function BillingAccountsListPage() {
|
|
|
107
117
|
if (typeof filters.currencyCode === 'string' && filters.currencyCode.trim()) {
|
|
108
118
|
params.set('currencyCode', filters.currencyCode.trim().toUpperCase())
|
|
109
119
|
}
|
|
120
|
+
if (typeof filters.billingMode === 'string' && filters.billingMode) {
|
|
121
|
+
params.set('billingMode', filters.billingMode)
|
|
122
|
+
}
|
|
110
123
|
if (filters.isActive === 'true') params.set('isActive', 'true')
|
|
111
124
|
if (filters.isActive === 'false') params.set('isActive', 'false')
|
|
112
125
|
const result = await readApiResultOrThrow<ListResponse>(
|
|
@@ -118,7 +131,15 @@ export default function BillingAccountsListPage() {
|
|
|
118
131
|
} finally {
|
|
119
132
|
setLoading(false)
|
|
120
133
|
}
|
|
121
|
-
}, [
|
|
134
|
+
}, [
|
|
135
|
+
filters.billCycle,
|
|
136
|
+
filters.currencyCode,
|
|
137
|
+
filters.billingMode,
|
|
138
|
+
filters.isActive,
|
|
139
|
+
page,
|
|
140
|
+
pageSize,
|
|
141
|
+
search,
|
|
142
|
+
])
|
|
122
143
|
|
|
123
144
|
React.useEffect(() => {
|
|
124
145
|
void loadRows()
|
|
@@ -154,6 +175,16 @@ export default function BillingAccountsListPage() {
|
|
|
154
175
|
</span>
|
|
155
176
|
),
|
|
156
177
|
},
|
|
178
|
+
{
|
|
179
|
+
accessorKey: 'billing_mode',
|
|
180
|
+
header: t('billing.accounts.columns.mode', 'Mode'),
|
|
181
|
+
cell: ({ row }) =>
|
|
182
|
+
row.original.billing_mode === 'prepaid' ? (
|
|
183
|
+
<Tag variant="brand">{t('billing.mode.prepaid', 'Prepaid')}</Tag>
|
|
184
|
+
) : (
|
|
185
|
+
<Tag variant="neutral">{t('billing.mode.postpaid', 'Postpaid')}</Tag>
|
|
186
|
+
),
|
|
187
|
+
},
|
|
157
188
|
{
|
|
158
189
|
accessorKey: 'next_bill_date',
|
|
159
190
|
header: t('billing.accounts.columns.next_bill', 'Next bill'),
|