@86d-store/multi-currency 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +234 -0
  2. package/dist/admin/components/_shared.d.ts +28 -0
  3. package/dist/admin/components/_shared.d.ts.map +1 -0
  4. package/dist/admin/components/_shared.js +15 -0
  5. package/dist/admin/components/currency-detail.d.ts +6 -0
  6. package/dist/admin/components/currency-detail.d.ts.map +1 -0
  7. package/dist/admin/components/currency-detail.js +26 -0
  8. package/dist/admin/components/currency-form.d.ts +6 -0
  9. package/dist/admin/components/currency-form.d.ts.map +1 -0
  10. package/dist/admin/components/currency-form.js +95 -0
  11. package/dist/admin/components/currency-list.d.ts +2 -0
  12. package/dist/admin/components/currency-list.d.ts.map +1 -0
  13. package/dist/admin/components/currency-list.js +35 -0
  14. package/dist/admin/endpoints/bulk-update-rates.d.ts +15 -0
  15. package/dist/admin/endpoints/bulk-update-rates.d.ts.map +1 -0
  16. package/dist/admin/endpoints/bulk-update-rates.js +17 -0
  17. package/dist/admin/endpoints/create-currency.d.ts +34 -0
  18. package/dist/admin/endpoints/create-currency.d.ts.map +1 -0
  19. package/dist/admin/endpoints/create-currency.js +34 -0
  20. package/dist/admin/endpoints/delete-currency.d.ts +16 -0
  21. package/dist/admin/endpoints/delete-currency.d.ts.map +1 -0
  22. package/dist/admin/endpoints/delete-currency.js +17 -0
  23. package/dist/admin/endpoints/delete-price-override.d.ts +10 -0
  24. package/dist/admin/endpoints/delete-price-override.d.ts.map +1 -0
  25. package/dist/admin/endpoints/delete-price-override.js +11 -0
  26. package/dist/admin/endpoints/get-currency.d.ts +16 -0
  27. package/dist/admin/endpoints/get-currency.d.ts.map +1 -0
  28. package/dist/admin/endpoints/get-currency.js +14 -0
  29. package/dist/admin/endpoints/list-currencies.d.ts +6 -0
  30. package/dist/admin/endpoints/list-currencies.d.ts.map +1 -0
  31. package/dist/admin/endpoints/list-currencies.js +7 -0
  32. package/dist/admin/endpoints/list-price-overrides.d.ts +10 -0
  33. package/dist/admin/endpoints/list-price-overrides.d.ts.map +1 -0
  34. package/dist/admin/endpoints/list-price-overrides.js +11 -0
  35. package/dist/admin/endpoints/rate-history.d.ts +11 -0
  36. package/dist/admin/endpoints/rate-history.d.ts.map +1 -0
  37. package/dist/admin/endpoints/rate-history.js +17 -0
  38. package/dist/admin/endpoints/routes.d.ts +180 -0
  39. package/dist/admin/endpoints/routes.d.ts.map +1 -0
  40. package/dist/admin/endpoints/routes.js +26 -0
  41. package/dist/admin/endpoints/set-base-currency.d.ts +16 -0
  42. package/dist/admin/endpoints/set-base-currency.d.ts.map +1 -0
  43. package/dist/admin/endpoints/set-base-currency.js +14 -0
  44. package/dist/admin/endpoints/set-price-override.d.ts +13 -0
  45. package/dist/admin/endpoints/set-price-override.d.ts.map +1 -0
  46. package/dist/admin/endpoints/set-price-override.js +16 -0
  47. package/dist/admin/endpoints/update-currency.d.ts +35 -0
  48. package/dist/admin/endpoints/update-currency.d.ts.map +1 -0
  49. package/dist/admin/endpoints/update-currency.js +27 -0
  50. package/dist/admin/endpoints/update-rate.d.ts +18 -0
  51. package/dist/admin/endpoints/update-rate.d.ts.map +1 -0
  52. package/dist/admin/endpoints/update-rate.js +22 -0
  53. package/dist/capabilities.d.ts +56 -0
  54. package/dist/capabilities.d.ts.map +1 -0
  55. package/dist/capabilities.js +16 -0
  56. package/dist/index.d.ts +16 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +69 -0
  59. package/dist/schema.d.ts +94 -0
  60. package/dist/schema.d.ts.map +1 -0
  61. package/dist/schema.js +50 -0
  62. package/dist/service-impl.d.ts +5 -0
  63. package/dist/service-impl.d.ts.map +1 -0
  64. package/dist/service-impl.js +369 -0
  65. package/dist/service.d.ts +139 -0
  66. package/dist/service.d.ts.map +1 -0
  67. package/dist/service.js +1 -0
  68. package/dist/store/components/_hooks.d.ts +6 -0
  69. package/dist/store/components/_hooks.d.ts.map +1 -0
  70. package/dist/store/components/_hooks.js +10 -0
  71. package/dist/store/components/currency-selector.d.ts +10 -0
  72. package/dist/store/components/currency-selector.d.ts.map +1 -0
  73. package/dist/store/components/currency-selector.js +22 -0
  74. package/dist/store/components/currency-selector.mdx +55 -0
  75. package/dist/store/components/mdx.d.ts +8 -0
  76. package/dist/store/components/mdx.d.ts.map +1 -0
  77. package/dist/store/components/mdx.js +7 -0
  78. package/dist/store/components/price-display.d.ts +14 -0
  79. package/dist/store/components/price-display.d.ts.map +1 -0
  80. package/dist/store/components/price-display.js +30 -0
  81. package/dist/store/components/price-display.mdx +12 -0
  82. package/dist/store/endpoints/convert-price.d.ts +22 -0
  83. package/dist/store/endpoints/convert-price.d.ts.map +1 -0
  84. package/dist/store/endpoints/convert-price.js +26 -0
  85. package/dist/store/endpoints/get-product-price.d.ts +22 -0
  86. package/dist/store/endpoints/get-product-price.d.ts.map +1 -0
  87. package/dist/store/endpoints/get-product-price.js +26 -0
  88. package/dist/store/endpoints/list-currencies.d.ts +6 -0
  89. package/dist/store/endpoints/list-currencies.d.ts.map +1 -0
  90. package/dist/store/endpoints/list-currencies.js +7 -0
  91. package/dist/store/endpoints/routes.d.ts +48 -0
  92. package/dist/store/endpoints/routes.d.ts.map +1 -0
  93. package/dist/store/endpoints/routes.js +8 -0
  94. package/package.json +108 -0
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ <p align="center">
2
+ <a href="https://86d.app">
3
+ <img src="https://86d.app/icon" height="96" alt="86d" />
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ The Modern Foundation for Commerce
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://x.com/86d_app"><strong>X</strong></a> ·
13
+ <a href="https://www.linkedin.com/company/86d"><strong>LinkedIn</strong></a>
14
+ </p>
15
+ <br/>
16
+
17
+ > [!WARNING]
18
+ > This project is under active development and is not ready for production use. Please proceed with caution. Use at your own risk.
19
+
20
+ 📚 **Documentation:** [86d.app/docs/modules/multi-currency](https://86d.app/docs/modules/multi-currency)
21
+
22
+ # @86d-store/multi-currency
23
+
24
+ Multi-currency support for 86d commerce stores. Manage currencies, exchange rates, price conversions, and per-product price overrides.
25
+
26
+ ## Installation
27
+
28
+ The module is included in the 86d monorepo. Enable it in your store's template `config.json`:
29
+
30
+ ```json
31
+ {
32
+ "modules": ["multi-currency"]
33
+ }
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```typescript
39
+ import multiCurrency from "@86d-store/multi-currency";
40
+
41
+ // Register with options
42
+ multiCurrency({
43
+ baseCurrency: "USD",
44
+ });
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ | Option | Type | Default | Description |
50
+ |--------|------|---------|-------------|
51
+ | `baseCurrency` | `string` | `"USD"` | Default base currency ISO 4217 code |
52
+
53
+ ## Store Endpoints
54
+
55
+ | Endpoint | Method | Description |
56
+ |----------|--------|-------------|
57
+ | `/currencies` | GET | List all active currencies |
58
+ | `/currencies/convert` | POST | Convert an amount between currencies |
59
+ | `/currencies/product-price` | POST | Get a product's price in a specific currency |
60
+
61
+ ### Convert Price
62
+
63
+ ```json
64
+ POST /currencies/convert
65
+ {
66
+ "amount": 100,
67
+ "to": "EUR",
68
+ "from": "USD" // optional, defaults to base currency
69
+ }
70
+ // Response: { "amount": 85, "formatted": "85,00EUR", "currency": "EUR" }
71
+ ```
72
+
73
+ ### Get Product Price
74
+
75
+ ```json
76
+ POST /currencies/product-price
77
+ {
78
+ "productId": "prod-123",
79
+ "basePriceInCents": 9999,
80
+ "currencyCode": "EUR"
81
+ }
82
+ // Response: { "amount": 8499, "formatted": "84,99EUR", "currency": "EUR" }
83
+ ```
84
+
85
+ ## Admin Endpoints
86
+
87
+ | Endpoint | Method | Description |
88
+ |----------|--------|-------------|
89
+ | `/admin/currencies` | GET | List all currencies |
90
+ | `/admin/currencies/create` | POST | Create a new currency |
91
+ | `/admin/currencies/:id` | GET | Get currency by ID |
92
+ | `/admin/currencies/:id/update` | POST | Update a currency |
93
+ | `/admin/currencies/:id/delete` | POST | Delete a currency |
94
+ | `/admin/currencies/:id/set-base` | POST | Set as base currency |
95
+ | `/admin/currencies/update-rate` | POST | Update a single exchange rate |
96
+ | `/admin/currencies/bulk-update-rates` | POST | Update multiple exchange rates |
97
+ | `/admin/currencies/rate-history` | POST | Get exchange rate history |
98
+ | `/admin/currencies/price-override` | POST | Set a product price override |
99
+ | `/admin/currencies/price-overrides/:productId` | GET | List overrides for a product |
100
+ | `/admin/currencies/price-overrides/:id/delete` | POST | Delete a price override |
101
+
102
+ ## Store Components
103
+
104
+ The module exports customer-facing components for MDX templates:
105
+
106
+ ### CurrencySelector
107
+
108
+ Dropdown to switch the active currency. Hidden when only one currency is active.
109
+
110
+ ```mdx
111
+ <CurrencySelector value={selectedCurrency} onChange={setCurrency} />
112
+ <CurrencySelector compact />
113
+ ```
114
+
115
+ | Prop | Type | Default | Description |
116
+ |------|------|---------|-------------|
117
+ | `value` | `string` | — | Currently selected currency code (ISO 4217) |
118
+ | `onChange` | `(code: string) => void` | — | Called when user selects a different currency |
119
+ | `compact` | `boolean` | `false` | Show code only (no full name) |
120
+
121
+ ### PriceDisplay
122
+
123
+ Show a price converted to the selected currency, with optional compare-at (sale) display.
124
+
125
+ ```mdx
126
+ <PriceDisplay productId="prod-123" basePriceInCents={9999} currencyCode="EUR" />
127
+ ```
128
+
129
+ | Prop | Type | Default | Description |
130
+ |------|------|---------|-------------|
131
+ | `productId` | `string` | — | Product ID (used for price override lookup) |
132
+ | `basePriceInCents` | `number` | **required** | Base price in smallest currency unit |
133
+ | `currencyCode` | `string` | — | Target currency code |
134
+ | `compareAtPriceInCents` | `number` | — | Compare-at price for sale display |
135
+ | `className` | `string` | — | Additional CSS class |
136
+
137
+ ## Admin UI
138
+
139
+ The module includes admin UI components in `src/admin/components/index.tsx` (client components using `useModuleClient`):
140
+
141
+ | Page | Component | Description |
142
+ |------|-----------|-------------|
143
+ | `/admin/currencies` | `CurrencyList` | Currency list with exchange rates and status indicators |
144
+ | `/admin/currencies/new` | `CurrencyForm` | Create a new currency |
145
+ | `/admin/currencies/:id` | `CurrencyDetail` | Currency detail with rate history and price overrides |
146
+ | `/admin/currencies/:id/edit` | `CurrencyForm` | Edit an existing currency |
147
+
148
+ ## Controller API
149
+
150
+ Access via `ctx.context.controllers.multiCurrency`:
151
+
152
+ ### Currency Management
153
+
154
+ - `create(params)` — Create a currency (code is auto-uppercased)
155
+ - `getById(id)` / `getByCode(code)` — Lookup
156
+ - `update(id, params)` — Update currency details
157
+ - `delete(id)` — Delete (cannot delete base currency)
158
+ - `list({ activeOnly? })` — List currencies sorted by `sortOrder`
159
+
160
+ ### Base Currency
161
+
162
+ - `getBaseCurrency()` — Get the store's base currency
163
+ - `setBaseCurrency(id)` — Set a new base currency (unsets previous)
164
+
165
+ ### Exchange Rates
166
+
167
+ - `updateRate({ currencyCode, rate, source? })` — Update rate and record history
168
+ - `bulkUpdateRates(rates[])` — Update multiple rates at once
169
+ - `getRateHistory({ currencyCode, limit? })` — Get historical rates
170
+
171
+ ### Price Conversion
172
+
173
+ - `convert({ amount, to, from? })` — Convert between currencies
174
+ - `formatPrice(amount, currencyCode)` — Format with locale settings
175
+ - `getProductPrice({ productId, basePriceInCents, currencyCode })` — Resolve best price
176
+
177
+ ### Price Overrides
178
+
179
+ - `setPriceOverride({ productId, currencyCode, price, compareAtPrice? })` — Set fixed price
180
+ - `getPriceOverride(productId, currencyCode)` — Get override
181
+ - `listPriceOverrides(productId)` — List all overrides for a product
182
+ - `deletePriceOverride(id)` — Remove an override
183
+
184
+ ## Types
185
+
186
+ ```typescript
187
+ interface Currency {
188
+ id: string;
189
+ code: string; // ISO 4217 (e.g., "USD")
190
+ name: string; // Display name
191
+ symbol: string; // Currency symbol
192
+ decimalPlaces: number; // 0 for JPY, 2 for USD
193
+ exchangeRate: number; // Relative to base (base = 1)
194
+ isBase: boolean;
195
+ isActive: boolean;
196
+ symbolPosition: "before" | "after";
197
+ thousandsSeparator: string;
198
+ decimalSeparator: string;
199
+ roundingMode: "round" | "ceil" | "floor";
200
+ sortOrder: number;
201
+ }
202
+
203
+ interface PriceOverride {
204
+ id: string;
205
+ productId: string;
206
+ currencyCode: string;
207
+ price: number; // In smallest unit (cents)
208
+ compareAtPrice?: number;
209
+ }
210
+
211
+ interface ConvertedPrice {
212
+ amount: number;
213
+ currency: Currency;
214
+ formatted: string;
215
+ }
216
+ ```
217
+
218
+ ## Events
219
+
220
+ | Event | Payload | Description |
221
+ |-------|---------|-------------|
222
+ | `currency.created` | `{ id, code }` | New currency added |
223
+ | `currency.updated` | `{ id, code }` | Currency details changed |
224
+ | `currency.deleted` | `{ id, code }` | Currency removed |
225
+ | `currency.baseChanged` | `{ id, code }` | Base currency switched |
226
+ | `currency.rateUpdated` | `{ code, oldRate, newRate, source }` | Exchange rate changed |
227
+
228
+ ## Notes
229
+
230
+ - Only one base currency per store. Base rate is always 1.
231
+ - Cross-currency conversion goes through the base rate (EUR -> USD -> GBP).
232
+ - Price overrides take priority over automatic conversion.
233
+ - Zero-decimal currencies (JPY, KRW) are fully supported via `decimalPlaces: 0`.
234
+ - Exchange rate history is recorded automatically on every rate update.
@@ -0,0 +1,28 @@
1
+ export declare function useCurrencyApi(): {
2
+ listCurrencies: import("@86d-store/core/client/types").AnyEndpointHook;
3
+ getCurrency: import("@86d-store/core/client/types").AnyEndpointHook;
4
+ createCurrency: import("@86d-store/core/client/types").AnyEndpointHook;
5
+ updateCurrency: import("@86d-store/core/client/types").AnyEndpointHook;
6
+ deleteCurrency: import("@86d-store/core/client/types").AnyEndpointHook;
7
+ setBase: import("@86d-store/core/client/types").AnyEndpointHook;
8
+ updateRate: import("@86d-store/core/client/types").AnyEndpointHook;
9
+ rateHistory: import("@86d-store/core/client/types").AnyEndpointHook;
10
+ };
11
+ export interface Currency {
12
+ id: string;
13
+ code: string;
14
+ name: string;
15
+ symbol: string;
16
+ decimalPlaces: number;
17
+ exchangeRate: number;
18
+ isBase: boolean;
19
+ isActive: boolean;
20
+ symbolPosition: string;
21
+ thousandsSeparator?: string;
22
+ decimalSeparator?: string;
23
+ roundingMode: string;
24
+ sortOrder: number;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ }
28
+ //# sourceMappingURL=_shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../../src/admin/components/_shared.tsx"],"names":[],"mappings":"AAIA,wBAAgB,cAAc;;;;;;;;;EAkB7B;AAED,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { useModuleClient } from "@86d-store/core/client/provider";
3
+ export function useCurrencyApi() {
4
+ const client = useModuleClient();
5
+ return {
6
+ listCurrencies: client.module("multi-currency").admin["/admin/currencies"],
7
+ getCurrency: client.module("multi-currency").admin["/admin/currencies/:id"],
8
+ createCurrency: client.module("multi-currency").admin["/admin/currencies/create"],
9
+ updateCurrency: client.module("multi-currency").admin["/admin/currencies/:id/update"],
10
+ deleteCurrency: client.module("multi-currency").admin["/admin/currencies/:id/delete"],
11
+ setBase: client.module("multi-currency").admin["/admin/currencies/:id/set-base"],
12
+ updateRate: client.module("multi-currency").admin["/admin/currencies/update-rate"],
13
+ rateHistory: client.module("multi-currency").admin["/admin/currencies/rate-history"],
14
+ };
15
+ }
@@ -0,0 +1,6 @@
1
+ export declare function CurrencyDetail({ params }: {
2
+ params: {
3
+ id: string;
4
+ };
5
+ }): import("react").JSX.Element;
6
+ //# sourceMappingURL=currency-detail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"currency-detail.d.ts","sourceRoot":"","sources":["../../../src/admin/components/currency-detail.tsx"],"names":[],"mappings":"AAYA,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,+BA0HpE"}
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCurrencyApi } from "./_shared";
4
+ function formatDate(dateStr) {
5
+ return new Date(dateStr).toLocaleDateString(undefined, {
6
+ year: "numeric",
7
+ month: "short",
8
+ day: "numeric",
9
+ });
10
+ }
11
+ export function CurrencyDetail({ params }) {
12
+ const api = useCurrencyApi();
13
+ const { data, isLoading } = api.getCurrency.useQuery({
14
+ params: { id: params.id },
15
+ });
16
+ const currency = data?.currency;
17
+ if (isLoading) {
18
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "h-8 w-48 animate-pulse rounded bg-muted/30" }), _jsx("div", { className: "h-48 animate-pulse rounded-lg border border-border bg-muted/30" })] }));
19
+ }
20
+ if (!currency) {
21
+ return (_jsxs("div", { className: "rounded-lg border border-border bg-card p-8 text-center", children: [_jsx("p", { className: "text-muted-foreground text-sm", children: "Currency not found." }), _jsx("a", { href: "/admin/currencies", className: "mt-2 inline-block text-sm underline", children: "Back to currencies" })] }));
22
+ }
23
+ return (_jsxs("div", { children: [_jsxs("div", { className: "mb-6", children: [_jsx("a", { href: "/admin/currencies", className: "text-muted-foreground text-sm hover:underline", children: "\u2190 Back to currencies" }), _jsxs("div", { className: "mt-2 flex items-center gap-3", children: [_jsxs("h1", { className: "font-bold text-2xl text-foreground", children: [currency.name, " (", currency.code, ")"] }), currency.isBase ? (_jsx("span", { className: "inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 font-medium text-indigo-800 text-xs dark:bg-indigo-900/30 dark:text-indigo-400", children: "Base Currency" })) : null, _jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 font-medium text-xs ${currency.isActive
24
+ ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
25
+ : "bg-muted text-muted-foreground"}`, children: currency.isActive ? "Active" : "Inactive" })] })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-2", children: [_jsxs("div", { className: "rounded-lg border border-border bg-card p-5", children: [_jsx("h2", { className: "mb-4 font-semibold text-foreground text-sm", children: "Details" }), _jsxs("dl", { className: "space-y-3 text-sm", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: "Symbol" }), _jsx("dd", { className: "font-medium text-foreground", children: currency.symbol })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: "Exchange Rate" }), _jsx("dd", { className: "font-mono text-foreground", children: currency.exchangeRate.toFixed(6) })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: "Decimal Places" }), _jsx("dd", { className: "text-foreground", children: currency.decimalPlaces })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: "Symbol Position" }), _jsx("dd", { className: "text-foreground", children: currency.symbolPosition })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: "Rounding" }), _jsx("dd", { className: "text-foreground", children: currency.roundingMode })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: "Sort Order" }), _jsx("dd", { className: "text-foreground", children: currency.sortOrder })] }), _jsxs("div", { className: "flex justify-between", children: [_jsx("dt", { className: "text-muted-foreground", children: "Created" }), _jsx("dd", { className: "text-foreground", children: formatDate(currency.createdAt) })] })] })] }), _jsxs("div", { className: "rounded-lg border border-border bg-card p-5", children: [_jsx("h2", { className: "mb-4 font-semibold text-foreground text-sm", children: "Actions" }), _jsx("div", { className: "space-y-2", children: _jsx("a", { href: `/admin/currencies/${params.id}/edit`, className: "block rounded-lg border border-border px-4 py-2 text-center text-sm hover:bg-muted", children: "Edit Currency" }) })] })] })] }));
26
+ }
@@ -0,0 +1,6 @@
1
+ export declare function CurrencyForm({ params }?: {
2
+ params?: {
3
+ id?: string;
4
+ };
5
+ }): import("react").JSX.Element;
6
+ //# sourceMappingURL=currency-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"currency-form.d.ts","sourceRoot":"","sources":["../../../src/admin/components/currency-form.tsx"],"names":[],"mappings":"AAYA,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAO,+BA6QzE"}
@@ -0,0 +1,95 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { useCurrencyApi } from "./_shared";
5
+ function extractError(err) {
6
+ if (err && typeof err === "object" && "message" in err) {
7
+ return String(err.message);
8
+ }
9
+ return "An unexpected error occurred";
10
+ }
11
+ export function CurrencyForm({ params } = {}) {
12
+ const api = useCurrencyApi();
13
+ const isEdit = Boolean(params?.id);
14
+ const [code, setCode] = useState("");
15
+ const [name, setName] = useState("");
16
+ const [symbol, setSymbol] = useState("");
17
+ const [decimalPlaces, setDecimalPlaces] = useState(2);
18
+ const [exchangeRate, setExchangeRate] = useState(1);
19
+ const [isActive, setIsActive] = useState(true);
20
+ const [symbolPosition, setSymbolPosition] = useState("before");
21
+ const [roundingMode, setRoundingMode] = useState("round");
22
+ const [initialized, setInitialized] = useState(false);
23
+ const [error, setError] = useState("");
24
+ const [saved, setSaved] = useState(false);
25
+ const { data: currencyData, isLoading } = api.getCurrency.useQuery({ params: { id: params?.id ?? "" } }, { enabled: isEdit });
26
+ const currency = currencyData?.currency;
27
+ if (currency && !initialized) {
28
+ setCode(currency.code);
29
+ setName(currency.name);
30
+ setSymbol(currency.symbol);
31
+ setDecimalPlaces(currency.decimalPlaces);
32
+ setExchangeRate(currency.exchangeRate);
33
+ setIsActive(currency.isActive);
34
+ setSymbolPosition(currency.symbolPosition);
35
+ setRoundingMode(currency.roundingMode);
36
+ setInitialized(true);
37
+ }
38
+ const createMutation = api.createCurrency.useMutation();
39
+ const updateMutation = api.updateCurrency.useMutation();
40
+ const handleSubmit = async (e) => {
41
+ e.preventDefault();
42
+ setError("");
43
+ setSaved(false);
44
+ if (!code.trim() || !name.trim() || !symbol.trim()) {
45
+ setError("Code, name, and symbol are required.");
46
+ return;
47
+ }
48
+ try {
49
+ if (isEdit && params?.id) {
50
+ await updateMutation.mutateAsync({
51
+ params: { id: params.id },
52
+ body: {
53
+ name: name.trim(),
54
+ symbol: symbol.trim(),
55
+ decimalPlaces,
56
+ exchangeRate,
57
+ isActive,
58
+ symbolPosition,
59
+ roundingMode,
60
+ },
61
+ });
62
+ setSaved(true);
63
+ }
64
+ else {
65
+ await createMutation.mutateAsync({
66
+ body: {
67
+ code: code.trim().toUpperCase(),
68
+ name: name.trim(),
69
+ symbol: symbol.trim(),
70
+ decimalPlaces,
71
+ exchangeRate,
72
+ isActive,
73
+ symbolPosition,
74
+ roundingMode,
75
+ },
76
+ });
77
+ window.location.href = "/admin/currencies";
78
+ }
79
+ }
80
+ catch (err) {
81
+ setError(extractError(err));
82
+ }
83
+ };
84
+ if (isEdit && isLoading) {
85
+ return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "h-8 w-48 animate-pulse rounded bg-muted/30" }), _jsx("div", { className: "h-64 animate-pulse rounded-lg border border-border bg-muted/30" })] }));
86
+ }
87
+ if (isEdit && !currency && !isLoading) {
88
+ return (_jsxs("div", { className: "rounded-lg border border-border bg-card p-8 text-center", children: [_jsx("p", { className: "text-muted-foreground text-sm", children: "Currency not found." }), _jsx("a", { href: "/admin/currencies", className: "mt-2 inline-block text-sm underline", children: "Back to currencies" })] }));
89
+ }
90
+ return (_jsxs("div", { children: [_jsxs("div", { className: "mb-6", children: [_jsx("a", { href: "/admin/currencies", className: "text-muted-foreground text-sm hover:underline", children: "\u2190 Back to currencies" }), _jsx("h1", { className: "mt-2 font-bold text-2xl text-foreground", children: isEdit ? "Edit Currency" : "Add Currency" })] }), error ? (_jsx("div", { className: "mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-red-800 text-sm dark:border-red-800 dark:bg-red-900/20 dark:text-red-400", children: error })) : null, saved ? (_jsx("div", { className: "mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-green-800 text-sm dark:border-green-800 dark:bg-green-900/20 dark:text-green-400", children: "Currency saved successfully." })) : null, _jsxs("form", { onSubmit: handleSubmit, className: "max-w-2xl space-y-4 rounded-lg border border-border bg-card p-5", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block font-medium text-sm", children: "Code (ISO 4217)" }), _jsx("input", { type: "text", value: code, onChange: (e) => setCode(e.target.value), placeholder: "USD", maxLength: 3, disabled: isEdit, className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm uppercase disabled:opacity-50" })] }), _jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block font-medium text-sm", children: "Name" }), _jsx("input", { type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: "US Dollar", className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm" })] }), _jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block font-medium text-sm", children: "Symbol" }), _jsx("input", { type: "text", value: symbol, onChange: (e) => setSymbol(e.target.value), placeholder: "$", className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm" })] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block font-medium text-sm", children: "Decimal Places" }), _jsx("input", { type: "number", value: decimalPlaces, onChange: (e) => setDecimalPlaces(Number.parseInt(e.target.value, 10) || 0), min: 0, max: 8, className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm" })] }), _jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block font-medium text-sm", children: "Exchange Rate" }), _jsx("input", { type: "number", value: exchangeRate, onChange: (e) => setExchangeRate(Number.parseFloat(e.target.value) || 0), step: "0.000001", min: 0, className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm" })] }), _jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block font-medium text-sm", children: "Symbol Position" }), _jsxs("select", { value: symbolPosition, onChange: (e) => setSymbolPosition(e.target.value), className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm", children: [_jsx("option", { value: "before", children: "Before ($100)" }), _jsx("option", { value: "after", children: "After (100$)" })] })] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("label", { className: "block", children: [_jsx("span", { className: "mb-1 block font-medium text-sm", children: "Rounding Mode" }), _jsxs("select", { value: roundingMode, onChange: (e) => setRoundingMode(e.target.value), className: "w-full rounded-md border border-border bg-background px-3 py-2 text-sm", children: [_jsx("option", { value: "round", children: "Round" }), _jsx("option", { value: "ceil", children: "Ceil" }), _jsx("option", { value: "floor", children: "Floor" })] })] }), _jsxs("label", { className: "flex items-center gap-2 self-end pb-2", children: [_jsx("input", { type: "checkbox", checked: isActive, onChange: (e) => setIsActive(e.target.checked), className: "rounded border-border" }), _jsx("span", { className: "font-medium text-sm", children: "Active" })] })] }), _jsx("button", { type: "submit", disabled: createMutation.isPending || updateMutation.isPending, className: "rounded-lg bg-foreground px-4 py-2 font-medium text-background text-sm hover:opacity-90 disabled:opacity-50", children: createMutation.isPending || updateMutation.isPending
91
+ ? "Saving..."
92
+ : isEdit
93
+ ? "Save Changes"
94
+ : "Create Currency" })] })] }));
95
+ }
@@ -0,0 +1,2 @@
1
+ export declare function CurrencyList(): import("react").JSX.Element;
2
+ //# sourceMappingURL=currency-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"currency-list.d.ts","sourceRoot":"","sources":["../../../src/admin/components/currency-list.tsx"],"names":[],"mappings":"AAIA,wBAAgB,YAAY,gCA2L3B"}
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useCurrencyApi } from "./_shared";
4
+ export function CurrencyList() {
5
+ const api = useCurrencyApi();
6
+ const { data, isLoading } = api.listCurrencies.useQuery({});
7
+ const deleteMutation = api.deleteCurrency.useMutation();
8
+ const setBaseMutation = api.setBase.useMutation();
9
+ const currencies = data?.currencies ?? [];
10
+ const handleDelete = async (id) => {
11
+ if (!confirm("Delete this currency? This cannot be undone."))
12
+ return;
13
+ try {
14
+ await deleteMutation.mutateAsync({ params: { id } });
15
+ window.location.reload();
16
+ }
17
+ catch {
18
+ // silently handled
19
+ }
20
+ };
21
+ const handleSetBase = async (id) => {
22
+ if (!confirm("Set this currency as the base currency?"))
23
+ return;
24
+ try {
25
+ await setBaseMutation.mutateAsync({ params: { id } });
26
+ window.location.reload();
27
+ }
28
+ catch {
29
+ // silently handled
30
+ }
31
+ };
32
+ return (_jsxs("div", { children: [_jsxs("div", { className: "mb-6 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "font-bold text-2xl text-foreground", children: "Currencies" }), _jsx("p", { className: "mt-1 text-muted-foreground text-sm", children: "Manage store currencies and exchange rates" })] }), _jsx("a", { href: "/admin/currencies/new", className: "rounded-lg bg-foreground px-4 py-2 font-medium text-background text-sm hover:opacity-90", children: "Add Currency" })] }), isLoading ? (_jsx("div", { className: "space-y-3", children: ["k0", "k1", "k2"].map((key) => (_jsx("div", { className: "h-16 animate-pulse rounded-lg border border-border bg-muted/30" }, key))) })) : currencies.length === 0 ? (_jsx("div", { className: "rounded-lg border border-border bg-card p-8 text-center", children: _jsx("p", { className: "text-muted-foreground text-sm", children: "No currencies configured. Add your base currency to get started." }) })) : (_jsx("div", { className: "overflow-x-auto rounded-md border border-border", children: _jsxs("table", { className: "w-full text-left text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-border border-b bg-muted", children: [_jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Currency" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Code" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Symbol" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Rate" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Status" }), _jsx("th", { scope: "col", className: "px-4 py-2 font-medium text-muted-foreground", children: "Actions" })] }) }), _jsx("tbody", { className: "divide-y divide-border", children: currencies.map((c) => (_jsxs("tr", { className: "transition-colors hover:bg-muted/50", children: [_jsx("td", { className: "px-4 py-2 text-foreground", children: _jsxs("div", { className: "flex items-center gap-2", children: [c.name, c.isBase ? (_jsx("span", { className: "inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 font-medium text-indigo-800 text-xs dark:bg-indigo-900/30 dark:text-indigo-400", children: "Base" })) : null] }) }), _jsx("td", { className: "px-4 py-2 font-mono text-foreground text-xs", children: c.code }), _jsx("td", { className: "px-4 py-2 text-foreground", children: c.symbol }), _jsx("td", { className: "px-4 py-2 font-mono text-foreground text-xs", children: c.isBase ? "1.000000" : c.exchangeRate.toFixed(6) }), _jsx("td", { className: "px-4 py-2", children: _jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 font-medium text-xs ${c.isActive
33
+ ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
34
+ : "bg-muted text-muted-foreground"}`, children: c.isActive ? "Active" : "Inactive" }) }), _jsx("td", { className: "px-4 py-2", children: _jsxs("div", { className: "flex gap-1", children: [_jsx("a", { href: `/admin/currencies/${c.id}`, className: "rounded px-2 py-1 text-xs hover:bg-muted", children: "View" }), _jsx("a", { href: `/admin/currencies/${c.id}/edit`, className: "rounded px-2 py-1 text-xs hover:bg-muted", children: "Edit" }), !c.isBase ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => handleSetBase(c.id), className: "rounded px-2 py-1 text-xs hover:bg-muted", children: "Set Base" }), _jsx("button", { type: "button", onClick: () => handleDelete(c.id), className: "rounded px-2 py-1 text-red-600 text-xs hover:bg-red-50 dark:hover:bg-red-900/20", children: "Delete" })] })) : null] }) })] }, c.id))) })] }) }))] }));
35
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ export declare const adminBulkUpdateRates: import("better-call").StrictEndpoint<"/admin/currencies/bulk-update-rates", {
3
+ method: "POST";
4
+ body: z.ZodObject<{
5
+ rates: z.ZodArray<z.ZodObject<{
6
+ currencyCode: z.ZodString;
7
+ rate: z.ZodNumber;
8
+ source: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>>;
10
+ }, z.core.$strip>;
11
+ }, {
12
+ updated: number;
13
+ errors: string[];
14
+ }>;
15
+ //# sourceMappingURL=bulk-update-rates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulk-update-rates.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/bulk-update-rates.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAoBhC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { createAdminEndpoint } from "@86d-store/core/api";
2
+ import { z } from "zod";
3
+ export const adminBulkUpdateRates = createAdminEndpoint("/admin/currencies/bulk-update-rates", {
4
+ method: "POST",
5
+ body: z.object({
6
+ rates: z.array(z.object({
7
+ currencyCode: z.string().min(3).max(3),
8
+ rate: z.number().positive(),
9
+ source: z.string().max(100).optional(),
10
+ })),
11
+ }),
12
+ }, async (ctx) => {
13
+ const controller = ctx.context.controllers
14
+ .multiCurrency;
15
+ const result = await controller.bulkUpdateRates(ctx.body.rates);
16
+ return result;
17
+ });
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ export declare const adminCreateCurrency: import("better-call").StrictEndpoint<"/admin/currencies/create", {
3
+ method: "POST";
4
+ body: z.ZodObject<{
5
+ code: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
6
+ name: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
7
+ symbol: z.ZodString;
8
+ decimalPlaces: z.ZodOptional<z.ZodNumber>;
9
+ exchangeRate: z.ZodOptional<z.ZodNumber>;
10
+ isBase: z.ZodOptional<z.ZodBoolean>;
11
+ isActive: z.ZodOptional<z.ZodBoolean>;
12
+ symbolPosition: z.ZodOptional<z.ZodEnum<{
13
+ before: "before";
14
+ after: "after";
15
+ }>>;
16
+ thousandsSeparator: z.ZodOptional<z.ZodString>;
17
+ decimalSeparator: z.ZodOptional<z.ZodString>;
18
+ roundingMode: z.ZodOptional<z.ZodEnum<{
19
+ round: "round";
20
+ ceil: "ceil";
21
+ floor: "floor";
22
+ }>>;
23
+ sortOrder: z.ZodOptional<z.ZodNumber>;
24
+ }, z.core.$strip>;
25
+ }, {
26
+ error: string;
27
+ status: number;
28
+ currency?: never;
29
+ } | {
30
+ currency: import("../..").Currency;
31
+ error?: never;
32
+ status?: never;
33
+ }>;
34
+ //# sourceMappingURL=create-currency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-currency.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/create-currency.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoC/B,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { createAdminEndpoint } from "@86d-store/core/api";
2
+ import { sanitizeText } from "@86d-store/core/sanitize";
3
+ import { z } from "zod";
4
+ export const adminCreateCurrency = createAdminEndpoint("/admin/currencies/create", {
5
+ method: "POST",
6
+ body: z.object({
7
+ code: z
8
+ .string()
9
+ .min(3)
10
+ .max(3)
11
+ .transform((s) => s.toUpperCase()),
12
+ name: z.string().min(1).max(100).transform(sanitizeText),
13
+ symbol: z.string().min(1).max(10),
14
+ decimalPlaces: z.number().int().min(0).max(8).optional(),
15
+ exchangeRate: z.number().positive().optional(),
16
+ isBase: z.boolean().optional(),
17
+ isActive: z.boolean().optional(),
18
+ symbolPosition: z.enum(["before", "after"]).optional(),
19
+ thousandsSeparator: z.string().max(5).optional(),
20
+ decimalSeparator: z.string().max(5).optional(),
21
+ roundingMode: z.enum(["round", "ceil", "floor"]).optional(),
22
+ sortOrder: z.number().int().optional(),
23
+ }),
24
+ }, async (ctx) => {
25
+ const controller = ctx.context.controllers
26
+ .multiCurrency;
27
+ // Check for duplicate code
28
+ const existing = await controller.getByCode(ctx.body.code);
29
+ if (existing) {
30
+ return { error: "Currency with this code already exists", status: 409 };
31
+ }
32
+ const currency = await controller.create(ctx.body);
33
+ return { currency };
34
+ });
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ export declare const adminDeleteCurrency: import("better-call").StrictEndpoint<"/admin/currencies/:id/delete", {
3
+ method: "POST";
4
+ params: z.ZodObject<{
5
+ id: z.ZodString;
6
+ }, z.core.$strip>;
7
+ }, {
8
+ error: string;
9
+ status: number;
10
+ success?: never;
11
+ } | {
12
+ success: boolean;
13
+ error?: never;
14
+ status?: never;
15
+ }>;
16
+ //# sourceMappingURL=delete-currency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-currency.d.ts","sourceRoot":"","sources":["../../../src/admin/endpoints/delete-currency.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;EAkB/B,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { createAdminEndpoint } from "@86d-store/core/api";
2
+ import { z } from "zod";
3
+ export const adminDeleteCurrency = createAdminEndpoint("/admin/currencies/:id/delete", {
4
+ method: "POST",
5
+ params: z.object({ id: z.string() }),
6
+ }, async (ctx) => {
7
+ const controller = ctx.context.controllers
8
+ .multiCurrency;
9
+ const result = await controller.delete(ctx.params.id);
10
+ if (!result.deleted) {
11
+ return {
12
+ error: result.error ?? "Failed to delete currency",
13
+ status: 400,
14
+ };
15
+ }
16
+ return { success: true };
17
+ });