@dainamite/billing 0.17.0 → 0.18.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.
Files changed (205) hide show
  1. package/CHANGELOG.md +81 -19
  2. package/dist/modules/billing/acl.js +9 -1
  3. package/dist/modules/billing/acl.js.map +2 -2
  4. package/dist/modules/billing/api/accounts/[id]/balance/route.js +62 -0
  5. package/dist/modules/billing/api/accounts/[id]/balance/route.js.map +7 -0
  6. package/dist/modules/billing/api/accounts/[id]/credit-status/route.js +56 -0
  7. package/dist/modules/billing/api/accounts/[id]/credit-status/route.js.map +7 -0
  8. package/dist/modules/billing/api/accounts/[id]/topups/route.js +36 -0
  9. package/dist/modules/billing/api/accounts/[id]/topups/route.js.map +7 -0
  10. package/dist/modules/billing/api/accounts/route.js +7 -0
  11. package/dist/modules/billing/api/accounts/route.js.map +2 -2
  12. package/dist/modules/billing/api/export/account/[id]/route.js +48 -3
  13. package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
  14. package/dist/modules/billing/api/statements/generate/route.js +68 -0
  15. package/dist/modules/billing/api/statements/generate/route.js.map +7 -0
  16. package/dist/modules/billing/api/statements/route.js +110 -0
  17. package/dist/modules/billing/api/statements/route.js.map +7 -0
  18. package/dist/modules/billing/api/topups/route.js +139 -0
  19. package/dist/modules/billing/api/topups/route.js.map +7 -0
  20. package/dist/modules/billing/api/transactions/adjust/route.js +71 -0
  21. package/dist/modules/billing/api/transactions/adjust/route.js.map +7 -0
  22. package/dist/modules/billing/api/transactions/route.js +109 -0
  23. package/dist/modules/billing/api/transactions/route.js.map +7 -0
  24. package/dist/modules/billing/api/usage/route.js +30 -6
  25. package/dist/modules/billing/api/usage/route.js.map +2 -2
  26. package/dist/modules/billing/backend/billing/accounts/[id]/page.js +16 -1
  27. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
  28. package/dist/modules/billing/backend/billing/accounts/create/page.js +8 -1
  29. package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
  30. package/dist/modules/billing/backend/billing/accounts/page.js +26 -1
  31. package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
  32. package/dist/modules/billing/backend/billing/statements/[id]/page.js +129 -0
  33. package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +7 -0
  34. package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js +21 -0
  35. package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js.map +7 -0
  36. package/dist/modules/billing/backend/billing/statements/page.js +152 -0
  37. package/dist/modules/billing/backend/billing/statements/page.js.map +7 -0
  38. package/dist/modules/billing/backend/billing/statements/page.meta.js +18 -0
  39. package/dist/modules/billing/backend/billing/statements/page.meta.js.map +7 -0
  40. package/dist/modules/billing/backend/billing/topups/[id]/page.js +90 -0
  41. package/dist/modules/billing/backend/billing/topups/[id]/page.js.map +7 -0
  42. package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js +21 -0
  43. package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js.map +7 -0
  44. package/dist/modules/billing/backend/billing/topups/page.js +147 -0
  45. package/dist/modules/billing/backend/billing/topups/page.js.map +7 -0
  46. package/dist/modules/billing/backend/billing/topups/page.meta.js +18 -0
  47. package/dist/modules/billing/backend/billing/topups/page.meta.js.map +7 -0
  48. package/dist/modules/billing/backend/billing/transactions/[id]/page.js +114 -0
  49. package/dist/modules/billing/backend/billing/transactions/[id]/page.js.map +7 -0
  50. package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js +21 -0
  51. package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js.map +7 -0
  52. package/dist/modules/billing/backend/billing/transactions/page.js +149 -0
  53. package/dist/modules/billing/backend/billing/transactions/page.js.map +7 -0
  54. package/dist/modules/billing/backend/billing/transactions/page.meta.js +18 -0
  55. package/dist/modules/billing/backend/billing/transactions/page.meta.js.map +7 -0
  56. package/dist/modules/billing/commands/accounts.js +26 -1
  57. package/dist/modules/billing/commands/accounts.js.map +2 -2
  58. package/dist/modules/billing/commands/index.js +3 -0
  59. package/dist/modules/billing/commands/index.js.map +2 -2
  60. package/dist/modules/billing/commands/invoices.js +21 -1
  61. package/dist/modules/billing/commands/invoices.js.map +2 -2
  62. package/dist/modules/billing/commands/statements.js +74 -0
  63. package/dist/modules/billing/commands/statements.js.map +7 -0
  64. package/dist/modules/billing/commands/topups.js +139 -0
  65. package/dist/modules/billing/commands/topups.js.map +7 -0
  66. package/dist/modules/billing/commands/transactions.js +119 -0
  67. package/dist/modules/billing/commands/transactions.js.map +7 -0
  68. package/dist/modules/billing/commands/usage.js +72 -0
  69. package/dist/modules/billing/commands/usage.js.map +2 -2
  70. package/dist/modules/billing/components/AccountForm.js +53 -1
  71. package/dist/modules/billing/components/AccountForm.js.map +2 -2
  72. package/dist/modules/billing/components/PrepaidPanel.js +314 -0
  73. package/dist/modules/billing/components/PrepaidPanel.js.map +7 -0
  74. package/dist/modules/billing/components/accountNames.js +20 -0
  75. package/dist/modules/billing/components/accountNames.js.map +7 -0
  76. package/dist/modules/billing/data/entities.js +290 -0
  77. package/dist/modules/billing/data/entities.js.map +2 -2
  78. package/dist/modules/billing/data/entityIds.js +6 -1
  79. package/dist/modules/billing/data/entityIds.js.map +2 -2
  80. package/dist/modules/billing/data/validators.js +107 -2
  81. package/dist/modules/billing/data/validators.js.map +2 -2
  82. package/dist/modules/billing/events.js +52 -0
  83. package/dist/modules/billing/events.js.map +2 -2
  84. package/dist/modules/billing/i18n/en.json +112 -4
  85. package/dist/modules/billing/i18n/pl.json +112 -4
  86. package/dist/modules/billing/index.js +1 -1
  87. package/dist/modules/billing/index.js.map +2 -2
  88. package/dist/modules/billing/lib/accountStatus.js +106 -0
  89. package/dist/modules/billing/lib/accountStatus.js.map +7 -0
  90. package/dist/modules/billing/lib/balanceEvents.js +48 -0
  91. package/dist/modules/billing/lib/balanceEvents.js.map +7 -0
  92. package/dist/modules/billing/lib/balanceLedger.js +136 -0
  93. package/dist/modules/billing/lib/balanceLedger.js.map +7 -0
  94. package/dist/modules/billing/lib/balanceStatus.js +29 -0
  95. package/dist/modules/billing/lib/balanceStatus.js.map +7 -0
  96. package/dist/modules/billing/lib/billRunEngine.js +40 -2
  97. package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
  98. package/dist/modules/billing/lib/creditStatus.js +65 -0
  99. package/dist/modules/billing/lib/creditStatus.js.map +7 -0
  100. package/dist/modules/billing/lib/money.js +47 -0
  101. package/dist/modules/billing/lib/money.js.map +7 -0
  102. package/dist/modules/billing/lib/prepaidConfig.js +45 -0
  103. package/dist/modules/billing/lib/prepaidConfig.js.map +7 -0
  104. package/dist/modules/billing/lib/prepaidConsume.js +214 -0
  105. package/dist/modules/billing/lib/prepaidConsume.js.map +7 -0
  106. package/dist/modules/billing/lib/prepaidPeriodClose.js +285 -0
  107. package/dist/modules/billing/lib/prepaidPeriodClose.js.map +7 -0
  108. package/dist/modules/billing/lib/topupCapture.js +62 -0
  109. package/dist/modules/billing/lib/topupCapture.js.map +7 -0
  110. package/dist/modules/billing/lib/topupReceipt.js +97 -0
  111. package/dist/modules/billing/lib/topupReceipt.js.map +7 -0
  112. package/dist/modules/billing/lib/topupTransition.js +19 -0
  113. package/dist/modules/billing/lib/topupTransition.js.map +7 -0
  114. package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js +110 -0
  115. package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js.map +7 -0
  116. package/dist/modules/billing/setup.js +32 -1
  117. package/dist/modules/billing/setup.js.map +2 -2
  118. package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js +26 -0
  119. package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js.map +7 -0
  120. package/dist/modules/billing/subscribers/prepaid-topup-captured.js +87 -0
  121. package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +7 -0
  122. package/dist/modules/billing/subscribers/prepaid-topup-expired.js +26 -0
  123. package/dist/modules/billing/subscribers/prepaid-topup-expired.js.map +7 -0
  124. package/dist/modules/billing/subscribers/prepaid-topup-failed.js +26 -0
  125. package/dist/modules/billing/subscribers/prepaid-topup-failed.js.map +7 -0
  126. package/dist/modules/billing/workers/reconcile-prepaid-balances.js +50 -0
  127. package/dist/modules/billing/workers/reconcile-prepaid-balances.js.map +7 -0
  128. package/package.json +1 -1
  129. package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
  130. package/src/modules/billing/__tests__/commands/transactions.test.ts +84 -0
  131. package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
  132. package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
  133. package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
  134. package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
  135. package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
  136. package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
  137. package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
  138. package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
  139. package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
  140. package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
  141. package/src/modules/billing/__tests__/setup.test.ts +28 -2
  142. package/src/modules/billing/acl.ts +8 -0
  143. package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
  144. package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
  145. package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
  146. package/src/modules/billing/api/accounts/route.ts +7 -0
  147. package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
  148. package/src/modules/billing/api/statements/generate/route.ts +78 -0
  149. package/src/modules/billing/api/statements/route.ts +117 -0
  150. package/src/modules/billing/api/topups/route.ts +154 -0
  151. package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
  152. package/src/modules/billing/api/transactions/route.ts +116 -0
  153. package/src/modules/billing/api/usage/route.ts +36 -5
  154. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
  155. package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
  156. package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
  157. package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
  158. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
  159. package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
  160. package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
  161. package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
  162. package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
  163. package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
  164. package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
  165. package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
  166. package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
  167. package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
  168. package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
  169. package/src/modules/billing/commands/accounts.ts +42 -1
  170. package/src/modules/billing/commands/index.ts +3 -0
  171. package/src/modules/billing/commands/invoices.ts +26 -2
  172. package/src/modules/billing/commands/statements.ts +101 -0
  173. package/src/modules/billing/commands/topups.ts +181 -0
  174. package/src/modules/billing/commands/transactions.ts +152 -0
  175. package/src/modules/billing/commands/usage.ts +114 -4
  176. package/src/modules/billing/components/AccountForm.tsx +66 -0
  177. package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
  178. package/src/modules/billing/components/accountNames.ts +23 -0
  179. package/src/modules/billing/data/entities.ts +383 -0
  180. package/src/modules/billing/data/entityIds.ts +5 -0
  181. package/src/modules/billing/data/validators.ts +144 -0
  182. package/src/modules/billing/events.ts +52 -0
  183. package/src/modules/billing/i18n/en.json +112 -4
  184. package/src/modules/billing/i18n/pl.json +112 -4
  185. package/src/modules/billing/index.ts +1 -1
  186. package/src/modules/billing/lib/accountStatus.ts +155 -0
  187. package/src/modules/billing/lib/balanceEvents.ts +74 -0
  188. package/src/modules/billing/lib/balanceLedger.ts +258 -0
  189. package/src/modules/billing/lib/balanceStatus.ts +78 -0
  190. package/src/modules/billing/lib/billRunEngine.ts +66 -5
  191. package/src/modules/billing/lib/creditStatus.ts +153 -0
  192. package/src/modules/billing/lib/money.ts +73 -0
  193. package/src/modules/billing/lib/prepaidConfig.ts +58 -0
  194. package/src/modules/billing/lib/prepaidConsume.ts +299 -0
  195. package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
  196. package/src/modules/billing/lib/topupCapture.ts +117 -0
  197. package/src/modules/billing/lib/topupReceipt.ts +153 -0
  198. package/src/modules/billing/lib/topupTransition.ts +31 -0
  199. package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
  200. package/src/modules/billing/setup.ts +45 -0
  201. package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
  202. package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
  203. package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
  204. package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
  205. package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
