@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.
- package/README.md +234 -0
- package/dist/admin/components/_shared.d.ts +28 -0
- package/dist/admin/components/_shared.d.ts.map +1 -0
- package/dist/admin/components/_shared.js +15 -0
- package/dist/admin/components/currency-detail.d.ts +6 -0
- package/dist/admin/components/currency-detail.d.ts.map +1 -0
- package/dist/admin/components/currency-detail.js +26 -0
- package/dist/admin/components/currency-form.d.ts +6 -0
- package/dist/admin/components/currency-form.d.ts.map +1 -0
- package/dist/admin/components/currency-form.js +95 -0
- package/dist/admin/components/currency-list.d.ts +2 -0
- package/dist/admin/components/currency-list.d.ts.map +1 -0
- package/dist/admin/components/currency-list.js +35 -0
- package/dist/admin/endpoints/bulk-update-rates.d.ts +15 -0
- package/dist/admin/endpoints/bulk-update-rates.d.ts.map +1 -0
- package/dist/admin/endpoints/bulk-update-rates.js +17 -0
- package/dist/admin/endpoints/create-currency.d.ts +34 -0
- package/dist/admin/endpoints/create-currency.d.ts.map +1 -0
- package/dist/admin/endpoints/create-currency.js +34 -0
- package/dist/admin/endpoints/delete-currency.d.ts +16 -0
- package/dist/admin/endpoints/delete-currency.d.ts.map +1 -0
- package/dist/admin/endpoints/delete-currency.js +17 -0
- package/dist/admin/endpoints/delete-price-override.d.ts +10 -0
- package/dist/admin/endpoints/delete-price-override.d.ts.map +1 -0
- package/dist/admin/endpoints/delete-price-override.js +11 -0
- package/dist/admin/endpoints/get-currency.d.ts +16 -0
- package/dist/admin/endpoints/get-currency.d.ts.map +1 -0
- package/dist/admin/endpoints/get-currency.js +14 -0
- package/dist/admin/endpoints/list-currencies.d.ts +6 -0
- package/dist/admin/endpoints/list-currencies.d.ts.map +1 -0
- package/dist/admin/endpoints/list-currencies.js +7 -0
- package/dist/admin/endpoints/list-price-overrides.d.ts +10 -0
- package/dist/admin/endpoints/list-price-overrides.d.ts.map +1 -0
- package/dist/admin/endpoints/list-price-overrides.js +11 -0
- package/dist/admin/endpoints/rate-history.d.ts +11 -0
- package/dist/admin/endpoints/rate-history.d.ts.map +1 -0
- package/dist/admin/endpoints/rate-history.js +17 -0
- package/dist/admin/endpoints/routes.d.ts +180 -0
- package/dist/admin/endpoints/routes.d.ts.map +1 -0
- package/dist/admin/endpoints/routes.js +26 -0
- package/dist/admin/endpoints/set-base-currency.d.ts +16 -0
- package/dist/admin/endpoints/set-base-currency.d.ts.map +1 -0
- package/dist/admin/endpoints/set-base-currency.js +14 -0
- package/dist/admin/endpoints/set-price-override.d.ts +13 -0
- package/dist/admin/endpoints/set-price-override.d.ts.map +1 -0
- package/dist/admin/endpoints/set-price-override.js +16 -0
- package/dist/admin/endpoints/update-currency.d.ts +35 -0
- package/dist/admin/endpoints/update-currency.d.ts.map +1 -0
- package/dist/admin/endpoints/update-currency.js +27 -0
- package/dist/admin/endpoints/update-rate.d.ts +18 -0
- package/dist/admin/endpoints/update-rate.d.ts.map +1 -0
- package/dist/admin/endpoints/update-rate.js +22 -0
- package/dist/capabilities.d.ts +56 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/schema.d.ts +94 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +50 -0
- package/dist/service-impl.d.ts +5 -0
- package/dist/service-impl.d.ts.map +1 -0
- package/dist/service-impl.js +369 -0
- package/dist/service.d.ts +139 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +1 -0
- package/dist/store/components/_hooks.d.ts +6 -0
- package/dist/store/components/_hooks.d.ts.map +1 -0
- package/dist/store/components/_hooks.js +10 -0
- package/dist/store/components/currency-selector.d.ts +10 -0
- package/dist/store/components/currency-selector.d.ts.map +1 -0
- package/dist/store/components/currency-selector.js +22 -0
- package/dist/store/components/currency-selector.mdx +55 -0
- package/dist/store/components/mdx.d.ts +8 -0
- package/dist/store/components/mdx.d.ts.map +1 -0
- package/dist/store/components/mdx.js +7 -0
- package/dist/store/components/price-display.d.ts +14 -0
- package/dist/store/components/price-display.d.ts.map +1 -0
- package/dist/store/components/price-display.js +30 -0
- package/dist/store/components/price-display.mdx +12 -0
- package/dist/store/endpoints/convert-price.d.ts +22 -0
- package/dist/store/endpoints/convert-price.d.ts.map +1 -0
- package/dist/store/endpoints/convert-price.js +26 -0
- package/dist/store/endpoints/get-product-price.d.ts +22 -0
- package/dist/store/endpoints/get-product-price.d.ts.map +1 -0
- package/dist/store/endpoints/get-product-price.js +26 -0
- package/dist/store/endpoints/list-currencies.d.ts +6 -0
- package/dist/store/endpoints/list-currencies.d.ts.map +1 -0
- package/dist/store/endpoints/list-currencies.js +7 -0
- package/dist/store/endpoints/routes.d.ts +48 -0
- package/dist/store/endpoints/routes.d.ts.map +1 -0
- package/dist/store/endpoints/routes.js +8 -0
- 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
|
+
}
|
package/dist/schema.d.ts
ADDED
|
@@ -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
|
+
}
|