@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.
- package/CHANGELOG.md +81 -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 +1 -1
- package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
- package/src/modules/billing/__tests__/commands/transactions.test.ts +84 -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,119 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import {
|
|
3
|
+
ensureOrganizationScope,
|
|
4
|
+
ensureTenantScope
|
|
5
|
+
} from "@open-mercato/shared/lib/commands/scope";
|
|
6
|
+
import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
7
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
8
|
+
import { BillingAccount, BillingAccountBalance } from "../data/entities.js";
|
|
9
|
+
import { billingEntityIds } from "../data/entityIds.js";
|
|
10
|
+
import {
|
|
11
|
+
billingTransactionAdjustSchema
|
|
12
|
+
} from "../data/validators.js";
|
|
13
|
+
import { applyBalanceMovement } from "../lib/balanceLedger.js";
|
|
14
|
+
import { buildBalanceCrossingEvents } from "../lib/balanceEvents.js";
|
|
15
|
+
import { computeBalanceStatus, resolveLowBalanceThreshold } from "../lib/balanceStatus.js";
|
|
16
|
+
import { formatMoney } from "../lib/money.js";
|
|
17
|
+
import {
|
|
18
|
+
getLowBalanceThresholdDefault,
|
|
19
|
+
getNearLimitBufferDefault
|
|
20
|
+
} from "../lib/prepaidConfig.js";
|
|
21
|
+
import { emitBillingEvent } from "../events.js";
|
|
22
|
+
const transactionIndexer = { entityType: billingEntityIds.transaction };
|
|
23
|
+
const transactionEvents = { module: "billing", entity: "transaction", persistent: true };
|
|
24
|
+
function getEm(ctx) {
|
|
25
|
+
return ctx.container.resolve("em").fork();
|
|
26
|
+
}
|
|
27
|
+
function getDataEngine(ctx) {
|
|
28
|
+
return ctx.container.resolve("dataEngine");
|
|
29
|
+
}
|
|
30
|
+
const adjustTransactionCommand = {
|
|
31
|
+
id: "billing.transactions.adjust",
|
|
32
|
+
async execute(rawInput, ctx) {
|
|
33
|
+
const parsed = billingTransactionAdjustSchema.parse(rawInput);
|
|
34
|
+
ensureTenantScope(ctx, parsed.tenantId);
|
|
35
|
+
ensureOrganizationScope(ctx, parsed.organizationId);
|
|
36
|
+
const em = getEm(ctx);
|
|
37
|
+
const account = await em.findOne(BillingAccount, {
|
|
38
|
+
id: parsed.billAccountId,
|
|
39
|
+
tenantId: parsed.tenantId,
|
|
40
|
+
organizationId: parsed.organizationId,
|
|
41
|
+
deletedAt: null
|
|
42
|
+
});
|
|
43
|
+
if (!account) {
|
|
44
|
+
throw new CrudHttpError(404, { error: "Billing account not found" });
|
|
45
|
+
}
|
|
46
|
+
if (account.billingMode !== "prepaid") {
|
|
47
|
+
throw new CrudHttpError(409, {
|
|
48
|
+
error: "Balance adjustments are only available on prepaid accounts",
|
|
49
|
+
code: "billing.account.not_prepaid"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const balanceRow = await em.findOne(BillingAccountBalance, {
|
|
53
|
+
tenantId: parsed.tenantId,
|
|
54
|
+
billAccountId: account.id
|
|
55
|
+
});
|
|
56
|
+
const threshold = resolveLowBalanceThreshold(
|
|
57
|
+
balanceRow?.lowBalanceThreshold ?? null,
|
|
58
|
+
await getLowBalanceThresholdDefault(em)
|
|
59
|
+
);
|
|
60
|
+
const nearLimitBuffer = await getNearLimitBufferDefault(em);
|
|
61
|
+
const userId = ctx.auth?.sub ?? null;
|
|
62
|
+
const outcome = await em.transactional(
|
|
63
|
+
async (tem) => applyBalanceMovement(tem, {
|
|
64
|
+
organizationId: parsed.organizationId,
|
|
65
|
+
tenantId: parsed.tenantId,
|
|
66
|
+
billAccountId: account.id,
|
|
67
|
+
currencyCode: account.currencyCode,
|
|
68
|
+
type: "adjustment",
|
|
69
|
+
amount: formatMoney(parsed.amount),
|
|
70
|
+
description: parsed.description,
|
|
71
|
+
sourceRef: parsed.sourceRef ?? null,
|
|
72
|
+
userId,
|
|
73
|
+
metadata: { manual: true }
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
const created = outcome.transaction;
|
|
77
|
+
await emitCrudSideEffects({
|
|
78
|
+
dataEngine: getDataEngine(ctx),
|
|
79
|
+
action: "created",
|
|
80
|
+
entity: created,
|
|
81
|
+
identifiers: {
|
|
82
|
+
id: created.id,
|
|
83
|
+
tenantId: created.tenantId,
|
|
84
|
+
organizationId: created.organizationId
|
|
85
|
+
},
|
|
86
|
+
indexer: transactionIndexer,
|
|
87
|
+
events: transactionEvents
|
|
88
|
+
});
|
|
89
|
+
if (!outcome.deduplicated) {
|
|
90
|
+
const events = buildBalanceCrossingEvents({
|
|
91
|
+
scope: {
|
|
92
|
+
billAccountId: account.id,
|
|
93
|
+
tenantId: parsed.tenantId,
|
|
94
|
+
organizationId: parsed.organizationId,
|
|
95
|
+
currencyCode: account.currencyCode
|
|
96
|
+
},
|
|
97
|
+
balanceBefore: outcome.balanceBefore,
|
|
98
|
+
balanceAfter: outcome.balanceAfter,
|
|
99
|
+
threshold,
|
|
100
|
+
creditLimit: account.creditLimit ?? "0",
|
|
101
|
+
nearLimitBuffer
|
|
102
|
+
});
|
|
103
|
+
for (const ev of events) {
|
|
104
|
+
await emitBillingEvent(ev.id, ev.payload, { persistent: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
transactionId: created.id,
|
|
109
|
+
balance: outcome.balanceAfter,
|
|
110
|
+
balanceStatus: computeBalanceStatus(outcome.balanceAfter, threshold),
|
|
111
|
+
deduplicated: outcome.deduplicated
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
registerCommand(adjustTransactionCommand);
|
|
116
|
+
export {
|
|
117
|
+
adjustTransactionCommand
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=transactions.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/commands/transactions.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { BillingAccount, BillingAccountBalance } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport {\n billingTransactionAdjustSchema,\n type BillingTransactionAdjustInput,\n type BalanceStatus,\n} from '../data/validators'\nimport { applyBalanceMovement } from '../lib/balanceLedger'\nimport { buildBalanceCrossingEvents } from '../lib/balanceEvents'\nimport { computeBalanceStatus, resolveLowBalanceThreshold } from '../lib/balanceStatus'\nimport { formatMoney } from '../lib/money'\nimport {\n getLowBalanceThresholdDefault,\n getNearLimitBufferDefault,\n} from '../lib/prepaidConfig'\nimport { emitBillingEvent, type BillingEventId } from '../events'\n\n/**\n * Manual balance adjustment (SPEC-002 P5).\n *\n * Operator-driven credit/debit (`type='adjustment'`) audited via `user_id`.\n * Non-fiscal by default \u2014 an adjustment moves already-taxed balance, so no\n * receipt is issued (Open Question #3; can be revisited per jurisdiction).\n * Goes through the same atomic ledger primitive as every other movement, so\n * the reconciliation invariant still holds.\n */\n\nconst transactionIndexer = { entityType: billingEntityIds.transaction } as const\nconst transactionEvents = { module: 'billing', entity: 'transaction', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\nexport type AdjustResult = {\n transactionId: string\n balance: string\n balanceStatus: BalanceStatus\n deduplicated: boolean\n}\n\nconst adjustTransactionCommand: CommandHandler<BillingTransactionAdjustInput, AdjustResult> = {\n id: 'billing.transactions.adjust',\n\n async execute(rawInput, ctx) {\n const parsed = billingTransactionAdjustSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await em.findOne(BillingAccount, {\n id: parsed.billAccountId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n if (account.billingMode !== 'prepaid') {\n throw new CrudHttpError(409, {\n error: 'Balance adjustments are only available on prepaid accounts',\n code: 'billing.account.not_prepaid',\n })\n }\n\n const balanceRow = await em.findOne(BillingAccountBalance, {\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n })\n const threshold = resolveLowBalanceThreshold(\n balanceRow?.lowBalanceThreshold ?? null,\n await getLowBalanceThresholdDefault(em),\n )\n const nearLimitBuffer = await getNearLimitBufferDefault(em)\n const userId = (ctx.auth?.sub as string | undefined) ?? null\n\n const outcome = await em.transactional(async (tem) =>\n applyBalanceMovement(tem as EntityManager, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: account.id,\n currencyCode: account.currencyCode,\n type: 'adjustment',\n amount: formatMoney(parsed.amount),\n description: parsed.description,\n sourceRef: parsed.sourceRef ?? null,\n userId,\n metadata: { manual: true },\n }),\n )\n\n // Index the transaction + emit threshold-crossing events after commit.\n const created = outcome.transaction\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity: created,\n identifiers: {\n id: created.id,\n tenantId: created.tenantId,\n organizationId: created.organizationId,\n },\n indexer: transactionIndexer,\n events: transactionEvents,\n })\n\n if (!outcome.deduplicated) {\n const events = buildBalanceCrossingEvents({\n scope: {\n billAccountId: account.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n currencyCode: account.currencyCode,\n },\n balanceBefore: outcome.balanceBefore,\n balanceAfter: outcome.balanceAfter,\n threshold,\n creditLimit: account.creditLimit ?? '0',\n nearLimitBuffer,\n })\n for (const ev of events) {\n await emitBillingEvent(ev.id as BillingEventId, ev.payload, { persistent: true })\n }\n }\n\n return {\n transactionId: created.id,\n balance: outcome.balanceAfter,\n balanceStatus: computeBalanceStatus(outcome.balanceAfter, threshold),\n deduplicated: outcome.deduplicated,\n }\n },\n}\n\nregisterCommand(adjustTransactionCommand)\n\nexport { adjustTransactionCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UniqueConstraintViolationException } from "@mikro-orm/core";
|
|
1
2
|
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
3
|
import {
|
|
3
4
|
ensureOrganizationScope,
|
|
@@ -11,6 +12,10 @@ import { findBySourceRef } from "../lib/idempotency.js";
|
|
|
11
12
|
import {
|
|
12
13
|
billingUsageCreateSchema
|
|
13
14
|
} from "../data/validators.js";
|
|
15
|
+
import {
|
|
16
|
+
consumePrepaidUsage
|
|
17
|
+
} from "../lib/prepaidConsume.js";
|
|
18
|
+
import { emitBillingEvent } from "../events.js";
|
|
14
19
|
const usageIndexer = { entityType: billingEntityIds.usage };
|
|
15
20
|
const usageEvents = { module: "billing", entity: "usage", persistent: true };
|
|
16
21
|
function getEm(ctx) {
|
|
@@ -35,6 +40,9 @@ const createUsageCommand = {
|
|
|
35
40
|
if (!account) {
|
|
36
41
|
throw new CrudHttpError(404, { error: "Billing account not found" });
|
|
37
42
|
}
|
|
43
|
+
if (account.billingMode === "prepaid") {
|
|
44
|
+
return executePrepaidConsume(ctx, em, account, parsed);
|
|
45
|
+
}
|
|
38
46
|
if (parsed.sourceRef) {
|
|
39
47
|
const existing = await findBySourceRef(em, BillingAccountUsage, {
|
|
40
48
|
tenantId: parsed.tenantId,
|
|
@@ -81,6 +89,70 @@ const createUsageCommand = {
|
|
|
81
89
|
return { id: entity.id, deduplicated: false };
|
|
82
90
|
}
|
|
83
91
|
};
|
|
92
|
+
async function executePrepaidConsume(ctx, em, account, parsed) {
|
|
93
|
+
const input = {
|
|
94
|
+
organizationId: parsed.organizationId,
|
|
95
|
+
tenantId: parsed.tenantId,
|
|
96
|
+
billAccountId: parsed.billAccountId,
|
|
97
|
+
uomCode: parsed.uomCode,
|
|
98
|
+
quantity: parsed.quantity,
|
|
99
|
+
periodStart: parsed.periodStart,
|
|
100
|
+
periodEnd: parsed.periodEnd,
|
|
101
|
+
lineDescription: parsed.lineDescription ?? null,
|
|
102
|
+
sourceRef: parsed.sourceRef ?? null
|
|
103
|
+
};
|
|
104
|
+
let outcome;
|
|
105
|
+
try {
|
|
106
|
+
outcome = await em.transactional(
|
|
107
|
+
async (tem) => consumePrepaidUsage(tem, account, input)
|
|
108
|
+
);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (parsed.sourceRef && err instanceof UniqueConstraintViolationException) {
|
|
111
|
+
outcome = await em.fork().transactional(
|
|
112
|
+
async (tem) => consumePrepaidUsage(tem, account, input)
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!outcome.result.deduplicated) {
|
|
119
|
+
const usage = await em.findOne(BillingAccountUsage, {
|
|
120
|
+
id: outcome.result.id,
|
|
121
|
+
tenantId: parsed.tenantId,
|
|
122
|
+
organizationId: parsed.organizationId
|
|
123
|
+
});
|
|
124
|
+
if (usage) {
|
|
125
|
+
await emitCrudSideEffects({
|
|
126
|
+
dataEngine: getDataEngine(ctx),
|
|
127
|
+
action: "created",
|
|
128
|
+
entity: usage,
|
|
129
|
+
identifiers: {
|
|
130
|
+
id: usage.id,
|
|
131
|
+
tenantId: usage.tenantId,
|
|
132
|
+
organizationId: usage.organizationId
|
|
133
|
+
},
|
|
134
|
+
indexer: usageIndexer,
|
|
135
|
+
events: usageEvents
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const ev of outcome.events) {
|
|
140
|
+
const persistent = ev.id !== "billing.usage.rated";
|
|
141
|
+
await emitBillingEvent(ev.id, ev.payload, { persistent });
|
|
142
|
+
}
|
|
143
|
+
const r = outcome.result;
|
|
144
|
+
return {
|
|
145
|
+
id: r.id,
|
|
146
|
+
deduplicated: r.deduplicated,
|
|
147
|
+
ratedAmount: r.ratedAmount,
|
|
148
|
+
currencyCode: r.currencyCode,
|
|
149
|
+
balance: r.balance,
|
|
150
|
+
balanceStatus: r.balanceStatus,
|
|
151
|
+
creditStatus: r.creditStatus,
|
|
152
|
+
tierBreakdown: r.tierBreakdown,
|
|
153
|
+
warning: r.warning
|
|
154
|
+
};
|
|
155
|
+
}
|
|
84
156
|
registerCommand(createUsageCommand);
|
|
85
157
|
export {
|
|
86
158
|
createUsageCommand
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/commands/usage.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { BillingAccount, BillingAccountUsage } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport { findBySourceRef } from '../lib/idempotency'\nimport {\n billingUsageCreateSchema,\n type BillingUsageCreateInput,\n} from '../data/validators'\n\n/**\n * Usage ingest \u2014 append-only.\n *\n * Usage records are immutable once persisted. The Bill Run engine\n * (Phase 2) writes `rated_in_bill_run_id` on them but the data itself\n * never changes. There is therefore no update or delete command \u2014 if\n * an external system mis-aggregates and needs to \"correct\" a record,\n * they POST a compensating record (negative quantity is not allowed\n * by the validator; the connector posts a `one_time` Billing Item\n * credit instead, which is the spec's discount/refund model).\n *\n * Create is idempotent via `source_ref` \u2014 same shape as items.\n */\n\nconst usageIndexer = { entityType: billingEntityIds.usage } as const\nconst usageEvents = { module: 'billing', entity: 'usage', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport { BillingAccount, BillingAccountUsage } from '../data/entities'\nimport { billingEntityIds } from '../data/entityIds'\nimport { findBySourceRef } from '../lib/idempotency'\nimport {\n billingUsageCreateSchema,\n type BillingUsageCreateInput,\n} from '../data/validators'\nimport {\n consumePrepaidUsage,\n type PrepaidConsumeResult,\n} from '../lib/prepaidConsume'\nimport { emitBillingEvent, type BillingEventId } from '../events'\n\n/**\n * Usage ingest \u2014 append-only.\n *\n * Usage records are immutable once persisted. The Bill Run engine\n * (Phase 2) writes `rated_in_bill_run_id` on them but the data itself\n * never changes. There is therefore no update or delete command \u2014 if\n * an external system mis-aggregates and needs to \"correct\" a record,\n * they POST a compensating record (negative quantity is not allowed\n * by the validator; the connector posts a `one_time` Billing Item\n * credit instead, which is the spec's discount/refund model).\n *\n * Create is idempotent via `source_ref` \u2014 same shape as items.\n */\n\nconst usageIndexer = { entityType: billingEntityIds.usage } as const\nconst usageEvents = { module: 'billing', entity: 'usage', persistent: true } as const\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nfunction getDataEngine(ctx: CommandRuntimeContext): DataEngine {\n return ctx.container.resolve('dataEngine') as DataEngine\n}\n\nexport type CreateUsageResult = {\n id: string\n deduplicated: boolean\n // Prepaid-only extras (SPEC-002) \u2014 present only for prepaid accounts.\n ratedAmount?: string\n currencyCode?: string\n balance?: string\n balanceStatus?: PrepaidConsumeResult['balanceStatus']\n creditStatus?: PrepaidConsumeResult['creditStatus']\n tierBreakdown?: PrepaidConsumeResult['tierBreakdown']\n warning?: string | null\n}\n\nconst createUsageCommand: CommandHandler<BillingUsageCreateInput, CreateUsageResult> = {\n id: 'billing.usage.create',\n\n async execute(rawInput, ctx) {\n const parsed = billingUsageCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const account = await em.findOne(BillingAccount, {\n id: parsed.billAccountId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!account) {\n throw new CrudHttpError(404, { error: 'Billing account not found' })\n }\n\n // \u2500\u2500\u2500 Prepaid: rate + atomic debit + balance, all in one tx \u2500\u2500\u2500\u2500\n if (account.billingMode === 'prepaid') {\n return executePrepaidConsume(ctx, em, account, parsed)\n }\n\n if (parsed.sourceRef) {\n const existing = await findBySourceRef(em, BillingAccountUsage, {\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n sourceRef: parsed.sourceRef,\n })\n if (existing) {\n return { id: existing.id, deduplicated: true }\n }\n }\n\n const now = new Date()\n const entity = em.create(BillingAccountUsage, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n uomCode: parsed.uomCode,\n // numeric(18,4) \u2014 MikroORM stores numeric columns as string in app\n // code. JSON arrives as number; coerce here so the persisted shape\n // matches what later reads expect.\n quantity: parsed.quantity.toFixed(4),\n periodStart: parsed.periodStart,\n periodEnd: parsed.periodEnd,\n lineDescription: parsed.lineDescription ?? null,\n sourceRef: parsed.sourceRef ?? null,\n ratedInBillRunId: null,\n isActive: true,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(entity)\n await em.flush()\n\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity,\n identifiers: {\n id: entity.id,\n tenantId: entity.tenantId,\n organizationId: entity.organizationId,\n },\n indexer: usageIndexer,\n events: usageEvents,\n })\n\n return { id: entity.id, deduplicated: false }\n },\n}\n\n/**\n * Prepaid consume: rate + atomic debit + balance + transaction in one DB\n * transaction, then index + emit domain events AFTER the commit.\n */\nasync function executePrepaidConsume(\n ctx: CommandRuntimeContext,\n em: EntityManager,\n account: BillingAccount,\n parsed: BillingUsageCreateInput,\n): Promise<CreateUsageResult> {\n const input = {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n billAccountId: parsed.billAccountId,\n uomCode: parsed.uomCode,\n quantity: parsed.quantity,\n periodStart: parsed.periodStart,\n periodEnd: parsed.periodEnd,\n lineDescription: parsed.lineDescription ?? null,\n sourceRef: parsed.sourceRef ?? null,\n }\n\n let outcome\n try {\n outcome = await em.transactional(async (tem) =>\n consumePrepaidUsage(tem as EntityManager, account, input),\n )\n } catch (err) {\n // Concurrency: a simultaneous upload with the same sourceRef won the\n // unique-constraint race and committed first, so our tx rolled back (no\n // double-debit). Retry once on a fresh em \u2014 the dedup pre-check now finds\n // the winner's record and returns its balance_after (idempotent 200).\n if (parsed.sourceRef && err instanceof UniqueConstraintViolationException) {\n outcome = await em.fork().transactional(async (tem) =>\n consumePrepaidUsage(tem as EntityManager, account, input),\n )\n } else {\n throw err\n }\n }\n\n // Index the new usage record (parity with the postpaid path) \u2014 only when a\n // fresh record was actually written (a dedup hit re-reads the prior one).\n if (!outcome.result.deduplicated) {\n const usage = await em.findOne(BillingAccountUsage, {\n id: outcome.result.id,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (usage) {\n await emitCrudSideEffects({\n dataEngine: getDataEngine(ctx),\n action: 'created',\n entity: usage,\n identifiers: {\n id: usage.id,\n tenantId: usage.tenantId,\n organizationId: usage.organizationId,\n },\n indexer: usageIndexer,\n events: usageEvents,\n })\n }\n }\n\n // Domain events fire AFTER the DB commit (consistent with the post command).\n // `billing.usage.rated` fires on EVERY upload \u2014 emit it ephemerally (a live\n // signal, excluded from triggers) so the high-throughput consume path does\n // not write a queue row per call. The meaningful crossing events\n // (low/exhausted/credit) stay persistent so async consumers never miss them.\n for (const ev of outcome.events) {\n const persistent = ev.id !== 'billing.usage.rated'\n await emitBillingEvent(ev.id as BillingEventId, ev.payload, { persistent })\n }\n\n const r = outcome.result\n return {\n id: r.id,\n deduplicated: r.deduplicated,\n ratedAmount: r.ratedAmount,\n currencyCode: r.currencyCode,\n balance: r.balance,\n balanceStatus: r.balanceStatus,\n creditStatus: r.creditStatus,\n tierBreakdown: r.tierBreakdown,\n warning: r.warning,\n }\n}\n\nregisterCommand(createUsageCommand)\n\nexport { createUsageCommand }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -32,7 +32,10 @@ const EMPTY_VALUES = {
|
|
|
32
32
|
country: "PL"
|
|
33
33
|
},
|
|
34
34
|
nextBillDate: "",
|
|
35
|
-
isActive: true
|
|
35
|
+
isActive: true,
|
|
36
|
+
billingMode: "postpaid",
|
|
37
|
+
creditLimit: "0",
|
|
38
|
+
lowBalanceThreshold: ""
|
|
36
39
|
};
|
|
37
40
|
function todayIso() {
|
|
38
41
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -380,6 +383,55 @@ function AccountForm({
|
|
|
380
383
|
)
|
|
381
384
|
] })
|
|
382
385
|
] }),
|
|
386
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold mt-6 mb-2", children: t("billing.accounts.form.mode.title", "Billing mode & credit") }),
|
|
387
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
388
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
389
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.accounts.columns.mode", "Mode") }),
|
|
390
|
+
/* @__PURE__ */ jsxs(
|
|
391
|
+
Select,
|
|
392
|
+
{
|
|
393
|
+
value: values.billingMode,
|
|
394
|
+
disabled: isEdit,
|
|
395
|
+
onValueChange: (next) => set("billingMode", next),
|
|
396
|
+
children: [
|
|
397
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
398
|
+
/* @__PURE__ */ jsxs(SelectContent, { children: [
|
|
399
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "postpaid", children: t("billing.mode.postpaid", "Postpaid") }),
|
|
400
|
+
/* @__PURE__ */ jsx(SelectItem, { value: "prepaid", children: t("billing.mode.prepaid", "Prepaid") })
|
|
401
|
+
] })
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
),
|
|
405
|
+
isEdit ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("billing.accounts.form.mode.locked", "Immutable \u2014 frozen at account create time.") }) : null
|
|
406
|
+
] }),
|
|
407
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
408
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.accounts.columns.credit_limit", "Credit limit") }),
|
|
409
|
+
/* @__PURE__ */ jsx(
|
|
410
|
+
Input,
|
|
411
|
+
{
|
|
412
|
+
type: "number",
|
|
413
|
+
min: "0",
|
|
414
|
+
step: "0.01",
|
|
415
|
+
value: values.creditLimit,
|
|
416
|
+
onChange: (event) => set("creditLimit", event.currentTarget.value)
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
] }),
|
|
420
|
+
values.billingMode === "prepaid" ? /* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
421
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.prepaid.panel.threshold", "Low threshold") }),
|
|
422
|
+
/* @__PURE__ */ jsx(
|
|
423
|
+
Input,
|
|
424
|
+
{
|
|
425
|
+
type: "number",
|
|
426
|
+
min: "0",
|
|
427
|
+
step: "0.01",
|
|
428
|
+
value: values.lowBalanceThreshold,
|
|
429
|
+
placeholder: t("billing.accounts.form.threshold.hint", "Blank = tenant default"),
|
|
430
|
+
onChange: (event) => set("lowBalanceThreshold", event.currentTarget.value)
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
] }) : null
|
|
434
|
+
] }),
|
|
383
435
|
isEdit ? /* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center justify-between gap-3", children: [
|
|
384
436
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
385
437
|
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: t("billing.accounts.form.active", "Active") }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/billing/components/AccountForm.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\n/**\n * Shared form for Billing Account create + edit.\n *\n * Hand-rolled (not CrudForm) on purpose \u2014 the form's `invoiceAddress`\n * jsonb sub-object would need a custom-field renderer in the\n * CrudForm DSL, which is heavier than just rendering five inputs\n * inline. The trade-off is documented in `xd-249-billing-spec.md`\n * Phase 4d notes.\n *\n * UX per the OM UI AGENTS.md:\n * - `Cmd/Ctrl + Enter` submits.\n * - Buttons use the `Button` / `Switch` primitives, never raw HTML.\n * - Footer hint row uses `Kbd` / `KbdShortcut`.\n *\n * `mode='create'` shows the always-required fields. `mode='edit'`\n * disables `customerId` and `currencyCode` per the spec\n * (\"immutable per spec \u2014 create a new account if the customer FK\n * changes\" \u2014 see `billingAccountUpdateSchema`).\n */\n\nexport type AccountFormValues = {\n customerId: string\n name: string\n currencyCode: string\n billCycle: 'weekly' | 'monthly' | 'quarterly' | 'annually'\n billCycleAnchor: string\n invoiceEmail: string\n invoiceLanguage: string\n taxId: string\n invoiceAddress: {\n line1: string\n line2: string\n city: string\n postal_code: string\n country: string\n }\n nextBillDate: string\n isActive: boolean\n}\n\nexport type AccountFormProps = {\n mode: 'create' | 'edit'\n initial?: Partial<AccountFormValues>\n submitting?: boolean\n onSubmit: (values: AccountFormValues) => Promise<void> | void\n onCancel: () => void\n}\n\nconst EMPTY_VALUES: AccountFormValues = {\n customerId: '',\n name: '',\n currencyCode: 'EUR',\n billCycle: 'monthly',\n billCycleAnchor: '1',\n invoiceEmail: '',\n invoiceLanguage: 'en',\n taxId: '',\n invoiceAddress: {\n line1: '',\n line2: '',\n city: '',\n postal_code: '',\n country: 'PL',\n },\n nextBillDate: '',\n isActive: true,\n}\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\nfunction mergeInitial(initial: Partial<AccountFormValues> | undefined): AccountFormValues {\n if (!initial) return { ...EMPTY_VALUES, nextBillDate: todayIso() }\n return {\n ...EMPTY_VALUES,\n nextBillDate: EMPTY_VALUES.nextBillDate || todayIso(),\n ...initial,\n invoiceAddress: {\n ...EMPTY_VALUES.invoiceAddress,\n ...(initial.invoiceAddress ?? {}),\n },\n }\n}\n\nexport function AccountForm({\n mode,\n initial,\n submitting = false,\n onSubmit,\n onCancel,\n}: AccountFormProps) {\n const t = useT()\n const [values, setValues] = React.useState<AccountFormValues>(() => mergeInitial(initial))\n const [error, setError] = React.useState<string | null>(null)\n const isEdit = mode === 'edit'\n\n // Label cache for the customer combobox \u2014 populated as the user searches,\n // so the picker can render \"Acme Telecom\" after selection instead of the\n // raw UUID we send to the API.\n const customerLabelCacheRef = React.useRef<Map<string, ComboboxOption>>(new Map())\n\n const loadCustomerOptions = React.useCallback(\n async (query?: string): Promise<ComboboxOption[]> => {\n const params = new URLSearchParams({ page: '1', pageSize: '20' })\n const q = (query ?? '').trim()\n if (q) params.set('search', q)\n const call = await apiCall<unknown>(`/api/customers/companies?${params.toString()}`)\n if (!call.ok || call.result == null) return []\n const raw = call.result as { items?: unknown } | unknown[]\n const items: Array<Record<string, unknown>> = Array.isArray(raw)\n ? (raw as Array<Record<string, unknown>>)\n : Array.isArray((raw as { items?: unknown }).items)\n ? ((raw as { items: unknown[] }).items as Array<Record<string, unknown>>)\n : []\n const options: ComboboxOption[] = []\n for (const c of items) {\n const id = typeof c.id === 'string' ? c.id : c.id != null ? String(c.id) : ''\n if (!id) continue\n const label = String(c.display_name ?? c.displayName ?? c.name ?? id)\n const emailRaw = c.primary_email ?? c.email\n const description = typeof emailRaw === 'string' && emailRaw ? emailRaw : null\n const opt: ComboboxOption = { value: id, label, description }\n customerLabelCacheRef.current.set(id, opt)\n options.push(opt)\n }\n return options\n },\n [],\n )\n\n const resolveCustomerLabel = React.useCallback(\n (id: string) => customerLabelCacheRef.current.get(id)?.label ?? id,\n [],\n )\n const resolveCustomerDescription = React.useCallback(\n (id: string) => customerLabelCacheRef.current.get(id)?.description ?? null,\n [],\n )\n\n // Reset when the parent swaps `initial` (e.g. after loading the edit row).\n const initialKey = React.useMemo(\n () => JSON.stringify(initial ?? null),\n [initial],\n )\n React.useEffect(() => {\n setValues(mergeInitial(initial))\n }, [initialKey, initial])\n\n const set = React.useCallback(\n <K extends keyof AccountFormValues>(key: K, value: AccountFormValues[K]) => {\n setValues((prev) => ({ ...prev, [key]: value }))\n },\n [],\n )\n\n const setAddress = React.useCallback(\n <K extends keyof AccountFormValues['invoiceAddress']>(\n key: K,\n value: AccountFormValues['invoiceAddress'][K],\n ) => {\n setValues((prev) => ({\n ...prev,\n invoiceAddress: { ...prev.invoiceAddress, [key]: value },\n }))\n },\n [],\n )\n\n const handleSubmit = React.useCallback(async () => {\n if (!isEdit && !values.customerId.trim()) {\n setError(t('billing.accounts.form.error.customerId_required', 'Please select a customer'))\n return\n }\n if (!values.name.trim()) {\n setError(t('billing.accounts.form.error.name_required', 'Name is required'))\n return\n }\n if (!isEdit && values.currencyCode.trim().length !== 3) {\n setError(t('billing.accounts.form.error.currency_invalid', 'Currency must be a 3-letter ISO code'))\n return\n }\n if (!values.invoiceEmail.includes('@')) {\n setError(t('billing.accounts.form.error.email_invalid', 'Invoice email looks invalid'))\n return\n }\n if (\n !values.invoiceAddress.line1.trim() ||\n !values.invoiceAddress.city.trim() ||\n !values.invoiceAddress.postal_code.trim()\n ) {\n setError(\n t(\n 'billing.accounts.form.error.address_required',\n 'Invoice address (line 1 + city + postal code) is required',\n ),\n )\n return\n }\n const country = values.invoiceAddress.country.trim()\n if (country.length < 2 || country.length > 3) {\n setError(\n t(\n 'billing.accounts.form.error.country_invalid',\n 'Country must be a 2- or 3-letter ISO 3166 code',\n ),\n )\n return\n }\n if (!values.nextBillDate) {\n setError(t('billing.accounts.form.error.next_bill_date_required', 'Next bill date is required'))\n return\n }\n setError(null)\n await onSubmit(values)\n }, [isEdit, onSubmit, t, values])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n return (\n <div className=\"rounded-lg border border-border bg-card p-6\" onKeyDown={handleKeyDown}>\n <div className=\"grid grid-cols-2 gap-4\">\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {isEdit\n ? t('billing.accounts.form.customer_id', 'Customer ID')\n : t('billing.accounts.form.customer', 'Customer')}\n {!isEdit ? <span className=\"text-destructive\"> *</span> : null}\n </span>\n {isEdit ? (\n <Input value={values.customerId} disabled />\n ) : (\n <ComboboxInput\n value={values.customerId}\n onChange={(next) => {\n setValues((prev) => {\n const customerLabel = customerLabelCacheRef.current.get(next)?.label ?? ''\n // Auto-fill Name with the customer's display name on the first\n // pick. We leave it alone if the operator has already typed\n // something \u2014 one customer can have multiple billing accounts\n // (e.g. \"Acme \u2014 production\" / \"Acme \u2014 staging\").\n const shouldFillName = customerLabel && !prev.name.trim()\n return {\n ...prev,\n customerId: next,\n name: shouldFillName ? customerLabel : prev.name,\n }\n })\n }}\n loadSuggestions={loadCustomerOptions}\n resolveLabel={resolveCustomerLabel}\n resolveDescription={resolveCustomerDescription}\n placeholder={t(\n 'billing.accounts.form.customer.placeholder',\n 'Search customers by name or email\u2026',\n )}\n allowCustomValues={false}\n />\n )}\n {isEdit ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('billing.accounts.form.customer_id.locked', 'Immutable \u2014 create a new account to re-link the customer.')}\n </span>\n ) : null}\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.name', 'Name')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.name}\n placeholder={\n !isEdit\n ? t(\n 'billing.accounts.form.name.placeholder',\n 'Auto-filled from customer \u2014 override e.g. \"Acme \u2014 production\"',\n )\n : undefined\n }\n onChange={(event) => set('name', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.currency', 'Currency (ISO 4217)')}\n {!isEdit ? <span className=\"text-destructive\"> *</span> : null}\n </span>\n <Input\n value={values.currencyCode}\n disabled={isEdit}\n maxLength={3}\n onChange={(event) =>\n set('currencyCode', event.currentTarget.value.toUpperCase())\n }\n />\n {isEdit ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('billing.accounts.form.currency.locked', 'Immutable \u2014 frozen at account create time.')}\n </span>\n ) : null}\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.bill_cycle', 'Bill cycle')}\n </span>\n <Select\n value={values.billCycle}\n onValueChange={(next) =>\n set('billCycle', next as AccountFormValues['billCycle'])\n }\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"weekly\">{t('billing.cycle.weekly', 'Weekly')}</SelectItem>\n <SelectItem value=\"monthly\">{t('billing.cycle.monthly', 'Monthly')}</SelectItem>\n <SelectItem value=\"quarterly\">{t('billing.cycle.quarterly', 'Quarterly')}</SelectItem>\n <SelectItem value=\"annually\">{t('billing.cycle.annually', 'Annually')}</SelectItem>\n </SelectContent>\n </Select>\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.cycle_anchor', 'Cycle anchor')}\n </span>\n <Input\n value={values.billCycleAnchor}\n placeholder={\n values.billCycle === 'weekly'\n ? t('billing.accounts.form.cycle_anchor.weekly_hint', 'mon | tue | \u2026 | sun')\n : t('billing.accounts.form.cycle_anchor.month_hint', '1 \u2014 28')\n }\n onChange={(event) => set('billCycleAnchor', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.next_bill_date', 'Next bill date')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n type=\"date\"\n value={values.nextBillDate}\n onChange={(event) => set('nextBillDate', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.invoice_email', 'Invoice email')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n type=\"email\"\n value={values.invoiceEmail}\n onChange={(event) => set('invoiceEmail', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.invoice_language', 'Invoice language (ISO 639-1)')}\n </span>\n <Input\n value={values.invoiceLanguage}\n maxLength={5}\n onChange={(event) =>\n set('invoiceLanguage', event.currentTarget.value.toLowerCase())\n }\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm col-span-2\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.tax_id', 'Tax ID (NIP / VATIN, optional)')}\n </span>\n <Input\n value={values.taxId}\n onChange={(event) => set('taxId', event.currentTarget.value)}\n />\n </label>\n </div>\n\n <h3 className=\"text-sm font-semibold mt-6 mb-2\">\n {t('billing.accounts.form.address.title', 'Invoice address')}\n </h3>\n <div className=\"grid grid-cols-2 gap-4\">\n <label className=\"flex flex-col gap-1 text-sm col-span-2\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.line1', 'Line 1')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.line1}\n onChange={(event) => setAddress('line1', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm col-span-2\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.line2', 'Line 2')}\n </span>\n <Input\n value={values.invoiceAddress.line2}\n onChange={(event) => setAddress('line2', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.city', 'City')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.city}\n onChange={(event) => setAddress('city', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.postal_code', 'Postal code')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.postal_code}\n onChange={(event) => setAddress('postal_code', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.country', 'Country (ISO 3166)')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.country}\n maxLength={3}\n onChange={(event) =>\n setAddress('country', event.currentTarget.value.toUpperCase())\n }\n />\n </label>\n </div>\n\n {isEdit ? (\n <div className=\"mt-6 flex items-center justify-between gap-3\">\n <div>\n <div className=\"text-sm font-medium\">\n {t('billing.accounts.form.active', 'Active')}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.accounts.form.active.help',\n 'Inactive accounts stay billable until soft-deleted \u2014 toggle for visibility filtering only.',\n )}\n </div>\n </div>\n <Switch\n checked={values.isActive}\n onCheckedChange={(next) => set('isActive', next)}\n />\n </div>\n ) : null}\n\n {error ? <div className=\"mt-4 text-sm text-destructive\">{error}</div> : null}\n\n <div className=\"mt-6 flex items-center justify-between gap-3\">\n <span className=\"text-xs text-muted-foreground\">\n <KbdShortcut keys={['Ctrl/\u2318', 'Enter']} />{' '}\n {t('billing.accounts.form.submit_hint', 'to save,')} <Kbd>Esc</Kbd>{' '}\n {t('billing.accounts.form.cancel_hint', 'to discard')}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" onClick={onCancel}>\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleSubmit()} disabled={submitting}>\n {submitting\n ? t('billing.accounts.form.saving', 'Saving\u2026')\n : isEdit\n ? t('billing.accounts.form.save', 'Save changes')\n : t('billing.accounts.form.create', 'Create account')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AAwPU,SAIa,KAJb;AAvPV,YAAY,WAAW;AACvB,SAAS,cAAc;AACvB,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AACvB,SAAS,KAAK,mBAAmB;AACjC,SAAS,qBAA0C;AACnD,SAAS,eAAe;AACxB,SAAS,YAAY;AAkDrB,MAAM,eAAkC;AAAA,EACtC,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,cAAc;AAAA,EACd,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,OAAO;AAAA,EACP,gBAAgB;AAAA,IACd,OAAO;AAAA,IACP,OAAO;AAAA,IACP,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,EACX;AAAA,EACA,cAAc;AAAA,EACd,UAAU;AACZ;AAEA,SAAS,WAAmB;AAC1B,UAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC7C;AAEA,SAAS,aAAa,SAAoE;AACxF,MAAI,CAAC,QAAS,QAAO,EAAE,GAAG,cAAc,cAAc,SAAS,EAAE;AACjE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,cAAc,aAAa,gBAAgB,SAAS;AAAA,IACpD,GAAG;AAAA,IACH,gBAAgB;AAAA,MACd,GAAG,aAAa;AAAA,MAChB,GAAI,QAAQ,kBAAkB,CAAC;AAAA,IACjC;AAAA,EACF;AACF;AAEO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA4B,MAAM,aAAa,OAAO,CAAC;AACzF,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,SAAS,SAAS;AAKxB,QAAM,wBAAwB,MAAM,OAAoC,oBAAI,IAAI,CAAC;AAEjF,QAAM,sBAAsB,MAAM;AAAA,IAChC,OAAO,UAA8C;AACnD,YAAM,SAAS,IAAI,gBAAgB,EAAE,MAAM,KAAK,UAAU,KAAK,CAAC;AAChE,YAAM,KAAK,SAAS,IAAI,KAAK;AAC7B,UAAI,EAAG,QAAO,IAAI,UAAU,CAAC;AAC7B,YAAM,OAAO,MAAM,QAAiB,4BAA4B,OAAO,SAAS,CAAC,EAAE;AACnF,UAAI,CAAC,KAAK,MAAM,KAAK,UAAU,KAAM,QAAO,CAAC;AAC7C,YAAM,MAAM,KAAK;AACjB,YAAM,QAAwC,MAAM,QAAQ,GAAG,IAC1D,MACD,MAAM,QAAS,IAA4B,KAAK,IAC5C,IAA6B,QAC/B,CAAC;AACP,YAAM,UAA4B,CAAC;AACnC,iBAAW,KAAK,OAAO;AACrB,cAAM,KAAK,OAAO,EAAE,OAAO,WAAW,EAAE,KAAK,EAAE,MAAM,OAAO,OAAO,EAAE,EAAE,IAAI;AAC3E,YAAI,CAAC,GAAI;AACT,cAAM,QAAQ,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,QAAQ,EAAE;AACpE,cAAM,WAAW,EAAE,iBAAiB,EAAE;AACtC,cAAM,cAAc,OAAO,aAAa,YAAY,WAAW,WAAW;AAC1E,cAAM,MAAsB,EAAE,OAAO,IAAI,OAAO,YAAY;AAC5D,8BAAsB,QAAQ,IAAI,IAAI,GAAG;AACzC,gBAAQ,KAAK,GAAG;AAAA,MAClB;AACA,aAAO;AAAA,IACT;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,uBAAuB,MAAM;AAAA,IACjC,CAAC,OAAe,sBAAsB,QAAQ,IAAI,EAAE,GAAG,SAAS;AAAA,IAChE,CAAC;AAAA,EACH;AACA,QAAM,6BAA6B,MAAM;AAAA,IACvC,CAAC,OAAe,sBAAsB,QAAQ,IAAI,EAAE,GAAG,eAAe;AAAA,IACtE,CAAC;AAAA,EACH;AAGA,QAAM,aAAa,MAAM;AAAA,IACvB,MAAM,KAAK,UAAU,WAAW,IAAI;AAAA,IACpC,CAAC,OAAO;AAAA,EACV;AACA,QAAM,UAAU,MAAM;AACpB,cAAU,aAAa,OAAO,CAAC;AAAA,EACjC,GAAG,CAAC,YAAY,OAAO,CAAC;AAExB,QAAM,MAAM,MAAM;AAAA,IAChB,CAAoC,KAAQ,UAAgC;AAC1E,gBAAU,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,GAAG,GAAG,MAAM,EAAE;AAAA,IACjD;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,aAAa,MAAM;AAAA,IACvB,CACE,KACA,UACG;AACH,gBAAU,CAAC,UAAU;AAAA,QACnB,GAAG;AAAA,QACH,gBAAgB,EAAE,GAAG,KAAK,gBAAgB,CAAC,GAAG,GAAG,MAAM;AAAA,MACzD,EAAE;AAAA,IACJ;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,eAAe,MAAM,YAAY,YAAY;AACjD,QAAI,CAAC,UAAU,CAAC,OAAO,WAAW,KAAK,GAAG;AACxC,eAAS,EAAE,mDAAmD,0BAA0B,CAAC;AACzF;AAAA,IACF;AACA,QAAI,CAAC,OAAO,KAAK,KAAK,GAAG;AACvB,eAAS,EAAE,6CAA6C,kBAAkB,CAAC;AAC3E;AAAA,IACF;AACA,QAAI,CAAC,UAAU,OAAO,aAAa,KAAK,EAAE,WAAW,GAAG;AACtD,eAAS,EAAE,gDAAgD,sCAAsC,CAAC;AAClG;AAAA,IACF;AACA,QAAI,CAAC,OAAO,aAAa,SAAS,GAAG,GAAG;AACtC,eAAS,EAAE,6CAA6C,6BAA6B,CAAC;AACtF;AAAA,IACF;AACA,QACE,CAAC,OAAO,eAAe,MAAM,KAAK,KAClC,CAAC,OAAO,eAAe,KAAK,KAAK,KACjC,CAAC,OAAO,eAAe,YAAY,KAAK,GACxC;AACA;AAAA,QACE;AAAA,UACE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AACA,UAAM,UAAU,OAAO,eAAe,QAAQ,KAAK;AACnD,QAAI,QAAQ,SAAS,KAAK,QAAQ,SAAS,GAAG;AAC5C;AAAA,QACE;AAAA,UACE;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AACA,QAAI,CAAC,OAAO,cAAc;AACxB,eAAS,EAAE,uDAAuD,4BAA4B,CAAC;AAC/F;AAAA,IACF;AACA,aAAS,IAAI;AACb,UAAM,SAAS,MAAM;AAAA,EACvB,GAAG,CAAC,QAAQ,UAAU,GAAG,MAAM,CAAC;AAEhC,QAAM,gBAAgB,MAAM;AAAA,IAC1B,CAAC,UAA+C;AAC9C,WAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,cAAM,eAAe;AACrB,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,YAAY;AAAA,EACf;AAEA,SACE,qBAAC,SAAI,WAAU,+CAA8C,WAAW,eACtE;AAAA,yBAAC,SAAI,WAAU,0BACb;AAAA,2BAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,mBACG,EAAE,qCAAqC,aAAa,IACpD,EAAE,kCAAkC,UAAU;AAAA,UACjD,CAAC,SAAS,oBAAC,UAAK,WAAU,oBAAmB,gBAAE,IAAU;AAAA,WAC5D;AAAA,QACC,SACC,oBAAC,SAAM,OAAO,OAAO,YAAY,UAAQ,MAAC,IAE1C;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,UAAU,CAAC,SAAS;AAClB,wBAAU,CAAC,SAAS;AAClB,sBAAM,gBAAgB,sBAAsB,QAAQ,IAAI,IAAI,GAAG,SAAS;AAKxE,sBAAM,iBAAiB,iBAAiB,CAAC,KAAK,KAAK,KAAK;AACxD,uBAAO;AAAA,kBACL,GAAG;AAAA,kBACH,YAAY;AAAA,kBACZ,MAAM,iBAAiB,gBAAgB,KAAK;AAAA,gBAC9C;AAAA,cACF,CAAC;AAAA,YACH;AAAA,YACA,iBAAiB;AAAA,YACjB,cAAc;AAAA,YACd,oBAAoB;AAAA,YACpB,aAAa;AAAA,cACX;AAAA,cACA;AAAA,YACF;AAAA,YACA,mBAAmB;AAAA;AAAA,QACrB;AAAA,QAED,SACC,oBAAC,UAAK,WAAU,iCACb,YAAE,4CAA4C,gEAA2D,GAC5G,IACE;AAAA,SACN;AAAA,MAEA,qBAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,8BAA8B,MAAM;AAAA,UACvC,oBAAC,UAAK,WAAU,oBAAmB,gBAAE;AAAA,WACvC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,aACE,CAAC,SACG;AAAA,cACE;AAAA,cACA;AAAA,YACF,IACA;AAAA,YAEN,UAAU,CAAC,UAAU,IAAI,QAAQ,MAAM,cAAc,KAAK;AAAA;AAAA,QAC5D;AAAA,SACF;AAAA,MAEA,qBAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,kCAAkC,qBAAqB;AAAA,UACzD,CAAC,SAAS,oBAAC,UAAK,WAAU,oBAAmB,gBAAE,IAAU;AAAA,WAC5D;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,UAAU;AAAA,YACV,WAAW;AAAA,YACX,UAAU,CAAC,UACT,IAAI,gBAAgB,MAAM,cAAc,MAAM,YAAY,CAAC;AAAA;AAAA,QAE/D;AAAA,QACC,SACC,oBAAC,UAAK,WAAU,iCACb,YAAE,yCAAyC,iDAA4C,GAC1F,IACE;AAAA,SACN;AAAA,MAEA,qBAAC,WAAM,WAAU,+BACf;AAAA,4BAAC,UAAK,WAAU,yBACb,YAAE,oCAAoC,YAAY,GACrD;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,eAAe,CAAC,SACd,IAAI,aAAa,IAAsC;AAAA,YAGzD;AAAA,kCAAC,iBACC,8BAAC,eAAY,GACf;AAAA,cACA,qBAAC,iBACC;AAAA,oCAAC,cAAW,OAAM,UAAU,YAAE,wBAAwB,QAAQ,GAAE;AAAA,gBAChE,oBAAC,cAAW,OAAM,WAAW,YAAE,yBAAyB,SAAS,GAAE;AAAA,gBACnE,oBAAC,cAAW,OAAM,aAAa,YAAE,2BAA2B,WAAW,GAAE;AAAA,gBACzE,oBAAC,cAAW,OAAM,YAAY,YAAE,0BAA0B,UAAU,GAAE;AAAA,iBACxE;AAAA;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MAEA,qBAAC,WAAM,WAAU,+BACf;AAAA,4BAAC,UAAK,WAAU,yBACb,YAAE,sCAAsC,cAAc,GACzD;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,aACE,OAAO,cAAc,WACjB,EAAE,kDAAkD,0BAAqB,IACzE,EAAE,iDAAiD,aAAQ;AAAA,YAEjE,UAAU,CAAC,UAAU,IAAI,mBAAmB,MAAM,cAAc,KAAK;AAAA;AAAA,QACvE;AAAA,SACF;AAAA,MAEA,qBAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,wCAAwC,gBAAgB;AAAA,UAC3D,oBAAC,UAAK,WAAU,oBAAmB,gBAAE;AAAA,WACvC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAO,OAAO;AAAA,YACd,UAAU,CAAC,UAAU,IAAI,gBAAgB,MAAM,cAAc,KAAK;AAAA;AAAA,QACpE;AAAA,SACF;AAAA,MAEA,qBAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,uCAAuC,eAAe;AAAA,UACzD,oBAAC,UAAK,WAAU,oBAAmB,gBAAE;AAAA,WACvC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,OAAO,OAAO;AAAA,YACd,UAAU,CAAC,UAAU,IAAI,gBAAgB,MAAM,cAAc,KAAK;AAAA;AAAA,QACpE;AAAA,SACF;AAAA,MAEA,qBAAC,WAAM,WAAU,+BACf;AAAA,4BAAC,UAAK,WAAU,yBACb,YAAE,0CAA0C,8BAA8B,GAC7E;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,WAAW;AAAA,YACX,UAAU,CAAC,UACT,IAAI,mBAAmB,MAAM,cAAc,MAAM,YAAY,CAAC;AAAA;AAAA,QAElE;AAAA,SACF;AAAA,MAEA,qBAAC,WAAM,WAAU,0CACf;AAAA,4BAAC,UAAK,WAAU,yBACb,YAAE,gCAAgC,gCAAgC,GACrE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO;AAAA,YACd,UAAU,CAAC,UAAU,IAAI,SAAS,MAAM,cAAc,KAAK;AAAA;AAAA,QAC7D;AAAA,SACF;AAAA,OACF;AAAA,IAEA,oBAAC,QAAG,WAAU,mCACX,YAAE,uCAAuC,iBAAiB,GAC7D;AAAA,IACA,qBAAC,SAAI,WAAU,0BACb;AAAA,2BAAC,WAAM,WAAU,0CACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,uCAAuC,QAAQ;AAAA,UAClD,oBAAC,UAAK,WAAU,oBAAmB,gBAAE;AAAA,WACvC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO,eAAe;AAAA,YAC7B,UAAU,CAAC,UAAU,WAAW,SAAS,MAAM,cAAc,KAAK;AAAA;AAAA,QACpE;AAAA,SACF;AAAA,MACA,qBAAC,WAAM,WAAU,0CACf;AAAA,4BAAC,UAAK,WAAU,yBACb,YAAE,uCAAuC,QAAQ,GACpD;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO,eAAe;AAAA,YAC7B,UAAU,CAAC,UAAU,WAAW,SAAS,MAAM,cAAc,KAAK;AAAA;AAAA,QACpE;AAAA,SACF;AAAA,MACA,qBAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,sCAAsC,MAAM;AAAA,UAC/C,oBAAC,UAAK,WAAU,oBAAmB,gBAAE;AAAA,WACvC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO,eAAe;AAAA,YAC7B,UAAU,CAAC,UAAU,WAAW,QAAQ,MAAM,cAAc,KAAK;AAAA;AAAA,QACnE;AAAA,SACF;AAAA,MACA,qBAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,6CAA6C,aAAa;AAAA,UAC7D,oBAAC,UAAK,WAAU,oBAAmB,gBAAE;AAAA,WACvC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO,eAAe;AAAA,YAC7B,UAAU,CAAC,UAAU,WAAW,eAAe,MAAM,cAAc,KAAK;AAAA;AAAA,QAC1E;AAAA,SACF;AAAA,MACA,qBAAC,WAAM,WAAU,+BACf;AAAA,6BAAC,UAAK,WAAU,yBACb;AAAA,YAAE,yCAAyC,oBAAoB;AAAA,UAChE,oBAAC,UAAK,WAAU,oBAAmB,gBAAE;AAAA,WACvC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,OAAO,eAAe;AAAA,YAC7B,WAAW;AAAA,YACX,UAAU,CAAC,UACT,WAAW,WAAW,MAAM,cAAc,MAAM,YAAY,CAAC;AAAA;AAAA,QAEjE;AAAA,SACF;AAAA,OACF;AAAA,IAEC,SACC,qBAAC,SAAI,WAAU,gDACb;AAAA,2BAAC,SACC;AAAA,4BAAC,SAAI,WAAU,uBACZ,YAAE,gCAAgC,QAAQ,GAC7C;AAAA,QACA,oBAAC,SAAI,WAAU,iCACZ;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA,SACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,SAAS,OAAO;AAAA,UAChB,iBAAiB,CAAC,SAAS,IAAI,YAAY,IAAI;AAAA;AAAA,MACjD;AAAA,OACF,IACE;AAAA,IAEH,QAAQ,oBAAC,SAAI,WAAU,iCAAiC,iBAAM,IAAS;AAAA,IAExE,qBAAC,SAAI,WAAU,gDACb;AAAA,2BAAC,UAAK,WAAU,iCACd;AAAA,4BAAC,eAAY,MAAM,CAAC,eAAU,OAAO,GAAG;AAAA,QAAG;AAAA,QAC1C,EAAE,qCAAqC,UAAU;AAAA,QAAE;AAAA,QAAC,oBAAC,OAAI,iBAAG;AAAA,QAAO;AAAA,QACnE,EAAE,qCAAqC,YAAY;AAAA,SACtD;AAAA,MACA,qBAAC,SAAI,WAAU,2BACb;AAAA,4BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAS,UAC9C,YAAE,yBAAyB,QAAQ,GACtC;AAAA,QACA,oBAAC,UAAO,MAAK,UAAS,SAAS,MAAM,KAAK,aAAa,GAAG,UAAU,YACjE,uBACG,EAAE,gCAAgC,cAAS,IAC3C,SACE,EAAE,8BAA8B,cAAc,IAC9C,EAAE,gCAAgC,gBAAgB,GAC1D;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;",
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'\nimport { ComboboxInput, type ComboboxOption } from '@open-mercato/ui/backend/inputs'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\n/**\n * Shared form for Billing Account create + edit.\n *\n * Hand-rolled (not CrudForm) on purpose \u2014 the form's `invoiceAddress`\n * jsonb sub-object would need a custom-field renderer in the\n * CrudForm DSL, which is heavier than just rendering five inputs\n * inline. The trade-off is documented in `xd-249-billing-spec.md`\n * Phase 4d notes.\n *\n * UX per the OM UI AGENTS.md:\n * - `Cmd/Ctrl + Enter` submits.\n * - Buttons use the `Button` / `Switch` primitives, never raw HTML.\n * - Footer hint row uses `Kbd` / `KbdShortcut`.\n *\n * `mode='create'` shows the always-required fields. `mode='edit'`\n * disables `customerId` and `currencyCode` per the spec\n * (\"immutable per spec \u2014 create a new account if the customer FK\n * changes\" \u2014 see `billingAccountUpdateSchema`).\n */\n\nexport type AccountFormValues = {\n customerId: string\n name: string\n currencyCode: string\n billCycle: 'weekly' | 'monthly' | 'quarterly' | 'annually'\n billCycleAnchor: string\n invoiceEmail: string\n invoiceLanguage: string\n taxId: string\n invoiceAddress: {\n line1: string\n line2: string\n city: string\n postal_code: string\n country: string\n }\n nextBillDate: string\n isActive: boolean\n // SPEC-002 \u2014 immutable after create (like currencyCode).\n billingMode: 'postpaid' | 'prepaid'\n creditLimit: string\n // Prepaid-only initial low-balance threshold for the balance row.\n lowBalanceThreshold: string\n}\n\nexport type AccountFormProps = {\n mode: 'create' | 'edit'\n initial?: Partial<AccountFormValues>\n submitting?: boolean\n onSubmit: (values: AccountFormValues) => Promise<void> | void\n onCancel: () => void\n}\n\nconst EMPTY_VALUES: AccountFormValues = {\n customerId: '',\n name: '',\n currencyCode: 'EUR',\n billCycle: 'monthly',\n billCycleAnchor: '1',\n invoiceEmail: '',\n invoiceLanguage: 'en',\n taxId: '',\n invoiceAddress: {\n line1: '',\n line2: '',\n city: '',\n postal_code: '',\n country: 'PL',\n },\n nextBillDate: '',\n isActive: true,\n billingMode: 'postpaid',\n creditLimit: '0',\n lowBalanceThreshold: '',\n}\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\nfunction mergeInitial(initial: Partial<AccountFormValues> | undefined): AccountFormValues {\n if (!initial) return { ...EMPTY_VALUES, nextBillDate: todayIso() }\n return {\n ...EMPTY_VALUES,\n nextBillDate: EMPTY_VALUES.nextBillDate || todayIso(),\n ...initial,\n invoiceAddress: {\n ...EMPTY_VALUES.invoiceAddress,\n ...(initial.invoiceAddress ?? {}),\n },\n }\n}\n\nexport function AccountForm({\n mode,\n initial,\n submitting = false,\n onSubmit,\n onCancel,\n}: AccountFormProps) {\n const t = useT()\n const [values, setValues] = React.useState<AccountFormValues>(() => mergeInitial(initial))\n const [error, setError] = React.useState<string | null>(null)\n const isEdit = mode === 'edit'\n\n // Label cache for the customer combobox \u2014 populated as the user searches,\n // so the picker can render \"Acme Telecom\" after selection instead of the\n // raw UUID we send to the API.\n const customerLabelCacheRef = React.useRef<Map<string, ComboboxOption>>(new Map())\n\n const loadCustomerOptions = React.useCallback(\n async (query?: string): Promise<ComboboxOption[]> => {\n const params = new URLSearchParams({ page: '1', pageSize: '20' })\n const q = (query ?? '').trim()\n if (q) params.set('search', q)\n const call = await apiCall<unknown>(`/api/customers/companies?${params.toString()}`)\n if (!call.ok || call.result == null) return []\n const raw = call.result as { items?: unknown } | unknown[]\n const items: Array<Record<string, unknown>> = Array.isArray(raw)\n ? (raw as Array<Record<string, unknown>>)\n : Array.isArray((raw as { items?: unknown }).items)\n ? ((raw as { items: unknown[] }).items as Array<Record<string, unknown>>)\n : []\n const options: ComboboxOption[] = []\n for (const c of items) {\n const id = typeof c.id === 'string' ? c.id : c.id != null ? String(c.id) : ''\n if (!id) continue\n const label = String(c.display_name ?? c.displayName ?? c.name ?? id)\n const emailRaw = c.primary_email ?? c.email\n const description = typeof emailRaw === 'string' && emailRaw ? emailRaw : null\n const opt: ComboboxOption = { value: id, label, description }\n customerLabelCacheRef.current.set(id, opt)\n options.push(opt)\n }\n return options\n },\n [],\n )\n\n const resolveCustomerLabel = React.useCallback(\n (id: string) => customerLabelCacheRef.current.get(id)?.label ?? id,\n [],\n )\n const resolveCustomerDescription = React.useCallback(\n (id: string) => customerLabelCacheRef.current.get(id)?.description ?? null,\n [],\n )\n\n // Reset when the parent swaps `initial` (e.g. after loading the edit row).\n const initialKey = React.useMemo(\n () => JSON.stringify(initial ?? null),\n [initial],\n )\n React.useEffect(() => {\n setValues(mergeInitial(initial))\n }, [initialKey, initial])\n\n const set = React.useCallback(\n <K extends keyof AccountFormValues>(key: K, value: AccountFormValues[K]) => {\n setValues((prev) => ({ ...prev, [key]: value }))\n },\n [],\n )\n\n const setAddress = React.useCallback(\n <K extends keyof AccountFormValues['invoiceAddress']>(\n key: K,\n value: AccountFormValues['invoiceAddress'][K],\n ) => {\n setValues((prev) => ({\n ...prev,\n invoiceAddress: { ...prev.invoiceAddress, [key]: value },\n }))\n },\n [],\n )\n\n const handleSubmit = React.useCallback(async () => {\n if (!isEdit && !values.customerId.trim()) {\n setError(t('billing.accounts.form.error.customerId_required', 'Please select a customer'))\n return\n }\n if (!values.name.trim()) {\n setError(t('billing.accounts.form.error.name_required', 'Name is required'))\n return\n }\n if (!isEdit && values.currencyCode.trim().length !== 3) {\n setError(t('billing.accounts.form.error.currency_invalid', 'Currency must be a 3-letter ISO code'))\n return\n }\n if (!values.invoiceEmail.includes('@')) {\n setError(t('billing.accounts.form.error.email_invalid', 'Invoice email looks invalid'))\n return\n }\n if (\n !values.invoiceAddress.line1.trim() ||\n !values.invoiceAddress.city.trim() ||\n !values.invoiceAddress.postal_code.trim()\n ) {\n setError(\n t(\n 'billing.accounts.form.error.address_required',\n 'Invoice address (line 1 + city + postal code) is required',\n ),\n )\n return\n }\n const country = values.invoiceAddress.country.trim()\n if (country.length < 2 || country.length > 3) {\n setError(\n t(\n 'billing.accounts.form.error.country_invalid',\n 'Country must be a 2- or 3-letter ISO 3166 code',\n ),\n )\n return\n }\n if (!values.nextBillDate) {\n setError(t('billing.accounts.form.error.next_bill_date_required', 'Next bill date is required'))\n return\n }\n setError(null)\n await onSubmit(values)\n }, [isEdit, onSubmit, t, values])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n return (\n <div className=\"rounded-lg border border-border bg-card p-6\" onKeyDown={handleKeyDown}>\n <div className=\"grid grid-cols-2 gap-4\">\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {isEdit\n ? t('billing.accounts.form.customer_id', 'Customer ID')\n : t('billing.accounts.form.customer', 'Customer')}\n {!isEdit ? <span className=\"text-destructive\"> *</span> : null}\n </span>\n {isEdit ? (\n <Input value={values.customerId} disabled />\n ) : (\n <ComboboxInput\n value={values.customerId}\n onChange={(next) => {\n setValues((prev) => {\n const customerLabel = customerLabelCacheRef.current.get(next)?.label ?? ''\n // Auto-fill Name with the customer's display name on the first\n // pick. We leave it alone if the operator has already typed\n // something \u2014 one customer can have multiple billing accounts\n // (e.g. \"Acme \u2014 production\" / \"Acme \u2014 staging\").\n const shouldFillName = customerLabel && !prev.name.trim()\n return {\n ...prev,\n customerId: next,\n name: shouldFillName ? customerLabel : prev.name,\n }\n })\n }}\n loadSuggestions={loadCustomerOptions}\n resolveLabel={resolveCustomerLabel}\n resolveDescription={resolveCustomerDescription}\n placeholder={t(\n 'billing.accounts.form.customer.placeholder',\n 'Search customers by name or email\u2026',\n )}\n allowCustomValues={false}\n />\n )}\n {isEdit ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('billing.accounts.form.customer_id.locked', 'Immutable \u2014 create a new account to re-link the customer.')}\n </span>\n ) : null}\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.name', 'Name')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.name}\n placeholder={\n !isEdit\n ? t(\n 'billing.accounts.form.name.placeholder',\n 'Auto-filled from customer \u2014 override e.g. \"Acme \u2014 production\"',\n )\n : undefined\n }\n onChange={(event) => set('name', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.currency', 'Currency (ISO 4217)')}\n {!isEdit ? <span className=\"text-destructive\"> *</span> : null}\n </span>\n <Input\n value={values.currencyCode}\n disabled={isEdit}\n maxLength={3}\n onChange={(event) =>\n set('currencyCode', event.currentTarget.value.toUpperCase())\n }\n />\n {isEdit ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('billing.accounts.form.currency.locked', 'Immutable \u2014 frozen at account create time.')}\n </span>\n ) : null}\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.bill_cycle', 'Bill cycle')}\n </span>\n <Select\n value={values.billCycle}\n onValueChange={(next) =>\n set('billCycle', next as AccountFormValues['billCycle'])\n }\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"weekly\">{t('billing.cycle.weekly', 'Weekly')}</SelectItem>\n <SelectItem value=\"monthly\">{t('billing.cycle.monthly', 'Monthly')}</SelectItem>\n <SelectItem value=\"quarterly\">{t('billing.cycle.quarterly', 'Quarterly')}</SelectItem>\n <SelectItem value=\"annually\">{t('billing.cycle.annually', 'Annually')}</SelectItem>\n </SelectContent>\n </Select>\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.cycle_anchor', 'Cycle anchor')}\n </span>\n <Input\n value={values.billCycleAnchor}\n placeholder={\n values.billCycle === 'weekly'\n ? t('billing.accounts.form.cycle_anchor.weekly_hint', 'mon | tue | \u2026 | sun')\n : t('billing.accounts.form.cycle_anchor.month_hint', '1 \u2014 28')\n }\n onChange={(event) => set('billCycleAnchor', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.next_bill_date', 'Next bill date')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n type=\"date\"\n value={values.nextBillDate}\n onChange={(event) => set('nextBillDate', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.invoice_email', 'Invoice email')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n type=\"email\"\n value={values.invoiceEmail}\n onChange={(event) => set('invoiceEmail', event.currentTarget.value)}\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.invoice_language', 'Invoice language (ISO 639-1)')}\n </span>\n <Input\n value={values.invoiceLanguage}\n maxLength={5}\n onChange={(event) =>\n set('invoiceLanguage', event.currentTarget.value.toLowerCase())\n }\n />\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm col-span-2\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.tax_id', 'Tax ID (NIP / VATIN, optional)')}\n </span>\n <Input\n value={values.taxId}\n onChange={(event) => set('taxId', event.currentTarget.value)}\n />\n </label>\n </div>\n\n <h3 className=\"text-sm font-semibold mt-6 mb-2\">\n {t('billing.accounts.form.address.title', 'Invoice address')}\n </h3>\n <div className=\"grid grid-cols-2 gap-4\">\n <label className=\"flex flex-col gap-1 text-sm col-span-2\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.line1', 'Line 1')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.line1}\n onChange={(event) => setAddress('line1', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm col-span-2\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.line2', 'Line 2')}\n </span>\n <Input\n value={values.invoiceAddress.line2}\n onChange={(event) => setAddress('line2', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.city', 'City')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.city}\n onChange={(event) => setAddress('city', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.postal_code', 'Postal code')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.postal_code}\n onChange={(event) => setAddress('postal_code', event.currentTarget.value)}\n />\n </label>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.form.address.country', 'Country (ISO 3166)')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.invoiceAddress.country}\n maxLength={3}\n onChange={(event) =>\n setAddress('country', event.currentTarget.value.toUpperCase())\n }\n />\n </label>\n </div>\n\n <h3 className=\"text-sm font-semibold mt-6 mb-2\">\n {t('billing.accounts.form.mode.title', 'Billing mode & credit')}\n </h3>\n <div className=\"grid grid-cols-2 gap-4\">\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.columns.mode', 'Mode')}\n </span>\n <Select\n value={values.billingMode}\n disabled={isEdit}\n onValueChange={(next) => set('billingMode', next as AccountFormValues['billingMode'])}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"postpaid\">{t('billing.mode.postpaid', 'Postpaid')}</SelectItem>\n <SelectItem value=\"prepaid\">{t('billing.mode.prepaid', 'Prepaid')}</SelectItem>\n </SelectContent>\n </Select>\n {isEdit ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('billing.accounts.form.mode.locked', 'Immutable \u2014 frozen at account create time.')}\n </span>\n ) : null}\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.accounts.columns.credit_limit', 'Credit limit')}\n </span>\n <Input\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={values.creditLimit}\n onChange={(event) => set('creditLimit', event.currentTarget.value)}\n />\n </label>\n\n {values.billingMode === 'prepaid' ? (\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.prepaid.panel.threshold', 'Low threshold')}\n </span>\n <Input\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={values.lowBalanceThreshold}\n placeholder={t('billing.accounts.form.threshold.hint', 'Blank = tenant default')}\n onChange={(event) => set('lowBalanceThreshold', event.currentTarget.value)}\n />\n </label>\n ) : null}\n </div>\n\n {isEdit ? (\n <div className=\"mt-6 flex items-center justify-between gap-3\">\n <div>\n <div className=\"text-sm font-medium\">\n {t('billing.accounts.form.active', 'Active')}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.accounts.form.active.help',\n 'Inactive accounts stay billable until soft-deleted \u2014 toggle for visibility filtering only.',\n )}\n </div>\n </div>\n <Switch\n checked={values.isActive}\n onCheckedChange={(next) => set('isActive', next)}\n />\n </div>\n ) : null}\n\n {error ? <div className=\"mt-4 text-sm text-destructive\">{error}</div> : null}\n\n <div className=\"mt-6 flex items-center justify-between gap-3\">\n <span className=\"text-xs text-muted-foreground\">\n <KbdShortcut keys={['Ctrl/\u2318', 'Enter']} />{' '}\n {t('billing.accounts.form.submit_hint', 'to save,')} <Kbd>Esc</Kbd>{' '}\n {t('billing.accounts.form.cancel_hint', 'to discard')}\n </span>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" onClick={onCancel}>\n {t('billing.common.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void handleSubmit()} disabled={submitting}>\n {submitting\n ? t('billing.accounts.form.saving', 'Saving\u2026')\n : isEdit\n ? t('billing.accounts.form.save', 'Save changes')\n : t('billing.accounts.form.create', 'Create account')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|