@@ -0,0 +1,19 @@
1
+ import React from 'react'
2
+ import { FileText } from 'lucide-react'
3
+
4
+ const statementIcon = React.createElement(FileText, { size: 16 })
5
+
6
+ export const metadata = {
7
+ requireAuth: true,
8
+ requireFeatures: ['billing.statement.view'],
9
+ pageTitle: 'Statement',
10
+ pageTitleKey: 'billing.statements.detail.title',
11
+ pageGroup: 'Billing',
12
+ pageGroupKey: 'billing.nav.group',
13
+ navHidden: true,
14
+ icon: statementIcon,
15
+ breadcrumb: [
16
+ { label: 'Statements', labelKey: 'billing.statements.title', href: '/backend/billing/statements' },
17
+ { label: 'Statement', labelKey: 'billing.statements.detail.title' },
18
+ ],
19
+ } as const
@@ -0,0 +1,198 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import Link from 'next/link'
4
+ import { useParams } from 'next/navigation'
5
+ import type { ColumnDef } from '@tanstack/react-table'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
+ import { FormHeader } from '@open-mercato/ui/backend/forms'
9
+ import { Tag } from '@open-mercato/ui/primitives/tag'
10
+ import { Button } from '@open-mercato/ui/primitives/button'
11
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
12
+ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
13
+ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
14
+ import { DetailCard, DetailField } from '../../../../components/DetailFields'
15
+ import { statementStatusVariant } from '../page'
16
+
17
+ type BreakdownEntry = {
18
+ billing_item_id: string
19
+ billing_type: string
20
+ uom_code?: string
21
+ quantity: string
22
+ amount: string
23
+ }
24
+
25
+ type StatementRow = {
26
+ id: string
27
+ bill_account_id: string
28
+ bill_run_id: string | null
29
+ period_start: string
30
+ period_end: string
31
+ currency_code: string
32
+ opening_balance: string
33
+ total_topups: string
34
+ total_usage: string
35
+ total_recurring: string
36
+ total_adjustments: string
37
+ closing_balance: string
38
+ debit_breakdown: { items?: BreakdownEntry[] } | null
39
+ status: string
40
+ }
41
+
42
+ function formatDate(value: string | null | undefined): string {
43
+ if (!value) return '—'
44
+ const d = new Date(value)
45
+ return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()
46
+ }
47
+
48
+ export default function BillingStatementDetailPage(props: { params?: { id?: string } }) {
49
+ const t = useT()
50
+ const urlParams = useParams<{ id: string }>()
51
+ const statementId = (props.params?.id ?? urlParams?.id ?? '') as string
52
+
53
+ const [row, setRow] = React.useState<StatementRow | null>(null)
54
+ const [loading, setLoading] = React.useState(true)
55
+ const [error, setError] = React.useState<string | null>(null)
56
+
57
+ React.useEffect(() => {
58
+ let cancelled = false
59
+ if (!statementId) return
60
+ setLoading(true)
61
+ readApiResultOrThrow<{ items: StatementRow[] }>(
62
+ `/api/billing/statements?id=${statementId}&pageSize=1`,
63
+ )
64
+ .then((res) => {
65
+ if (cancelled) return
66
+ const found = res.items?.[0]
67
+ if (!found) setError(t('billing.statements.detail.not_found', 'Statement not found'))
68
+ else setRow(found)
69
+ })
70
+ .catch(() => {
71
+ if (!cancelled)
72
+ setError(t('billing.statements.detail.load_error', 'Failed to load statement'))
73
+ })
74
+ .finally(() => {
75
+ if (!cancelled) setLoading(false)
76
+ })
77
+ return () => {
78
+ cancelled = true
79
+ }
80
+ }, [statementId, t])
81
+
82
+ const breakdown = row?.debit_breakdown?.items ?? []
83
+ const columns = React.useMemo<ColumnDef<BreakdownEntry>[]>(
84
+ () => [
85
+ {
86
+ accessorKey: 'billing_type',
87
+ header: t('billing.statements.breakdown.type', 'Type'),
88
+ cell: ({ row }) => <Tag variant="info">{row.original.billing_type}</Tag>,
89
+ },
90
+ {
91
+ accessorKey: 'billing_item_id',
92
+ header: t('billing.statements.breakdown.item', 'Item'),
93
+ cell: ({ row }) => <span className="font-mono text-xs">{row.original.billing_item_id}</span>,
94
+ },
95
+ {
96
+ accessorKey: 'quantity',
97
+ header: t('billing.statements.breakdown.quantity', 'Qty'),
98
+ cell: ({ row }) => <span className="font-mono tabular-nums">{row.original.quantity}</span>,
99
+ },
100
+ {
101
+ accessorKey: 'amount',
102
+ header: t('billing.statements.breakdown.amount', 'Amount'),
103
+ cell: ({ row }) => <span className="font-mono tabular-nums">{row.original.amount}</span>,
104
+ },
105
+ ],
106
+ [t],
107
+ )
108
+
109
+ if (error) {
110
+ return (
111
+ <Page>
112
+ <PageBody>
113
+ <ErrorMessage label={error} />
114
+ </PageBody>
115
+ </Page>
116
+ )
117
+ }
118
+ if (loading || !row) {
119
+ return (
120
+ <Page>
121
+ <PageBody>
122
+ <LoadingMessage label={t('billing.statements.detail.loading', 'Loading statement…')} />
123
+ </PageBody>
124
+ </Page>
125
+ )
126
+ }
127
+
128
+ return (
129
+ <Page>
130
+ <FormHeader
131
+ mode="detail"
132
+ backHref="/backend/billing/statements"
133
+ entityTypeLabel={t('billing.statements.detail.title', 'Statement')}
134
+ title={`${formatDate(row.period_start)} – ${formatDate(row.period_end)}`}
135
+ statusBadge={
136
+ <Tag variant={statementStatusVariant(row.status)}>
137
+ {t(`billing.statements.status.${row.status}`, row.status)}
138
+ </Tag>
139
+ }
140
+ actionsContent={
141
+ <Button asChild variant="outline">
142
+ <Link href={`/backend/billing/accounts/${row.bill_account_id}`}>
143
+ {t('billing.statements.detail.view_account', 'View account')}
144
+ </Link>
145
+ </Button>
146
+ }
147
+ />
148
+ <PageBody>
149
+ <DetailCard>
150
+ <DetailField label={t('billing.statements.columns.opening', 'Opening')}>
151
+ <span className="font-mono tabular-nums">{row.opening_balance}</span>
152
+ </DetailField>
153
+ <DetailField label={t('billing.statements.columns.topups', 'Top-ups')}>
154
+ <span className="font-mono tabular-nums">{row.total_topups}</span>
155
+ </DetailField>
156
+ <DetailField label={t('billing.statements.columns.usage', 'Usage')}>
157
+ <span className="font-mono tabular-nums">{row.total_usage}</span>
158
+ </DetailField>
159
+ <DetailField label={t('billing.statements.columns.recurring', 'Recurring')}>
160
+ <span className="font-mono tabular-nums">{row.total_recurring}</span>
161
+ </DetailField>
162
+ <DetailField label={t('billing.statements.columns.adjustments', 'Adjustments')}>
163
+ <span className="font-mono tabular-nums">{row.total_adjustments}</span>
164
+ </DetailField>
165
+ <DetailField label={t('billing.statements.columns.closing', 'Closing')}>
166
+ <span className="font-mono tabular-nums">
167
+ {row.closing_balance} {row.currency_code}
168
+ </span>
169
+ </DetailField>
170
+ <DetailField label={t('billing.statements.detail.bill_run', 'Bill Run')}>
171
+ {row.bill_run_id ? (
172
+ <Link
173
+ href={`/backend/billing/runs/${row.bill_run_id}`}
174
+ className="font-mono text-xs hover:underline"
175
+ >
176
+ {row.bill_run_id}
177
+ </Link>
178
+ ) : (
179
+ '—'
180
+ )}
181
+ </DetailField>
182
+ </DetailCard>
183
+ <div className="mt-6">
184
+ <DataTable
185
+ title={t('billing.statements.detail.breakdown', 'Period-close charges')}
186
+ columns={columns}
187
+ data={breakdown}
188
+ emptyState={
189
+ <div className="rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground">
190
+ {t('billing.statements.detail.no_charges', 'No recurring charges this period.')}
191
+ </div>
192
+ }
193
+ />
194
+ </div>
195
+ </PageBody>
196
+ </Page>
197
+ )
198
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react'
2
+ import { FileText } from 'lucide-react'
3
+
4
+ const statementsIcon = React.createElement(FileText, { size: 16 })
5
+
6
+ export const metadata = {
7
+ requireAuth: true,
8
+ requireFeatures: ['billing.statement.view'],
9
+ pageTitle: 'Statements',
10
+ pageTitleKey: 'billing.statements.title',
11
+ pageGroup: 'Billing',
12
+ pageGroupKey: 'billing.nav.group',
13
+ pageOrder: 60,
14
+ icon: statementsIcon,
15
+ breadcrumb: [{ label: 'Statements', labelKey: 'billing.statements.title' }],
16
+ } as const
@@ -0,0 +1,190 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { useRouter } from 'next/navigation'
4
+ import type { ColumnDef } from '@tanstack/react-table'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
7
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
+ import { DataTable } from '@open-mercato/ui/backend/DataTable'
9
+ import { Tag } from '@open-mercato/ui/primitives/tag'
10
+ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { fetchAccountNames } from '../../../components/accountNames'
12
+
13
+ /**
14
+ * Consumption statements admin list (SPEC-002 P3). Non-fiscal period summaries
15
+ * for prepaid accounts — opening / top-ups / usage / recurring / closing.
16
+ */
17
+
18
+ type StatementRow = {
19
+ id: string
20
+ bill_account_id: string
21
+ period_start: string
22
+ period_end: string
23
+ currency_code: string
24
+ opening_balance: string
25
+ total_topups: string
26
+ total_usage: string
27
+ total_recurring: string
28
+ closing_balance: string
29
+ status: string
30
+ }
31
+
32
+ type ListResponse = { items: StatementRow[]; total: number; totalPages: number }
33
+
34
+ function formatDate(value: string | null | undefined): string {
35
+ if (!value) return '—'
36
+ const d = new Date(value)
37
+ return Number.isNaN(d.getTime()) ? value : d.toLocaleDateString()
38
+ }
39
+
40
+ export function statementStatusVariant(status: string): 'success' | 'info' {
41
+ return status === 'sent' ? 'success' : 'info'
42
+ }
43
+
44
+ export default function BillingStatementsListPage() {
45
+ const t = useT()
46
+ const router = useRouter()
47
+ const [rows, setRows] = React.useState<StatementRow[]>([])
48
+ const [page, setPage] = React.useState(1)
49
+ const [pageSize] = React.useState(25)
50
+ const [total, setTotal] = React.useState(0)
51
+ const [totalPages, setTotalPages] = React.useState(1)
52
+ const [loading, setLoading] = React.useState(true)
53
+ const [filters, setFilters] = React.useState<FilterValues>({})
54
+ const [accountNames, setAccountNames] = React.useState<Record<string, string>>({})
55
+
56
+ const filterDefs = React.useMemo<FilterDef[]>(
57
+ () => [
58
+ {
59
+ id: 'status',
60
+ label: t('billing.statements.filters.status', 'Status'),
61
+ type: 'select',
62
+ options: [
63
+ { value: 'generated', label: t('billing.statements.status.generated', 'Generated') },
64
+ { value: 'sent', label: t('billing.statements.status.sent', 'Sent') },
65
+ ],
66
+ },
67
+ ],
68
+ [t],
69
+ )
70
+
71
+ const loadRows = React.useCallback(async () => {
72
+ setLoading(true)
73
+ try {
74
+ const params = new URLSearchParams({
75
+ page: String(page),
76
+ pageSize: String(pageSize),
77
+ sortField: 'periodEnd',
78
+ sortDir: 'desc',
79
+ })
80
+ if (typeof filters.status === 'string' && filters.status) params.set('status', filters.status)
81
+ const result = await readApiResultOrThrow<ListResponse>(
82
+ `/api/billing/statements?${params.toString()}`,
83
+ )
84
+ const items = result.items ?? []
85
+ setRows(items)
86
+ setTotal(result.total ?? 0)
87
+ setTotalPages(result.totalPages ?? 1)
88
+ try {
89
+ const names = await fetchAccountNames(items.map((r) => r.bill_account_id))
90
+ setAccountNames((prev) => ({ ...prev, ...names }))
91
+ } catch {
92
+ // Name resolution is best-effort — fall back to the id in the cell.
93
+ }
94
+ } finally {
95
+ setLoading(false)
96
+ }
97
+ }, [filters.status, page, pageSize])
98
+
99
+ React.useEffect(() => {
100
+ void loadRows()
101
+ }, [loadRows])
102
+
103
+ const columns = React.useMemo<ColumnDef<StatementRow>[]>(
104
+ () => [
105
+ {
106
+ accessorKey: 'period_end',
107
+ header: t('billing.statements.columns.period', 'Period'),
108
+ cell: ({ row }) =>
109
+ `${formatDate(row.original.period_start)} – ${formatDate(row.original.period_end)}`,
110
+ },
111
+ {
112
+ accessorKey: 'bill_account_id',
113
+ header: t('billing.statements.columns.account', 'Account'),
114
+ cell: ({ row }) => {
115
+ const name = accountNames[row.original.bill_account_id]
116
+ return name ? (
117
+ <span className="text-sm">{name}</span>
118
+ ) : (
119
+ <span className="font-mono text-xs">{row.original.bill_account_id}</span>
120
+ )
121
+ },
122
+ },
123
+ {
124
+ accessorKey: 'opening_balance',
125
+ header: t('billing.statements.columns.opening', 'Opening'),
126
+ cell: ({ row }) => <span className="font-mono tabular-nums">{row.original.opening_balance}</span>,
127
+ },
128
+ {
129
+ accessorKey: 'total_topups',
130
+ header: t('billing.statements.columns.topups', 'Top-ups'),
131
+ cell: ({ row }) => <span className="font-mono tabular-nums">{row.original.total_topups}</span>,
132
+ },
133
+ {
134
+ accessorKey: 'total_usage',
135
+ header: t('billing.statements.columns.usage', 'Usage'),
136
+ cell: ({ row }) => <span className="font-mono tabular-nums">{row.original.total_usage}</span>,
137
+ },
138
+ {
139
+ accessorKey: 'total_recurring',
140
+ header: t('billing.statements.columns.recurring', 'Recurring'),
141
+ cell: ({ row }) => <span className="font-mono tabular-nums">{row.original.total_recurring}</span>,
142
+ },
143
+ {
144
+ accessorKey: 'closing_balance',
145
+ header: t('billing.statements.columns.closing', 'Closing'),
146
+ cell: ({ row }) => (
147
+ <span className="font-mono tabular-nums">
148
+ {row.original.closing_balance} {row.original.currency_code}
149
+ </span>
150
+ ),
151
+ },
152
+ {
153
+ accessorKey: 'status',
154
+ header: t('billing.statements.columns.status', 'Status'),
155
+ cell: ({ row }) => (
156
+ <Tag variant={statementStatusVariant(row.original.status)}>
157
+ {t(`billing.statements.status.${row.original.status}`, row.original.status)}
158
+ </Tag>
159
+ ),
160
+ },
161
+ ],
162
+ [t, accountNames],
163
+ )
164
+
165
+ return (
166
+ <Page>
167
+ <PageBody>
168
+ <DataTable
169
+ title={t('billing.statements.title', 'Statements')}
170
+ refreshButton={{
171
+ label: t('billing.common.refresh', 'Refresh'),
172
+ onRefresh: loadRows,
173
+ isRefreshing: loading,
174
+ }}
175
+ columnChooser={{ auto: true }}
176
+ perspective={{ tableId: 'billing-statements' }}
177
+ columns={columns}
178
+ data={rows}
179
+ onRowClick={(row) => router.push(`/backend/billing/statements/${row.id}`)}
180
+ isLoading={loading}
181
+ pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}
182
+ filters={filterDefs}
183
+ filterValues={filters}
184
+ onFiltersApply={setFilters}
185
+ onFiltersClear={() => setFilters({})}
186
+ />
187
+ </PageBody>
188
+ </Page>
189
+ )
190
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react'
2
+ import { Wallet } from 'lucide-react'
3
+
4
+ const topupIcon = React.createElement(Wallet, { size: 16 })
5
+
6
+ export const metadata = {
7
+ requireAuth: true,
8
+ requireFeatures: ['billing.topup.view'],
9
+ pageTitle: 'Top-up',
10
+ pageTitleKey: 'billing.topups.detail.title',
11
+ pageGroup: 'Billing',
12
+ pageGroupKey: 'billing.nav.group',
13
+ navHidden: true,
14
+ icon: topupIcon,
15
+ breadcrumb: [
16
+ { label: 'Top-ups', labelKey: 'billing.topups.title', href: '/backend/billing/topups' },
17
+ { label: 'Top-up', labelKey: 'billing.topups.detail.title' },
18
+ ],
19
+ } as const
@@ -0,0 +1,151 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import Link from 'next/link'
4
+ import { useParams } from 'next/navigation'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
+ import { FormHeader } from '@open-mercato/ui/backend/forms'
8
+ import { Tag } from '@open-mercato/ui/primitives/tag'
9
+ import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
11
+ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
12
+ import { DetailCard, DetailField } from '../../../../components/DetailFields'
13
+ import { statusVariant } from '../page'
14
+
15
+ type TopupRow = {
16
+ id: string
17
+ bill_account_id: string
18
+ status: string
19
+ amount: string
20
+ currency_code: string
21
+ provider_key: string
22
+ payment_id: string
23
+ gateway_transaction_id: string | null
24
+ transaction_id: string | null
25
+ receipt_invoice_id: string | null
26
+ source_ref: string | null
27
+ created_at: string
28
+ updated_at: string
29
+ }
30
+
31
+ function formatDate(value: string | null | undefined): string {
32
+ if (!value) return '—'
33
+ const d = new Date(value)
34
+ return Number.isNaN(d.getTime()) ? value : d.toLocaleString()
35
+ }
36
+
37
+ export default function BillingTopupDetailPage(props: { params?: { id?: string } }) {
38
+ const t = useT()
39
+ const urlParams = useParams<{ id: string }>()
40
+ const topupId = (props.params?.id ?? urlParams?.id ?? '') as string
41
+
42
+ const [row, setRow] = React.useState<TopupRow | null>(null)
43
+ const [loading, setLoading] = React.useState(true)
44
+ const [error, setError] = React.useState<string | null>(null)
45
+
46
+ React.useEffect(() => {
47
+ let cancelled = false
48
+ if (!topupId) return
49
+ setLoading(true)
50
+ readApiResultOrThrow<{ items: TopupRow[] }>(`/api/billing/topups?id=${topupId}&pageSize=1`)
51
+ .then((res) => {
52
+ if (cancelled) return
53
+ const found = res.items?.[0]
54
+ if (!found) setError(t('billing.topups.detail.not_found', 'Top-up not found'))
55
+ else setRow(found)
56
+ })
57
+ .catch(() => {
58
+ if (!cancelled) setError(t('billing.topups.detail.load_error', 'Failed to load top-up'))
59
+ })
60
+ .finally(() => {
61
+ if (!cancelled) setLoading(false)
62
+ })
63
+ return () => {
64
+ cancelled = true
65
+ }
66
+ }, [topupId, t])
67
+
68
+ if (error) {
69
+ return (
70
+ <Page>
71
+ <PageBody>
72
+ <ErrorMessage label={error} />
73
+ </PageBody>
74
+ </Page>
75
+ )
76
+ }
77
+ if (loading || !row) {
78
+ return (
79
+ <Page>
80
+ <PageBody>
81
+ <LoadingMessage label={t('billing.topups.detail.loading', 'Loading top-up…')} />
82
+ </PageBody>
83
+ </Page>
84
+ )
85
+ }
86
+
87
+ return (
88
+ <Page>
89
+ <FormHeader
90
+ mode="detail"
91
+ backHref="/backend/billing/topups"
92
+ entityTypeLabel={t('billing.topups.detail.title', 'Top-up')}
93
+ title={`${row.amount} ${row.currency_code}`}
94
+ statusBadge={
95
+ <Tag variant={statusVariant(row.status)}>
96
+ {t(`billing.topups.status.${row.status}`, row.status)}
97
+ </Tag>
98
+ }
99
+ actionsContent={
100
+ <Button asChild variant="outline">
101
+ <Link href={`/backend/billing/accounts/${row.bill_account_id}`}>
102
+ {t('billing.topups.detail.view_account', 'View account')}
103
+ </Link>
104
+ </Button>
105
+ }
106
+ />
107
+ <PageBody>
108
+ <DetailCard>
109
+ <DetailField label={t('billing.topups.columns.amount', 'Amount')}>
110
+ <span className="font-mono tabular-nums">
111
+ {row.amount} {row.currency_code}
112
+ </span>
113
+ </DetailField>
114
+ <DetailField label={t('billing.topups.columns.provider', 'Provider')}>
115
+ {row.provider_key}
116
+ </DetailField>
117
+ <DetailField label={t('billing.topups.columns.created', 'Created')}>
118
+ {formatDate(row.created_at)}
119
+ </DetailField>
120
+ <DetailField label={t('billing.topups.detail.updated', 'Updated')}>
121
+ {formatDate(row.updated_at)}
122
+ </DetailField>
123
+ <DetailField label={t('billing.topups.detail.payment_id', 'Payment ID')}>
124
+ <span className="font-mono text-xs">{row.payment_id}</span>
125
+ </DetailField>
126
+ <DetailField label={t('billing.topups.detail.gateway_txn', 'Gateway transaction')}>
127
+ <span className="font-mono text-xs">{row.gateway_transaction_id || '—'}</span>
128
+ </DetailField>
129
+ <DetailField label={t('billing.topups.detail.balance_txn', 'Balance transaction')}>
130
+ <span className="font-mono text-xs">{row.transaction_id || '—'}</span>
131
+ </DetailField>
132
+ <DetailField label={t('billing.topups.detail.receipt', 'VAT receipt')}>
133
+ {row.receipt_invoice_id ? (
134
+ <Link
135
+ href={`/backend/billing/invoices/${row.receipt_invoice_id}`}
136
+ className="font-mono text-xs hover:underline"
137
+ >
138
+ {row.receipt_invoice_id}
139
+ </Link>
140
+ ) : (
141
+ '—'
142
+ )}
143
+ </DetailField>
144
+ <DetailField label={t('billing.common.id', 'ID')}>
145
+ <span className="font-mono text-xs">{row.id}</span>
146
+ </DetailField>
147
+ </DetailCard>
148
+ </PageBody>
149
+ </Page>
150
+ )
151
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react'
2
+ import { Wallet } from 'lucide-react'
3
+
4
+ const topupsIcon = React.createElement(Wallet, { size: 16 })
5
+
6
+ export const metadata = {
7
+ requireAuth: true,
8
+ requireFeatures: ['billing.topup.view'],
9
+ pageTitle: 'Top-ups',
10
+ pageTitleKey: 'billing.topups.title',
11
+ pageGroup: 'Billing',
12
+ pageGroupKey: 'billing.nav.group',
13
+ pageOrder: 50,
14
+ icon: topupsIcon,
15
+ breadcrumb: [{ label: 'Top-ups', labelKey: 'billing.topups.title' }],
16
+ } as const