@dainamite/billing 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +658 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/modules/billing/acl.js +20 -0
- package/dist/modules/billing/acl.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +182 -0
- package/dist/modules/billing/api/accounts/route.js.map +7 -0
- package/dist/modules/billing/api/export/account/[id]/route.js +170 -0
- package/dist/modules/billing/api/export/account/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/[id]/route.js +85 -0
- package/dist/modules/billing/api/invoices/[id]/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/add-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/add-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js +78 -0
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/post/route.js +74 -0
- package/dist/modules/billing/api/invoices/post/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js +69 -0
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +7 -0
- package/dist/modules/billing/api/invoices/route.js +109 -0
- package/dist/modules/billing/api/invoices/route.js.map +7 -0
- package/dist/modules/billing/api/items/bulk/route.js +74 -0
- package/dist/modules/billing/api/items/bulk/route.js.map +7 -0
- package/dist/modules/billing/api/items/route.js +224 -0
- package/dist/modules/billing/api/items/route.js.map +7 -0
- package/dist/modules/billing/api/openapi.js +25 -0
- package/dist/modules/billing/api/openapi.js.map +7 -0
- package/dist/modules/billing/api/run-outcomes/route.js +122 -0
- package/dist/modules/billing/api/run-outcomes/route.js.map +7 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js +70 -0
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +7 -0
- package/dist/modules/billing/api/runs/route.js +143 -0
- package/dist/modules/billing/api/runs/route.js.map +7 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js +65 -0
- package/dist/modules/billing/api/test-invoices/wipe/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +138 -0
- package/dist/modules/billing/api/usage/route.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +402 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js +82 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/accounts/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.js +173 -0
- package/dist/modules/billing/backend/billing/accounts/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/accounts/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js +500 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.js +187 -0
- package/dist/modules/billing/backend/billing/invoices/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/invoices/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js +265 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.js +90 -0
- package/dist/modules/billing/backend/billing/items/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/items/create/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.js +208 -0
- package/dist/modules/billing/backend/billing/items/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/items/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js +293 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.js +341 -0
- package/dist/modules/billing/backend/billing/runs/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/runs/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js +100 -0
- package/dist/modules/billing/backend/billing/usage/create/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js +25 -0
- package/dist/modules/billing/backend/billing/usage/create/page.meta.js.map +7 -0
- package/dist/modules/billing/ce.js +56 -0
- package/dist/modules/billing/ce.js.map +7 -0
- package/dist/modules/billing/cli.js +67 -0
- package/dist/modules/billing/cli.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +157 -0
- package/dist/modules/billing/commands/accounts.js.map +7 -0
- package/dist/modules/billing/commands/index.js +6 -0
- package/dist/modules/billing/commands/index.js.map +7 -0
- package/dist/modules/billing/commands/invoices.js +401 -0
- package/dist/modules/billing/commands/invoices.js.map +7 -0
- package/dist/modules/billing/commands/items.js +317 -0
- package/dist/modules/billing/commands/items.js.map +7 -0
- package/dist/modules/billing/commands/runs.js +123 -0
- package/dist/modules/billing/commands/runs.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +88 -0
- package/dist/modules/billing/commands/usage.js.map +7 -0
- package/dist/modules/billing/components/AccountForm.js +420 -0
- package/dist/modules/billing/components/AccountForm.js.map +7 -0
- package/dist/modules/billing/components/DetailFields.js +37 -0
- package/dist/modules/billing/components/DetailFields.js.map +7 -0
- package/dist/modules/billing/components/ItemForm.js +440 -0
- package/dist/modules/billing/components/ItemForm.js.map +7 -0
- package/dist/modules/billing/components/LineFormDialog.js +195 -0
- package/dist/modules/billing/components/LineFormDialog.js.map +7 -0
- package/dist/modules/billing/components/UsageForm.js +262 -0
- package/dist/modules/billing/components/UsageForm.js.map +7 -0
- package/dist/modules/billing/data/entities.js +433 -0
- package/dist/modules/billing/data/entities.js.map +7 -0
- package/dist/modules/billing/data/entityIds.js +12 -0
- package/dist/modules/billing/data/entityIds.js.map +7 -0
- package/dist/modules/billing/data/validators.js +396 -0
- package/dist/modules/billing/data/validators.js.map +7 -0
- package/dist/modules/billing/di.js +6 -0
- package/dist/modules/billing/di.js.map +7 -0
- package/dist/modules/billing/events.js +35 -0
- package/dist/modules/billing/events.js.map +7 -0
- package/dist/modules/billing/i18n/en.json +336 -0
- package/dist/modules/billing/i18n/pl.json +336 -0
- package/dist/modules/billing/index.js +17 -0
- package/dist/modules/billing/index.js.map +7 -0
- package/dist/modules/billing/lib/billPeriod.js +101 -0
- package/dist/modules/billing/lib/billPeriod.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +256 -0
- package/dist/modules/billing/lib/billRunEngine.js.map +7 -0
- package/dist/modules/billing/lib/billRunLabel.js +8 -0
- package/dist/modules/billing/lib/billRunLabel.js.map +7 -0
- package/dist/modules/billing/lib/idempotency.js +13 -0
- package/dist/modules/billing/lib/idempotency.js.map +7 -0
- package/dist/modules/billing/lib/invoiceStatus.js +38 -0
- package/dist/modules/billing/lib/invoiceStatus.js.map +7 -0
- package/dist/modules/billing/lib/invoiceWriter.js +153 -0
- package/dist/modules/billing/lib/invoiceWriter.js.map +7 -0
- package/dist/modules/billing/lib/itemSelector.js +77 -0
- package/dist/modules/billing/lib/itemSelector.js.map +7 -0
- package/dist/modules/billing/lib/seeds/demo.js +130 -0
- package/dist/modules/billing/lib/seeds/demo.js.map +7 -0
- package/dist/modules/billing/lib/tenantLock.js +32 -0
- package/dist/modules/billing/lib/tenantLock.js.map +7 -0
- package/dist/modules/billing/lib/usageRater.js +150 -0
- package/dist/modules/billing/lib/usageRater.js.map +7 -0
- package/dist/modules/billing/lib/usageRunner.js +82 -0
- package/dist/modules/billing/lib/usageRunner.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js +144 -0
- package/dist/modules/billing/migrations/Migration20260519000000_billing.js.map +7 -0
- package/dist/modules/billing/setup.js +190 -0
- package/dist/modules/billing/setup.js.map +7 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js +47 -0
- package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +7 -0
- package/dist/modules/billing/translations.js +5 -0
- package/dist/modules/billing/translations.js.map +7 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js +31 -0
- package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +7 -0
- package/package.json +148 -0
- package/src/modules/billing/__integration__/TC-BILL-001.spec.ts +116 -0
- package/src/modules/billing/__integration__/TC-BILL-002.spec.ts +169 -0
- package/src/modules/billing/__integration__/TC-BILL-003.spec.ts +28 -0
- package/src/modules/billing/__integration__/meta.ts +4 -0
- package/src/modules/billing/__tests__/commands/invoices.test.ts +464 -0
- package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +284 -0
- package/src/modules/billing/__tests__/lib/billPeriod.test.ts +131 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +561 -0
- package/src/modules/billing/__tests__/lib/idempotency.test.ts +81 -0
- package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +97 -0
- package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +119 -0
- package/src/modules/billing/__tests__/lib/itemSelector.test.ts +192 -0
- package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +86 -0
- package/src/modules/billing/__tests__/lib/tenantLock.test.ts +126 -0
- package/src/modules/billing/__tests__/lib/usageRater.test.ts +190 -0
- package/src/modules/billing/__tests__/package-isolation.test.ts +223 -0
- package/src/modules/billing/__tests__/setup.test.ts +107 -0
- package/src/modules/billing/__tests__/validators.test.ts +594 -0
- package/src/modules/billing/acl.ts +16 -0
- package/src/modules/billing/api/accounts/route.ts +198 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +233 -0
- package/src/modules/billing/api/invoices/[id]/route.ts +143 -0
- package/src/modules/billing/api/invoices/add-line/route.ts +74 -0
- package/src/modules/billing/api/invoices/edit-line/route.ts +97 -0
- package/src/modules/billing/api/invoices/post/route.ts +97 -0
- package/src/modules/billing/api/invoices/remove-line/route.ts +75 -0
- package/src/modules/billing/api/invoices/route.ts +167 -0
- package/src/modules/billing/api/items/bulk/route.ts +92 -0
- package/src/modules/billing/api/items/route.ts +255 -0
- package/src/modules/billing/api/openapi.ts +33 -0
- package/src/modules/billing/api/run-outcomes/route.ts +149 -0
- package/src/modules/billing/api/runs/retry-failed/route.ts +88 -0
- package/src/modules/billing/api/runs/route.ts +170 -0
- package/src/modules/billing/api/test-invoices/wipe/route.ts +87 -0
- package/src/modules/billing/api/usage/route.ts +148 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +574 -0
- package/src/modules/billing/backend/billing/accounts/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +101 -0
- package/src/modules/billing/backend/billing/accounts/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +222 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +613 -0
- package/src/modules/billing/backend/billing/invoices/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/invoices/page.tsx +245 -0
- package/src/modules/billing/backend/billing/items/[id]/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/[id]/page.tsx +366 -0
- package/src/modules/billing/backend/billing/items/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/items/create/page.tsx +110 -0
- package/src/modules/billing/backend/billing/items/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/items/page.tsx +272 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +404 -0
- package/src/modules/billing/backend/billing/runs/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/runs/page.tsx +426 -0
- package/src/modules/billing/backend/billing/usage/create/page.meta.ts +23 -0
- package/src/modules/billing/backend/billing/usage/create/page.tsx +120 -0
- package/src/modules/billing/ce.ts +74 -0
- package/src/modules/billing/cli.ts +89 -0
- package/src/modules/billing/commands/accounts.ts +205 -0
- package/src/modules/billing/commands/index.ts +13 -0
- package/src/modules/billing/commands/invoices.ts +542 -0
- package/src/modules/billing/commands/items.ts +438 -0
- package/src/modules/billing/commands/runs.ts +166 -0
- package/src/modules/billing/commands/usage.ts +118 -0
- package/src/modules/billing/components/AccountForm.tsx +517 -0
- package/src/modules/billing/components/DetailFields.tsx +60 -0
- package/src/modules/billing/components/ItemForm.tsx +558 -0
- package/src/modules/billing/components/LineFormDialog.tsx +253 -0
- package/src/modules/billing/components/UsageForm.tsx +326 -0
- package/src/modules/billing/data/entities.ts +533 -0
- package/src/modules/billing/data/entityIds.ts +25 -0
- package/src/modules/billing/data/validators.ts +562 -0
- package/src/modules/billing/di.ts +14 -0
- package/src/modules/billing/events.ts +47 -0
- package/src/modules/billing/i18n/en.json +336 -0
- package/src/modules/billing/i18n/pl.json +336 -0
- package/src/modules/billing/index.ts +22 -0
- package/src/modules/billing/lib/billPeriod.ts +181 -0
- package/src/modules/billing/lib/billRunEngine.ts +416 -0
- package/src/modules/billing/lib/billRunLabel.ts +14 -0
- package/src/modules/billing/lib/idempotency.ts +46 -0
- package/src/modules/billing/lib/invoiceStatus.ts +61 -0
- package/src/modules/billing/lib/invoiceWriter.ts +260 -0
- package/src/modules/billing/lib/itemSelector.ts +154 -0
- package/src/modules/billing/lib/seeds/demo.ts +199 -0
- package/src/modules/billing/lib/tenantLock.ts +87 -0
- package/src/modules/billing/lib/usageRater.ts +250 -0
- package/src/modules/billing/lib/usageRunner.ts +189 -0
- package/src/modules/billing/migrations/Migration20260519000000_billing.ts +175 -0
- package/src/modules/billing/setup.ts +252 -0
- package/src/modules/billing/subscribers/payment-captured-to-paid.ts +89 -0
- package/src/modules/billing/translations.ts +9 -0
- package/src/modules/billing/workers/reap-stale-bill-runs.ts +57 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle
|
|
11
|
+
} from "@open-mercato/ui/primitives/dialog";
|
|
12
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
13
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
14
|
+
import { Textarea } from "@open-mercato/ui/primitives/textarea";
|
|
15
|
+
import { Kbd, KbdShortcut } from "@open-mercato/ui/primitives/kbd";
|
|
16
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
17
|
+
function toNumber(raw) {
|
|
18
|
+
if (raw.trim() === "") return null;
|
|
19
|
+
const n = Number.parseFloat(raw);
|
|
20
|
+
return Number.isFinite(n) ? n : null;
|
|
21
|
+
}
|
|
22
|
+
function LineFormDialog({
|
|
23
|
+
open,
|
|
24
|
+
mode,
|
|
25
|
+
initial,
|
|
26
|
+
submitting = false,
|
|
27
|
+
onSubmit,
|
|
28
|
+
onCancel
|
|
29
|
+
}) {
|
|
30
|
+
const t = useT();
|
|
31
|
+
const [description, setDescription] = React.useState(initial?.description ?? "");
|
|
32
|
+
const [quantity, setQuantity] = React.useState(
|
|
33
|
+
initial?.quantity != null ? String(initial.quantity) : ""
|
|
34
|
+
);
|
|
35
|
+
const [unitPriceNet, setUnitPriceNet] = React.useState(
|
|
36
|
+
initial?.unitPriceNet != null ? String(initial.unitPriceNet) : ""
|
|
37
|
+
);
|
|
38
|
+
const [totalOverride, setTotalOverride] = React.useState(
|
|
39
|
+
initial?.totalNetAmount != null ? String(initial.totalNetAmount) : ""
|
|
40
|
+
);
|
|
41
|
+
const [error, setError] = React.useState(null);
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
if (open) {
|
|
44
|
+
setDescription(initial?.description ?? "");
|
|
45
|
+
setQuantity(initial?.quantity != null ? String(initial.quantity) : "");
|
|
46
|
+
setUnitPriceNet(initial?.unitPriceNet != null ? String(initial.unitPriceNet) : "");
|
|
47
|
+
setTotalOverride(initial?.totalNetAmount != null ? String(initial.totalNetAmount) : "");
|
|
48
|
+
setError(null);
|
|
49
|
+
}
|
|
50
|
+
}, [open, initial]);
|
|
51
|
+
const handleSubmit = React.useCallback(async () => {
|
|
52
|
+
const trimmedDescription = description.trim();
|
|
53
|
+
if (!trimmedDescription) {
|
|
54
|
+
setError(t("billing.invoices.lines.form.error.description_required", "Description is required"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const quantityValue = toNumber(quantity);
|
|
58
|
+
if (quantityValue == null || quantityValue < 0) {
|
|
59
|
+
setError(t("billing.invoices.lines.form.error.quantity_invalid", "Quantity must be a non-negative number"));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const unitPriceValue = toNumber(unitPriceNet);
|
|
63
|
+
if (unitPriceValue == null) {
|
|
64
|
+
setError(t("billing.invoices.lines.form.error.unit_price_invalid", "Unit price must be a number"));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const totalOverrideRaw = totalOverride.trim();
|
|
68
|
+
let totalOverrideValue;
|
|
69
|
+
if (totalOverrideRaw !== "") {
|
|
70
|
+
const parsed = toNumber(totalOverrideRaw);
|
|
71
|
+
if (parsed == null) {
|
|
72
|
+
setError(t("billing.invoices.lines.form.error.total_invalid", "Total override must be a number when set"));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
totalOverrideValue = parsed;
|
|
76
|
+
}
|
|
77
|
+
setError(null);
|
|
78
|
+
await onSubmit({
|
|
79
|
+
description: trimmedDescription,
|
|
80
|
+
quantity: quantityValue,
|
|
81
|
+
unitPriceNet: unitPriceValue,
|
|
82
|
+
totalNetAmount: totalOverrideValue
|
|
83
|
+
});
|
|
84
|
+
}, [description, onSubmit, quantity, t, totalOverride, unitPriceNet]);
|
|
85
|
+
const handleKeyDown = React.useCallback(
|
|
86
|
+
(event) => {
|
|
87
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
void handleSubmit();
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
[handleSubmit]
|
|
93
|
+
);
|
|
94
|
+
const computedTotal = React.useMemo(() => {
|
|
95
|
+
const q = toNumber(quantity);
|
|
96
|
+
const u = toNumber(unitPriceNet);
|
|
97
|
+
if (q == null || u == null) return null;
|
|
98
|
+
return (Math.round(q * u * 100) / 100).toFixed(2);
|
|
99
|
+
}, [quantity, unitPriceNet]);
|
|
100
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (next) => {
|
|
101
|
+
if (!next) onCancel();
|
|
102
|
+
}, children: /* @__PURE__ */ jsxs(DialogContent, { onKeyDown: handleKeyDown, children: [
|
|
103
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
104
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: mode === "add" ? t("billing.invoices.lines.form.title_add", "Add invoice line") : t("billing.invoices.lines.form.title_edit", "Edit invoice line") }),
|
|
105
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: t(
|
|
106
|
+
"billing.invoices.lines.form.help",
|
|
107
|
+
"Changes are recorded in the draft-edit audit log."
|
|
108
|
+
) })
|
|
109
|
+
] }),
|
|
110
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 py-2", children: [
|
|
111
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
112
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.invoices.lines.form.description", "Description") }),
|
|
113
|
+
/* @__PURE__ */ jsx(
|
|
114
|
+
Textarea,
|
|
115
|
+
{
|
|
116
|
+
value: description,
|
|
117
|
+
onChange: (event) => setDescription(event.currentTarget.value),
|
|
118
|
+
rows: 2
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
123
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
124
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.invoices.lines.form.quantity", "Quantity") }),
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
Input,
|
|
127
|
+
{
|
|
128
|
+
type: "number",
|
|
129
|
+
value: quantity,
|
|
130
|
+
step: "0.0001",
|
|
131
|
+
onChange: (event) => setQuantity(event.currentTarget.value)
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
136
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.invoices.lines.form.unit_price", "Unit price (net)") }),
|
|
137
|
+
/* @__PURE__ */ jsx(
|
|
138
|
+
Input,
|
|
139
|
+
{
|
|
140
|
+
type: "number",
|
|
141
|
+
value: unitPriceNet,
|
|
142
|
+
step: "0.0001",
|
|
143
|
+
onChange: (event) => setUnitPriceNet(event.currentTarget.value)
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
] })
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
149
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.invoices.lines.form.total_override", "Total override (optional)") }),
|
|
150
|
+
/* @__PURE__ */ jsx(
|
|
151
|
+
Input,
|
|
152
|
+
{
|
|
153
|
+
type: "number",
|
|
154
|
+
value: totalOverride,
|
|
155
|
+
step: "0.0001",
|
|
156
|
+
placeholder: computedTotal ?? "",
|
|
157
|
+
onChange: (event) => setTotalOverride(event.currentTarget.value)
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: computedTotal != null ? t(
|
|
161
|
+
"billing.invoices.lines.form.computed_total",
|
|
162
|
+
"Computed total: {computed}"
|
|
163
|
+
).replace("{computed}", computedTotal) : null })
|
|
164
|
+
] }),
|
|
165
|
+
error ? /* @__PURE__ */ jsx("div", { className: "text-sm text-destructive", children: error }) : null
|
|
166
|
+
] }),
|
|
167
|
+
/* @__PURE__ */ jsx(DialogFooter, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 w-full", children: [
|
|
168
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
169
|
+
/* @__PURE__ */ jsx(KbdShortcut, { keys: ["Ctrl/\u2318", "Enter"] }),
|
|
170
|
+
" ",
|
|
171
|
+
t("billing.invoices.lines.form.submit_hint", "to save,"),
|
|
172
|
+
" ",
|
|
173
|
+
/* @__PURE__ */ jsx(Kbd, { children: "Esc" }),
|
|
174
|
+
" ",
|
|
175
|
+
t("billing.invoices.lines.form.cancel_hint", "to cancel")
|
|
176
|
+
] }),
|
|
177
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
178
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onCancel, children: t("billing.common.cancel", "Cancel") }),
|
|
179
|
+
/* @__PURE__ */ jsx(
|
|
180
|
+
Button,
|
|
181
|
+
{
|
|
182
|
+
type: "button",
|
|
183
|
+
onClick: () => void handleSubmit(),
|
|
184
|
+
disabled: submitting,
|
|
185
|
+
children: submitting ? t("billing.invoices.lines.form.saving", "Saving\u2026") : t("billing.common.save", "Save")
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
] })
|
|
189
|
+
] }) })
|
|
190
|
+
] }) });
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
LineFormDialog
|
|
194
|
+
};
|
|
195
|
+
//# sourceMappingURL=LineFormDialog.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/components/LineFormDialog.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\n/**\n * Shared dialog for add / edit of an invoice line.\n *\n * - `mode='add'` shows empty fields and calls the\n * `billing.invoices.add_draft_line` route on submit.\n * - `mode='edit'` pre-fills from `initial` and calls\n * `billing.invoices.edit_draft_line`.\n *\n * The caller owns the network call (so we can stay simple and avoid\n * threading apiCall context through the component). On submit, this\n * dialog only emits `onSubmit(values)` and the parent decides which\n * command to hit.\n *\n * UX conventions per `@open-mercato/ui`:\n * - `Cmd/Ctrl + Enter` submits.\n * - `Escape` cancels (Radix Dialog handles this for free).\n * - Hint row at the bottom lists both shortcuts via `Kbd` /\n * `KbdShortcut` (not raw `<span>`).\n */\n\nexport type LineFormValues = {\n description: string\n quantity: number\n unitPriceNet: number\n /** Optional explicit total override (operator \"I know better\" path). */\n totalNetAmount?: number\n}\n\nexport type LineFormDialogProps = {\n open: boolean\n mode: 'add' | 'edit'\n /** Pre-fill values for edit mode. */\n initial?: Partial<LineFormValues>\n submitting?: boolean\n onSubmit: (values: LineFormValues) => Promise<void> | void\n onCancel: () => void\n}\n\nfunction toNumber(raw: string): number | null {\n if (raw.trim() === '') return null\n const n = Number.parseFloat(raw)\n return Number.isFinite(n) ? n : null\n}\n\nexport function LineFormDialog({\n open,\n mode,\n initial,\n submitting = false,\n onSubmit,\n onCancel,\n}: LineFormDialogProps) {\n const t = useT()\n const [description, setDescription] = React.useState(initial?.description ?? '')\n const [quantity, setQuantity] = React.useState(\n initial?.quantity != null ? String(initial.quantity) : '',\n )\n const [unitPriceNet, setUnitPriceNet] = React.useState(\n initial?.unitPriceNet != null ? String(initial.unitPriceNet) : '',\n )\n const [totalOverride, setTotalOverride] = React.useState(\n initial?.totalNetAmount != null ? String(initial.totalNetAmount) : '',\n )\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n if (open) {\n setDescription(initial?.description ?? '')\n setQuantity(initial?.quantity != null ? String(initial.quantity) : '')\n setUnitPriceNet(initial?.unitPriceNet != null ? String(initial.unitPriceNet) : '')\n setTotalOverride(initial?.totalNetAmount != null ? String(initial.totalNetAmount) : '')\n setError(null)\n }\n }, [open, initial])\n\n const handleSubmit = React.useCallback(async () => {\n const trimmedDescription = description.trim()\n if (!trimmedDescription) {\n setError(t('billing.invoices.lines.form.error.description_required', 'Description is required'))\n return\n }\n const quantityValue = toNumber(quantity)\n if (quantityValue == null || quantityValue < 0) {\n setError(t('billing.invoices.lines.form.error.quantity_invalid', 'Quantity must be a non-negative number'))\n return\n }\n const unitPriceValue = toNumber(unitPriceNet)\n if (unitPriceValue == null) {\n setError(t('billing.invoices.lines.form.error.unit_price_invalid', 'Unit price must be a number'))\n return\n }\n const totalOverrideRaw = totalOverride.trim()\n let totalOverrideValue: number | undefined\n if (totalOverrideRaw !== '') {\n const parsed = toNumber(totalOverrideRaw)\n if (parsed == null) {\n setError(t('billing.invoices.lines.form.error.total_invalid', 'Total override must be a number when set'))\n return\n }\n totalOverrideValue = parsed\n }\n setError(null)\n await onSubmit({\n description: trimmedDescription,\n quantity: quantityValue,\n unitPriceNet: unitPriceValue,\n totalNetAmount: totalOverrideValue,\n })\n }, [description, onSubmit, quantity, t, totalOverride, unitPriceNet])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n // Cmd/Ctrl + Enter submits \u2014 required by the OM UX convention\n // for every dialog. Escape is handled by Radix Dialog itself.\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n // Auto-compute total for the helper text \u2014 purely informational, not\n // submitted unless the operator types into `totalOverride`.\n const computedTotal = React.useMemo(() => {\n const q = toNumber(quantity)\n const u = toNumber(unitPriceNet)\n if (q == null || u == null) return null\n return (Math.round(q * u * 100) / 100).toFixed(2)\n }, [quantity, unitPriceNet])\n\n return (\n <Dialog open={open} onOpenChange={(next) => { if (!next) onCancel() }}>\n <DialogContent onKeyDown={handleKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {mode === 'add'\n ? t('billing.invoices.lines.form.title_add', 'Add invoice line')\n : t('billing.invoices.lines.form.title_edit', 'Edit invoice line')}\n </DialogTitle>\n <DialogDescription>\n {t(\n 'billing.invoices.lines.form.help',\n 'Changes are recorded in the draft-edit audit log.',\n )}\n </DialogDescription>\n </DialogHeader>\n <div className=\"flex flex-col gap-4 py-2\">\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.invoices.lines.form.description', 'Description')}\n </span>\n <Textarea\n value={description}\n onChange={(event) => setDescription(event.currentTarget.value)}\n rows={2}\n />\n </label>\n <div className=\"grid grid-cols-2 gap-3\">\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.invoices.lines.form.quantity', 'Quantity')}\n </span>\n <Input\n type=\"number\"\n value={quantity}\n step=\"0.0001\"\n onChange={(event) => setQuantity(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.invoices.lines.form.unit_price', 'Unit price (net)')}\n </span>\n <Input\n type=\"number\"\n value={unitPriceNet}\n step=\"0.0001\"\n onChange={(event) => setUnitPriceNet(event.currentTarget.value)}\n />\n </label>\n </div>\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.invoices.lines.form.total_override', 'Total override (optional)')}\n </span>\n <Input\n type=\"number\"\n value={totalOverride}\n step=\"0.0001\"\n placeholder={computedTotal ?? ''}\n onChange={(event) => setTotalOverride(event.currentTarget.value)}\n />\n <span className=\"text-xs text-muted-foreground\">\n {computedTotal != null\n ? t(\n 'billing.invoices.lines.form.computed_total',\n 'Computed total: {computed}',\n ).replace('{computed}', computedTotal)\n : null}\n </span>\n </label>\n {error ? (\n <div className=\"text-sm text-destructive\">{error}</div>\n ) : null}\n </div>\n <DialogFooter>\n <div className=\"flex items-center justify-between gap-3 w-full\">\n <span className=\"text-xs text-muted-foreground\">\n <KbdShortcut keys={['Ctrl/\u2318', 'Enter']} />\n {' '}\n {t('billing.invoices.lines.form.submit_hint', 'to save,')}\n {' '}\n <Kbd>Esc</Kbd>\n {' '}\n {t('billing.invoices.lines.form.cancel_hint', 'to cancel')}\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\n type=\"button\"\n onClick={() => void handleSubmit()}\n disabled={submitting}\n >\n {submitting\n ? t('billing.invoices.lines.form.saving', 'Saving\u2026')\n : t('billing.common.save', 'Save')}\n </Button>\n </div>\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
5
|
+
import { Input } from "@open-mercato/ui/primitives/input";
|
|
6
|
+
import { Textarea } from "@open-mercato/ui/primitives/textarea";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue
|
|
13
|
+
} from "@open-mercato/ui/primitives/select";
|
|
14
|
+
import { Kbd, KbdShortcut } from "@open-mercato/ui/primitives/kbd";
|
|
15
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
16
|
+
const EMPTY_VALUES = {
|
|
17
|
+
billAccountId: "",
|
|
18
|
+
uomCode: "",
|
|
19
|
+
quantity: "",
|
|
20
|
+
periodStart: "",
|
|
21
|
+
periodEnd: "",
|
|
22
|
+
lineDescription: "",
|
|
23
|
+
sourceRef: ""
|
|
24
|
+
};
|
|
25
|
+
function firstOfMonthIso() {
|
|
26
|
+
const now = /* @__PURE__ */ new Date();
|
|
27
|
+
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
|
|
28
|
+
}
|
|
29
|
+
function todayIso() {
|
|
30
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
function mergeInitial(initial) {
|
|
33
|
+
return {
|
|
34
|
+
...EMPTY_VALUES,
|
|
35
|
+
periodStart: EMPTY_VALUES.periodStart || firstOfMonthIso(),
|
|
36
|
+
periodEnd: EMPTY_VALUES.periodEnd || todayIso(),
|
|
37
|
+
...initial
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toNumber(raw) {
|
|
41
|
+
if (raw.trim() === "") return null;
|
|
42
|
+
const n = Number.parseFloat(raw);
|
|
43
|
+
return Number.isFinite(n) ? n : null;
|
|
44
|
+
}
|
|
45
|
+
function UsageForm({
|
|
46
|
+
accountOptions,
|
|
47
|
+
initial,
|
|
48
|
+
submitting = false,
|
|
49
|
+
onSubmit,
|
|
50
|
+
onCancel
|
|
51
|
+
}) {
|
|
52
|
+
const t = useT();
|
|
53
|
+
const [values, setValues] = React.useState(() => mergeInitial(initial));
|
|
54
|
+
const [error, setError] = React.useState(null);
|
|
55
|
+
const initialKey = React.useMemo(() => JSON.stringify(initial ?? null), [initial]);
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
setValues(mergeInitial(initial));
|
|
58
|
+
}, [initialKey]);
|
|
59
|
+
const set = React.useCallback(
|
|
60
|
+
(key, value) => {
|
|
61
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
62
|
+
},
|
|
63
|
+
[]
|
|
64
|
+
);
|
|
65
|
+
const handleSubmit = React.useCallback(async () => {
|
|
66
|
+
if (!values.billAccountId.trim()) {
|
|
67
|
+
setError(t("billing.usage.form.error.account_required", "Billing Account is required"));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!values.uomCode.trim()) {
|
|
71
|
+
setError(t("billing.usage.form.error.uom_required", "UoM code is required"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const quantity = toNumber(values.quantity);
|
|
75
|
+
if (quantity == null || quantity < 0) {
|
|
76
|
+
setError(
|
|
77
|
+
t("billing.usage.form.error.quantity_invalid", "Quantity must be a non-negative number")
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!values.periodStart || !values.periodEnd) {
|
|
82
|
+
setError(t("billing.usage.form.error.period_required", "Period start and end are required"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (values.periodEnd < values.periodStart) {
|
|
86
|
+
setError(
|
|
87
|
+
t("billing.usage.form.error.period_order", "Period end must be on or after period start")
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setError(null);
|
|
92
|
+
const payload = {
|
|
93
|
+
billAccountId: values.billAccountId,
|
|
94
|
+
uomCode: values.uomCode.trim(),
|
|
95
|
+
quantity,
|
|
96
|
+
periodStart: values.periodStart,
|
|
97
|
+
periodEnd: values.periodEnd
|
|
98
|
+
};
|
|
99
|
+
if (values.lineDescription.trim()) payload.lineDescription = values.lineDescription.trim();
|
|
100
|
+
if (values.sourceRef.trim()) payload.sourceRef = values.sourceRef.trim();
|
|
101
|
+
await onSubmit(payload);
|
|
102
|
+
}, [onSubmit, t, values]);
|
|
103
|
+
const handleKeyDown = React.useCallback(
|
|
104
|
+
(event) => {
|
|
105
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
void handleSubmit();
|
|
108
|
+
} else if (event.key === "Escape") {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
onCancel();
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[handleSubmit, onCancel]
|
|
114
|
+
);
|
|
115
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-border bg-card p-6", onKeyDown: handleKeyDown, children: [
|
|
116
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
117
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm col-span-2", children: [
|
|
118
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
119
|
+
t("billing.usage.form.account", "Billing Account"),
|
|
120
|
+
/* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsxs(
|
|
123
|
+
Select,
|
|
124
|
+
{
|
|
125
|
+
value: values.billAccountId,
|
|
126
|
+
onValueChange: (next) => set("billAccountId", next),
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(
|
|
129
|
+
SelectValue,
|
|
130
|
+
{
|
|
131
|
+
placeholder: t("billing.usage.form.account.placeholder", "Select an account")
|
|
132
|
+
}
|
|
133
|
+
) }),
|
|
134
|
+
/* @__PURE__ */ jsx(SelectContent, { children: accountOptions.map((opt) => /* @__PURE__ */ jsxs(SelectItem, { value: opt.id, children: [
|
|
135
|
+
opt.name,
|
|
136
|
+
" \xB7 ",
|
|
137
|
+
opt.customer_id,
|
|
138
|
+
" \xB7 ",
|
|
139
|
+
opt.currency_code
|
|
140
|
+
] }, opt.id)) })
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
] }),
|
|
145
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
146
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
147
|
+
t("billing.usage.form.uom_code", "UoM code"),
|
|
148
|
+
/* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
|
|
149
|
+
] }),
|
|
150
|
+
/* @__PURE__ */ jsx(
|
|
151
|
+
Input,
|
|
152
|
+
{
|
|
153
|
+
value: values.uomCode,
|
|
154
|
+
placeholder: t("billing.usage.form.uom_code.placeholder", "api_request, gb, seat, \u2026"),
|
|
155
|
+
onChange: (event) => set("uomCode", event.currentTarget.value)
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
|
|
159
|
+
"billing.usage.form.uom_code.help",
|
|
160
|
+
"Must exactly match the UoM on a usage Billing Item for this account \u2014 no conversion."
|
|
161
|
+
) })
|
|
162
|
+
] }),
|
|
163
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
164
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
165
|
+
t("billing.usage.form.quantity", "Quantity"),
|
|
166
|
+
/* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
|
|
167
|
+
] }),
|
|
168
|
+
/* @__PURE__ */ jsx(
|
|
169
|
+
Input,
|
|
170
|
+
{
|
|
171
|
+
type: "number",
|
|
172
|
+
step: "0.0001",
|
|
173
|
+
min: "0",
|
|
174
|
+
value: values.quantity,
|
|
175
|
+
onChange: (event) => set("quantity", event.currentTarget.value)
|
|
176
|
+
}
|
|
177
|
+
),
|
|
178
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
|
|
179
|
+
"billing.usage.form.quantity.help",
|
|
180
|
+
"Total units consumed in the period (pre-aggregated)."
|
|
181
|
+
) })
|
|
182
|
+
] }),
|
|
183
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
184
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
185
|
+
t("billing.usage.form.period_start", "Period start"),
|
|
186
|
+
/* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
|
|
187
|
+
] }),
|
|
188
|
+
/* @__PURE__ */ jsx(
|
|
189
|
+
Input,
|
|
190
|
+
{
|
|
191
|
+
type: "date",
|
|
192
|
+
value: values.periodStart,
|
|
193
|
+
onChange: (event) => set("periodStart", event.currentTarget.value)
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
] }),
|
|
197
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm", children: [
|
|
198
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
199
|
+
t("billing.usage.form.period_end", "Period end"),
|
|
200
|
+
/* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
|
|
201
|
+
] }),
|
|
202
|
+
/* @__PURE__ */ jsx(
|
|
203
|
+
Input,
|
|
204
|
+
{
|
|
205
|
+
type: "date",
|
|
206
|
+
value: values.periodEnd,
|
|
207
|
+
onChange: (event) => set("periodEnd", event.currentTarget.value)
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
] }),
|
|
211
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm col-span-2", children: [
|
|
212
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.usage.form.description", "Description (optional)") }),
|
|
213
|
+
/* @__PURE__ */ jsx(
|
|
214
|
+
Textarea,
|
|
215
|
+
{
|
|
216
|
+
value: values.lineDescription,
|
|
217
|
+
rows: 2,
|
|
218
|
+
onChange: (event) => set("lineDescription", event.currentTarget.value)
|
|
219
|
+
}
|
|
220
|
+
),
|
|
221
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
|
|
222
|
+
"billing.usage.form.description.help",
|
|
223
|
+
"Shown on its own invoice line when set; otherwise matching records are aggregated into one line."
|
|
224
|
+
) })
|
|
225
|
+
] }),
|
|
226
|
+
/* @__PURE__ */ jsxs("label", { className: "flex flex-col gap-1 text-sm col-span-2", children: [
|
|
227
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("billing.usage.form.source_ref", "Source reference (optional)") }),
|
|
228
|
+
/* @__PURE__ */ jsx(
|
|
229
|
+
Input,
|
|
230
|
+
{
|
|
231
|
+
value: values.sourceRef,
|
|
232
|
+
onChange: (event) => set("sourceRef", event.currentTarget.value)
|
|
233
|
+
}
|
|
234
|
+
),
|
|
235
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t(
|
|
236
|
+
"billing.usage.form.source_ref.help",
|
|
237
|
+
"Idempotency key \u2014 re-submitting the same reference for this account is deduplicated."
|
|
238
|
+
) })
|
|
239
|
+
] })
|
|
240
|
+
] }),
|
|
241
|
+
error ? /* @__PURE__ */ jsx("div", { className: "mt-4 text-sm text-destructive", children: error }) : null,
|
|
242
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center justify-between gap-3", children: [
|
|
243
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
244
|
+
/* @__PURE__ */ jsx(KbdShortcut, { keys: ["Ctrl/\u2318", "Enter"] }),
|
|
245
|
+
" ",
|
|
246
|
+
t("billing.usage.form.submit_hint", "to save,"),
|
|
247
|
+
" ",
|
|
248
|
+
/* @__PURE__ */ jsx(Kbd, { children: "Esc" }),
|
|
249
|
+
" ",
|
|
250
|
+
t("billing.usage.form.cancel_hint", "to discard")
|
|
251
|
+
] }),
|
|
252
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
253
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onCancel, children: t("billing.common.cancel", "Cancel") }),
|
|
254
|
+
/* @__PURE__ */ jsx(Button, { type: "button", onClick: () => void handleSubmit(), disabled: submitting, children: submitting ? t("billing.usage.form.saving", "Saving\u2026") : t("billing.usage.form.create", "Record usage") })
|
|
255
|
+
] })
|
|
256
|
+
] })
|
|
257
|
+
] });
|
|
258
|
+
}
|
|
259
|
+
export {
|
|
260
|
+
UsageForm
|
|
261
|
+
};
|
|
262
|
+
//# sourceMappingURL=UsageForm.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/billing/components/UsageForm.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 { Textarea } from '@open-mercato/ui/primitives/textarea'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Kbd, KbdShortcut } from '@open-mercato/ui/primitives/kbd'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\n/**\n * Manual Billing Account Usage entry form.\n *\n * Usage is normally ingested by an external metering system via\n * `POST /api/billing/usage` + an `X-Api-Key` scoped to\n * `billing.usage.ingest`. This form gives operators the same path from\n * the admin UI \u2014 for corrections, demos, or customers without an\n * automated meter. It POSTs to the very same endpoint, so the\n * idempotency (`sourceRef`) and append-only guarantees are identical.\n *\n * Usage records are immutable once written \u2014 there is no edit/delete,\n * hence this is a create-only form (mirrors the `billing.usage.create`\n * command, which has no update sibling).\n */\n\n// The accounts list API returns snake_case rows \u2014 mirror that here so\n// the picker reads real values (camelCase would resolve to `undefined`).\nexport type UsageAccountOption = {\n id: string\n name: string\n customer_id: string\n currency_code: string\n}\n\nexport type UsageFormValues = {\n billAccountId: string\n uomCode: string\n quantity: string\n periodStart: string\n periodEnd: string\n lineDescription: string\n sourceRef: string\n}\n\nexport type AssembledUsagePayload = {\n billAccountId: string\n uomCode: string\n quantity: number\n periodStart: string\n periodEnd: string\n lineDescription?: string\n sourceRef?: string\n}\n\nexport type UsageFormProps = {\n accountOptions: UsageAccountOption[]\n initial?: Partial<UsageFormValues>\n submitting?: boolean\n onSubmit: (assembled: AssembledUsagePayload) => Promise<void> | void\n onCancel: () => void\n}\n\nconst EMPTY_VALUES: UsageFormValues = {\n billAccountId: '',\n uomCode: '',\n quantity: '',\n periodStart: '',\n periodEnd: '',\n lineDescription: '',\n sourceRef: '',\n}\n\nfunction firstOfMonthIso(): string {\n const now = new Date()\n return new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10)\n}\n\nfunction todayIso(): string {\n return new Date().toISOString().slice(0, 10)\n}\n\nfunction mergeInitial(initial: Partial<UsageFormValues> | undefined): UsageFormValues {\n return {\n ...EMPTY_VALUES,\n periodStart: EMPTY_VALUES.periodStart || firstOfMonthIso(),\n periodEnd: EMPTY_VALUES.periodEnd || todayIso(),\n ...initial,\n }\n}\n\nfunction toNumber(raw: string): number | null {\n if (raw.trim() === '') return null\n const n = Number.parseFloat(raw)\n return Number.isFinite(n) ? n : null\n}\n\nexport function UsageForm({\n accountOptions,\n initial,\n submitting = false,\n onSubmit,\n onCancel,\n}: UsageFormProps) {\n const t = useT()\n const [values, setValues] = React.useState<UsageFormValues>(() => mergeInitial(initial))\n const [error, setError] = React.useState<string | null>(null)\n\n const initialKey = React.useMemo(() => JSON.stringify(initial ?? null), [initial])\n React.useEffect(() => {\n // Depend on the serialized key only \u2014 `initial` is a fresh object ref on\n // every parent render, so including it would re-run this effect (and wipe\n // in-progress edits) whenever the parent re-renders for an unrelated reason.\n setValues(mergeInitial(initial))\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [initialKey])\n\n const set = React.useCallback(\n <K extends keyof UsageFormValues>(key: K, value: UsageFormValues[K]) => {\n setValues((prev) => ({ ...prev, [key]: value }))\n },\n [],\n )\n\n const handleSubmit = React.useCallback(async () => {\n if (!values.billAccountId.trim()) {\n setError(t('billing.usage.form.error.account_required', 'Billing Account is required'))\n return\n }\n if (!values.uomCode.trim()) {\n setError(t('billing.usage.form.error.uom_required', 'UoM code is required'))\n return\n }\n const quantity = toNumber(values.quantity)\n if (quantity == null || quantity < 0) {\n setError(\n t('billing.usage.form.error.quantity_invalid', 'Quantity must be a non-negative number'),\n )\n return\n }\n if (!values.periodStart || !values.periodEnd) {\n setError(t('billing.usage.form.error.period_required', 'Period start and end are required'))\n return\n }\n if (values.periodEnd < values.periodStart) {\n setError(\n t('billing.usage.form.error.period_order', 'Period end must be on or after period start'),\n )\n return\n }\n setError(null)\n\n const payload: AssembledUsagePayload = {\n billAccountId: values.billAccountId,\n uomCode: values.uomCode.trim(),\n quantity,\n periodStart: values.periodStart,\n periodEnd: values.periodEnd,\n }\n if (values.lineDescription.trim()) payload.lineDescription = values.lineDescription.trim()\n if (values.sourceRef.trim()) payload.sourceRef = values.sourceRef.trim()\n await onSubmit(payload)\n }, [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 } else if (event.key === 'Escape') {\n event.preventDefault()\n onCancel()\n }\n },\n [handleSubmit, onCancel],\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 col-span-2\">\n <span className=\"text-muted-foreground\">\n {t('billing.usage.form.account', 'Billing Account')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Select\n value={values.billAccountId}\n onValueChange={(next) => set('billAccountId', next)}\n >\n <SelectTrigger>\n <SelectValue\n placeholder={t('billing.usage.form.account.placeholder', 'Select an account')}\n />\n </SelectTrigger>\n <SelectContent>\n {accountOptions.map((opt) => (\n <SelectItem key={opt.id} value={opt.id}>\n {opt.name} \u00B7 {opt.customer_id} \u00B7 {opt.currency_code}\n </SelectItem>\n ))}\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.usage.form.uom_code', 'UoM code')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n value={values.uomCode}\n placeholder={t('billing.usage.form.uom_code.placeholder', 'api_request, gb, seat, \u2026')}\n onChange={(event) => set('uomCode', event.currentTarget.value)}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.usage.form.uom_code.help',\n 'Must exactly match the UoM on a usage Billing Item for this account \u2014 no conversion.',\n )}\n </span>\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.usage.form.quantity', 'Quantity')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n type=\"number\"\n step=\"0.0001\"\n min=\"0\"\n value={values.quantity}\n onChange={(event) => set('quantity', event.currentTarget.value)}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.usage.form.quantity.help',\n 'Total units consumed in the period (pre-aggregated).',\n )}\n </span>\n </label>\n\n <label className=\"flex flex-col gap-1 text-sm\">\n <span className=\"text-muted-foreground\">\n {t('billing.usage.form.period_start', 'Period start')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n type=\"date\"\n value={values.periodStart}\n onChange={(event) => set('periodStart', 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.usage.form.period_end', 'Period end')}\n <span className=\"text-destructive\"> *</span>\n </span>\n <Input\n type=\"date\"\n value={values.periodEnd}\n onChange={(event) => set('periodEnd', event.currentTarget.value)}\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.usage.form.description', 'Description (optional)')}\n </span>\n <Textarea\n value={values.lineDescription}\n rows={2}\n onChange={(event) => set('lineDescription', event.currentTarget.value)}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.usage.form.description.help',\n 'Shown on its own invoice line when set; otherwise matching records are aggregated into one line.',\n )}\n </span>\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.usage.form.source_ref', 'Source reference (optional)')}\n </span>\n <Input\n value={values.sourceRef}\n onChange={(event) => set('sourceRef', event.currentTarget.value)}\n />\n <span className=\"text-xs text-muted-foreground\">\n {t(\n 'billing.usage.form.source_ref.help',\n 'Idempotency key \u2014 re-submitting the same reference for this account is deduplicated.',\n )}\n </span>\n </label>\n </div>\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.usage.form.submit_hint', 'to save,')} <Kbd>Esc</Kbd>{' '}\n {t('billing.usage.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.usage.form.saving', 'Saving\u2026')\n : t('billing.usage.form.create', 'Record usage')}\n </Button>\n </div>\n </div>\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|