@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/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ import { adminEndpoints } from "./admin/endpoints/routes";
2
+ import { productPriceConversionProvider } from "./capabilities";
3
+ import { multiCurrencyStorage } from "./schema";
4
+ import { createMultiCurrencyController } from "./service-impl";
5
+ import { storeEndpoints } from "./store/endpoints/routes";
6
+ /**
7
+ * Multi-currency module factory.
8
+ * Provides currency management, exchange rate tracking, price conversion,
9
+ * and per-product price overrides for international commerce.
10
+ */
11
+ export default function multiCurrency(options) {
12
+ return {
13
+ id: "multi-currency",
14
+ version: "0.0.1",
15
+ storage: multiCurrencyStorage,
16
+ capabilities: { provides: [productPriceConversionProvider] },
17
+ exports: {
18
+ read: [
19
+ "currencyCode",
20
+ "exchangeRate",
21
+ "formattedPrice",
22
+ "convertedAmount",
23
+ ],
24
+ },
25
+ events: {
26
+ emits: [
27
+ "currency.created",
28
+ "currency.updated",
29
+ "currency.deleted",
30
+ "currency.baseChanged",
31
+ "currency.rateUpdated",
32
+ ],
33
+ },
34
+ init: async (ctx) => {
35
+ const controller = createMultiCurrencyController(ctx.data, ctx.events);
36
+ return {
37
+ controllers: { multiCurrency: controller },
38
+ };
39
+ },
40
+ endpoints: {
41
+ store: storeEndpoints,
42
+ admin: adminEndpoints,
43
+ },
44
+ admin: {
45
+ pages: [
46
+ {
47
+ path: "/admin/currencies",
48
+ component: "CurrencyList",
49
+ label: "Currencies",
50
+ icon: "Coins",
51
+ group: "Finance",
52
+ },
53
+ {
54
+ path: "/admin/currencies/new",
55
+ component: "CurrencyForm",
56
+ },
57
+ {
58
+ path: "/admin/currencies/:id",
59
+ component: "CurrencyDetail",
60
+ },
61
+ {
62
+ path: "/admin/currencies/:id/edit",
63
+ component: "CurrencyForm",
64
+ },
65
+ ],
66
+ },
67
+ options,
68
+ };
69
+ }
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ export declare const multiCurrencyCurrencyShape: z.ZodObject<{
3
+ id: z.ZodString;
4
+ code: z.ZodString;
5
+ name: z.ZodString;
6
+ symbol: z.ZodString;
7
+ decimalPlaces: z.ZodDefault<z.ZodInt>;
8
+ exchangeRate: z.ZodDefault<z.ZodInt>;
9
+ isBase: z.ZodDefault<z.ZodBoolean>;
10
+ isActive: z.ZodDefault<z.ZodBoolean>;
11
+ symbolPosition: z.ZodDefault<z.ZodEnum<{
12
+ before: "before";
13
+ after: "after";
14
+ }>>;
15
+ thousandsSeparator: z.ZodDefault<z.ZodString>;
16
+ decimalSeparator: z.ZodDefault<z.ZodString>;
17
+ roundingMode: z.ZodDefault<z.ZodEnum<{
18
+ round: "round";
19
+ ceil: "ceil";
20
+ floor: "floor";
21
+ }>>;
22
+ sortOrder: z.ZodDefault<z.ZodInt>;
23
+ createdAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
24
+ updatedAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
25
+ }, z.core.$strip>;
26
+ export declare const multiCurrencyExchangeRateHistoryShape: z.ZodObject<{
27
+ id: z.ZodString;
28
+ currencyCode: z.ZodString;
29
+ rate: z.ZodNumber;
30
+ source: z.ZodDefault<z.ZodString>;
31
+ recordedAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
32
+ }, z.core.$strip>;
33
+ export declare const multiCurrencyPriceOverrideShape: z.ZodObject<{
34
+ id: z.ZodString;
35
+ productId: z.ZodString;
36
+ currencyCode: z.ZodString;
37
+ price: z.ZodNumber;
38
+ compareAtPrice: z.ZodOptional<z.ZodNumber>;
39
+ createdAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
40
+ updatedAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
41
+ }, z.core.$strip>;
42
+ /** Native Relational storage for multi-currency. */
43
+ export declare const multiCurrencyStorage: {
44
+ readonly kind: "relational";
45
+ readonly tables: {
46
+ readonly currency: {
47
+ readonly shape: z.ZodObject<{
48
+ id: z.ZodString;
49
+ code: z.ZodString;
50
+ name: z.ZodString;
51
+ symbol: z.ZodString;
52
+ decimalPlaces: z.ZodDefault<z.ZodInt>;
53
+ exchangeRate: z.ZodDefault<z.ZodInt>;
54
+ isBase: z.ZodDefault<z.ZodBoolean>;
55
+ isActive: z.ZodDefault<z.ZodBoolean>;
56
+ symbolPosition: z.ZodDefault<z.ZodEnum<{
57
+ before: "before";
58
+ after: "after";
59
+ }>>;
60
+ thousandsSeparator: z.ZodDefault<z.ZodString>;
61
+ decimalSeparator: z.ZodDefault<z.ZodString>;
62
+ roundingMode: z.ZodDefault<z.ZodEnum<{
63
+ round: "round";
64
+ ceil: "ceil";
65
+ floor: "floor";
66
+ }>>;
67
+ sortOrder: z.ZodDefault<z.ZodInt>;
68
+ createdAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
69
+ updatedAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
70
+ }, z.core.$strip>;
71
+ };
72
+ readonly exchangeRateHistory: {
73
+ readonly shape: z.ZodObject<{
74
+ id: z.ZodString;
75
+ currencyCode: z.ZodString;
76
+ rate: z.ZodNumber;
77
+ source: z.ZodDefault<z.ZodString>;
78
+ recordedAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
79
+ }, z.core.$strip>;
80
+ };
81
+ readonly priceOverride: {
82
+ readonly shape: z.ZodObject<{
83
+ id: z.ZodString;
84
+ productId: z.ZodString;
85
+ currencyCode: z.ZodString;
86
+ price: z.ZodNumber;
87
+ compareAtPrice: z.ZodOptional<z.ZodNumber>;
88
+ createdAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
89
+ updatedAt: z.ZodDefault<z.ZodCoercedDate<unknown>>;
90
+ }, z.core.$strip>;
91
+ };
92
+ };
93
+ };
94
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;iBAgBrC,CAAC;AAEH,eAAO,MAAM,qCAAqC;;;;;;iBAMhD,CAAC;AAEH,eAAO,MAAM,+BAA+B;;;;;;;;iBAQ1C,CAAC;AAEH,oDAAoD;AACpD,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAaY,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,50 @@
1
+ import { col } from "@86d-store/core/schema/col";
2
+ import { z } from "zod";
3
+ export const multiCurrencyCurrencyShape = z.object({
4
+ id: z.string().register(col, { pk: true }),
5
+ code: z.string().register(col, { unique: true }),
6
+ name: z.string(),
7
+ symbol: z.string(),
8
+ decimalPlaces: z.int().default(2),
9
+ exchangeRate: z.int().default(1),
10
+ isBase: z.boolean().default(false),
11
+ isActive: z.boolean().default(true),
12
+ symbolPosition: z.enum(["before", "after"]).default("before"),
13
+ thousandsSeparator: z.string().default(","),
14
+ decimalSeparator: z.string().default("."),
15
+ roundingMode: z.enum(["round", "ceil", "floor"]).default("round"),
16
+ sortOrder: z.int().default(0),
17
+ createdAt: z.coerce.date().default(() => new Date()),
18
+ updatedAt: z.coerce.date().default(() => new Date()),
19
+ });
20
+ export const multiCurrencyExchangeRateHistoryShape = z.object({
21
+ id: z.string().register(col, { pk: true }),
22
+ currencyCode: z.string(),
23
+ rate: z.number(),
24
+ source: z.string().default("manual"),
25
+ recordedAt: z.coerce.date().default(() => new Date()),
26
+ });
27
+ export const multiCurrencyPriceOverrideShape = z.object({
28
+ id: z.string().register(col, { pk: true }),
29
+ productId: z.string(),
30
+ currencyCode: z.string(),
31
+ price: z.number(),
32
+ compareAtPrice: z.number().optional(),
33
+ createdAt: z.coerce.date().default(() => new Date()),
34
+ updatedAt: z.coerce.date().default(() => new Date()),
35
+ });
36
+ /** Native Relational storage for multi-currency. */
37
+ export const multiCurrencyStorage = {
38
+ kind: "relational",
39
+ tables: {
40
+ currency: {
41
+ shape: multiCurrencyCurrencyShape,
42
+ },
43
+ exchangeRateHistory: {
44
+ shape: multiCurrencyExchangeRateHistoryShape,
45
+ },
46
+ priceOverride: {
47
+ shape: multiCurrencyPriceOverrideShape,
48
+ },
49
+ },
50
+ };
@@ -0,0 +1,5 @@
1
+ import type { ScopedEventEmitter } from "@86d-store/core/events";
2
+ import type { ModuleDataService } from "@86d-store/core/types/module";
3
+ import type { MultiCurrencyController } from "./service";
4
+ export declare function createMultiCurrencyController(data: ModuleDataService, events?: ScopedEventEmitter): MultiCurrencyController;
5
+ //# sourceMappingURL=service-impl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-impl.d.ts","sourceRoot":"","sources":["../src/service-impl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,KAAK,EAIX,uBAAuB,EAGvB,MAAM,WAAW,CAAC;AAyCnB,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,iBAAiB,EACvB,MAAM,CAAC,EAAE,kBAAkB,GACzB,uBAAuB,CAibzB"}
@@ -0,0 +1,369 @@
1
+ function applyRounding(amount, decimalPlaces, mode) {
2
+ const factor = 10 ** decimalPlaces;
3
+ switch (mode) {
4
+ case "ceil":
5
+ return Math.ceil(amount * factor) / factor;
6
+ case "floor":
7
+ return Math.floor(amount * factor) / factor;
8
+ default:
9
+ return Math.round(amount * factor) / factor;
10
+ }
11
+ }
12
+ function formatAmount(amount, currency) {
13
+ const fixed = amount.toFixed(currency.decimalPlaces);
14
+ const [intPart, decPart] = fixed.split(".");
15
+ // Apply thousands separator
16
+ let formattedInt = intPart;
17
+ if (currency.thousandsSeparator) {
18
+ formattedInt = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, currency.thousandsSeparator);
19
+ }
20
+ const formattedNumber = currency.decimalPlaces > 0
21
+ ? `${formattedInt}${currency.decimalSeparator}${decPart}`
22
+ : formattedInt;
23
+ return currency.symbolPosition === "before"
24
+ ? `${currency.symbol}${formattedNumber}`
25
+ : `${formattedNumber}${currency.symbol}`;
26
+ }
27
+ export function createMultiCurrencyController(data, events) {
28
+ async function getCurrencyByCode(code) {
29
+ const results = (await data.findMany("currency", {
30
+ where: { code: code.toUpperCase() },
31
+ take: 1,
32
+ }));
33
+ return results[0] ?? null;
34
+ }
35
+ return {
36
+ async create(params) {
37
+ const id = crypto.randomUUID();
38
+ const now = new Date();
39
+ const code = params.code.toUpperCase();
40
+ // If this is set as base, ensure rate is 1
41
+ const isBase = params.isBase ?? false;
42
+ const exchangeRate = isBase ? 1 : (params.exchangeRate ?? 1);
43
+ // If setting as base, unset any existing base
44
+ if (isBase) {
45
+ const existing = (await data.findMany("currency", {
46
+ where: { isBase: true },
47
+ }));
48
+ for (const c of existing) {
49
+ const record = { ...c, isBase: false, updatedAt: now };
50
+ await data.upsert("currency", c.id, record);
51
+ }
52
+ }
53
+ const currency = {
54
+ id,
55
+ code,
56
+ name: params.name,
57
+ symbol: params.symbol,
58
+ decimalPlaces: params.decimalPlaces ?? 2,
59
+ exchangeRate,
60
+ isBase,
61
+ isActive: params.isActive ?? true,
62
+ symbolPosition: params.symbolPosition ?? "before",
63
+ thousandsSeparator: params.thousandsSeparator ?? ",",
64
+ decimalSeparator: params.decimalSeparator ?? ".",
65
+ roundingMode: params.roundingMode ?? "round",
66
+ sortOrder: params.sortOrder ?? 0,
67
+ createdAt: now,
68
+ updatedAt: now,
69
+ };
70
+ await data.upsert("currency", id, currency);
71
+ if (events) {
72
+ void events.emit("currency.created", {
73
+ id: currency.id,
74
+ code: currency.code,
75
+ });
76
+ }
77
+ return currency;
78
+ },
79
+ async getById(id) {
80
+ return (await data.get("currency", id));
81
+ },
82
+ async getByCode(code) {
83
+ return getCurrencyByCode(code);
84
+ },
85
+ async update(id, params) {
86
+ const existing = (await data.get("currency", id));
87
+ if (!existing)
88
+ return null;
89
+ const updated = {
90
+ ...existing,
91
+ ...(params.name !== undefined ? { name: params.name } : {}),
92
+ ...(params.symbol !== undefined ? { symbol: params.symbol } : {}),
93
+ ...(params.decimalPlaces !== undefined
94
+ ? { decimalPlaces: params.decimalPlaces }
95
+ : {}),
96
+ ...(params.exchangeRate !== undefined
97
+ ? { exchangeRate: params.exchangeRate }
98
+ : {}),
99
+ ...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
100
+ ...(params.symbolPosition !== undefined
101
+ ? { symbolPosition: params.symbolPosition }
102
+ : {}),
103
+ ...(params.thousandsSeparator !== undefined
104
+ ? { thousandsSeparator: params.thousandsSeparator }
105
+ : {}),
106
+ ...(params.decimalSeparator !== undefined
107
+ ? { decimalSeparator: params.decimalSeparator }
108
+ : {}),
109
+ ...(params.roundingMode !== undefined
110
+ ? { roundingMode: params.roundingMode }
111
+ : {}),
112
+ ...(params.sortOrder !== undefined
113
+ ? { sortOrder: params.sortOrder }
114
+ : {}),
115
+ updatedAt: new Date(),
116
+ };
117
+ await data.upsert("currency", id, updated);
118
+ if (events) {
119
+ void events.emit("currency.updated", {
120
+ id: updated.id,
121
+ code: updated.code,
122
+ });
123
+ }
124
+ return updated;
125
+ },
126
+ async delete(id) {
127
+ const currency = (await data.get("currency", id));
128
+ if (!currency) {
129
+ return { deleted: false, error: "Currency not found" };
130
+ }
131
+ if (currency.isBase) {
132
+ return {
133
+ deleted: false,
134
+ error: "Cannot delete the base currency",
135
+ };
136
+ }
137
+ // Delete price overrides for this currency
138
+ const overrides = (await data.findMany("priceOverride", {
139
+ where: { currencyCode: currency.code },
140
+ }));
141
+ for (const o of overrides) {
142
+ await data.delete("priceOverride", o.id);
143
+ }
144
+ // Delete rate history
145
+ const history = (await data.findMany("exchangeRateHistory", {
146
+ where: { currencyCode: currency.code },
147
+ }));
148
+ for (const h of history) {
149
+ await data.delete("exchangeRateHistory", h.id);
150
+ }
151
+ await data.delete("currency", id);
152
+ if (events) {
153
+ void events.emit("currency.deleted", {
154
+ id: currency.id,
155
+ code: currency.code,
156
+ });
157
+ }
158
+ return { deleted: true };
159
+ },
160
+ async list(params) {
161
+ const where = params?.activeOnly ? { isActive: true } : {};
162
+ const currencies = (await data.findMany("currency", {
163
+ where,
164
+ }));
165
+ currencies.sort((a, b) => a.sortOrder - b.sortOrder);
166
+ return currencies;
167
+ },
168
+ async getBaseCurrency() {
169
+ const results = (await data.findMany("currency", {
170
+ where: { isBase: true },
171
+ take: 1,
172
+ }));
173
+ return results[0] ?? null;
174
+ },
175
+ async setBaseCurrency(id) {
176
+ const currency = (await data.get("currency", id));
177
+ if (!currency)
178
+ return null;
179
+ const now = new Date();
180
+ // Unset existing base currencies
181
+ const existing = (await data.findMany("currency", {
182
+ where: { isBase: true },
183
+ }));
184
+ for (const c of existing) {
185
+ if (c.id !== id) {
186
+ const record = { ...c, isBase: false, updatedAt: now };
187
+ await data.upsert("currency", c.id, record);
188
+ }
189
+ }
190
+ const updated = {
191
+ ...currency,
192
+ isBase: true,
193
+ exchangeRate: 1,
194
+ updatedAt: now,
195
+ };
196
+ await data.upsert("currency", id, updated);
197
+ if (events) {
198
+ void events.emit("currency.baseChanged", {
199
+ id: updated.id,
200
+ code: updated.code,
201
+ });
202
+ }
203
+ return updated;
204
+ },
205
+ async updateRate(params) {
206
+ const currency = await getCurrencyByCode(params.currencyCode);
207
+ if (!currency)
208
+ return null;
209
+ if (currency.isBase)
210
+ return currency; // Base rate is always 1
211
+ const now = new Date();
212
+ // Record history
213
+ const historyId = crypto.randomUUID();
214
+ const historyEntry = {
215
+ id: historyId,
216
+ currencyCode: currency.code,
217
+ rate: params.rate,
218
+ source: params.source ?? "manual",
219
+ recordedAt: now,
220
+ };
221
+ await data.upsert("exchangeRateHistory", historyId, historyEntry);
222
+ // Update the currency
223
+ const updated = {
224
+ ...currency,
225
+ exchangeRate: params.rate,
226
+ updatedAt: now,
227
+ };
228
+ const updatedRecord = updated;
229
+ await data.upsert("currency", currency.id, updatedRecord);
230
+ if (events) {
231
+ void events.emit("currency.rateUpdated", {
232
+ code: currency.code,
233
+ oldRate: currency.exchangeRate,
234
+ newRate: params.rate,
235
+ source: params.source ?? "manual",
236
+ });
237
+ }
238
+ return updated;
239
+ },
240
+ async bulkUpdateRates(rates) {
241
+ let updated = 0;
242
+ const errors = [];
243
+ for (const entry of rates) {
244
+ const result = await this.updateRate(entry);
245
+ if (result) {
246
+ updated++;
247
+ }
248
+ else {
249
+ errors.push(`Currency not found: ${entry.currencyCode}`);
250
+ }
251
+ }
252
+ return { updated, errors };
253
+ },
254
+ async getRateHistory(params) {
255
+ const { currencyCode, limit = 50 } = params;
256
+ const history = (await data.findMany("exchangeRateHistory", {
257
+ where: { currencyCode: currencyCode.toUpperCase() },
258
+ }));
259
+ history.sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());
260
+ return history.slice(0, limit);
261
+ },
262
+ async convert(params) {
263
+ const { amount, to, from } = params;
264
+ const targetCurrency = await getCurrencyByCode(to);
265
+ if (!targetCurrency)
266
+ return null;
267
+ let convertedAmount;
268
+ if (from) {
269
+ // Convert from a non-base currency
270
+ const sourceCurrency = await getCurrencyByCode(from);
271
+ if (!sourceCurrency)
272
+ return null;
273
+ // Convert to base first, then to target
274
+ const inBase = amount / sourceCurrency.exchangeRate;
275
+ convertedAmount = inBase * targetCurrency.exchangeRate;
276
+ }
277
+ else {
278
+ // Convert from base currency
279
+ convertedAmount = amount * targetCurrency.exchangeRate;
280
+ }
281
+ convertedAmount = applyRounding(convertedAmount, targetCurrency.decimalPlaces, targetCurrency.roundingMode);
282
+ return {
283
+ amount: convertedAmount,
284
+ currency: targetCurrency,
285
+ formatted: formatAmount(convertedAmount, targetCurrency),
286
+ };
287
+ },
288
+ async formatPrice(amount, currencyCode) {
289
+ const currency = await getCurrencyByCode(currencyCode);
290
+ if (!currency)
291
+ return null;
292
+ return formatAmount(amount, currency);
293
+ },
294
+ async setPriceOverride(params) {
295
+ const code = params.currencyCode.toUpperCase();
296
+ // Check if override exists for this product+currency
297
+ const existing = (await data.findMany("priceOverride", {
298
+ where: {
299
+ productId: params.productId,
300
+ currencyCode: code,
301
+ },
302
+ take: 1,
303
+ }));
304
+ const now = new Date();
305
+ const id = existing[0]?.id ?? crypto.randomUUID();
306
+ const override = {
307
+ id,
308
+ productId: params.productId,
309
+ currencyCode: code,
310
+ price: params.price,
311
+ compareAtPrice: params.compareAtPrice,
312
+ createdAt: existing[0]?.createdAt ?? now,
313
+ updatedAt: now,
314
+ };
315
+ await data.upsert("priceOverride", id, override);
316
+ return override;
317
+ },
318
+ async getPriceOverride(productId, currencyCode) {
319
+ const results = (await data.findMany("priceOverride", {
320
+ where: {
321
+ productId,
322
+ currencyCode: currencyCode.toUpperCase(),
323
+ },
324
+ take: 1,
325
+ }));
326
+ return results[0] ?? null;
327
+ },
328
+ async listPriceOverrides(productId) {
329
+ return (await data.findMany("priceOverride", {
330
+ where: { productId },
331
+ }));
332
+ },
333
+ async deletePriceOverride(id) {
334
+ await data.delete("priceOverride", id);
335
+ },
336
+ async getProductPrice(params) {
337
+ const { productId, basePriceInCents, currencyCode } = params;
338
+ const currency = await getCurrencyByCode(currencyCode);
339
+ if (!currency)
340
+ return null;
341
+ // Check for price override first
342
+ const override = await this.getPriceOverride(productId, currencyCode);
343
+ if (override) {
344
+ // Convert from cents to display amount
345
+ const displayAmount = override.price / 10 ** currency.decimalPlaces;
346
+ return {
347
+ amount: override.price,
348
+ currency,
349
+ formatted: formatAmount(displayAmount, currency),
350
+ };
351
+ }
352
+ // Fall back to conversion from base currency
353
+ const baseCurrency = await this.getBaseCurrency();
354
+ if (!baseCurrency)
355
+ return null;
356
+ // Convert cents to base currency amount
357
+ const baseAmount = basePriceInCents / 10 ** baseCurrency.decimalPlaces;
358
+ const converted = baseAmount * currency.exchangeRate;
359
+ const rounded = applyRounding(converted, currency.decimalPlaces, currency.roundingMode);
360
+ // Return amount in smallest unit (cents)
361
+ const amountInSmallestUnit = Math.round(rounded * 10 ** currency.decimalPlaces);
362
+ return {
363
+ amount: amountInSmallestUnit,
364
+ currency,
365
+ formatted: formatAmount(rounded, currency),
366
+ };
367
+ },
368
+ };
369
+ }