@uplab/vendure-plugin-exchange-rates 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/CHANGELOG.md +7 -0
  2. package/README.md +369 -0
  3. package/dist/api/admin.resolver.d.ts +16 -0
  4. package/dist/api/admin.resolver.d.ts.map +1 -0
  5. package/dist/api/admin.resolver.js +66 -0
  6. package/dist/api/admin.resolver.js.map +1 -0
  7. package/dist/api/api-extensions.d.ts +6 -0
  8. package/dist/api/api-extensions.d.ts.map +1 -0
  9. package/dist/api/api-extensions.js +57 -0
  10. package/dist/api/api-extensions.js.map +1 -0
  11. package/dist/api/shop.resolver.d.ts +15 -0
  12. package/dist/api/shop.resolver.d.ts.map +1 -0
  13. package/dist/api/shop.resolver.js +63 -0
  14. package/dist/api/shop.resolver.js.map +1 -0
  15. package/dist/constants.d.ts +10 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +13 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/dashboard/format-rate.ts +8 -0
  20. package/dist/dashboard/graphql.ts +74 -0
  21. package/dist/dashboard/i18n/en.po +109 -0
  22. package/dist/dashboard/i18n/uk.po +109 -0
  23. package/dist/dashboard/index.tsx +7 -0
  24. package/dist/dashboard/routes/currency-exchange-rate-detail.tsx +194 -0
  25. package/dist/dashboard/routes/currency-exchange-rate-list.tsx +105 -0
  26. package/dist/derive-rates.d.ts +13 -0
  27. package/dist/derive-rates.d.ts.map +1 -0
  28. package/dist/derive-rates.js +40 -0
  29. package/dist/derive-rates.js.map +1 -0
  30. package/dist/effective-rate.d.ts +8 -0
  31. package/dist/effective-rate.d.ts.map +1 -0
  32. package/dist/effective-rate.js +13 -0
  33. package/dist/effective-rate.js.map +1 -0
  34. package/dist/entities/currency-exchange-rate.entity.d.ts +13 -0
  35. package/dist/entities/currency-exchange-rate.entity.d.ts.map +1 -0
  36. package/dist/entities/currency-exchange-rate.entity.js +55 -0
  37. package/dist/entities/currency-exchange-rate.entity.js.map +1 -0
  38. package/dist/events/currency-exchange-rate.event.d.ts +18 -0
  39. package/dist/events/currency-exchange-rate.event.d.ts.map +1 -0
  40. package/dist/events/currency-exchange-rate.event.js +18 -0
  41. package/dist/events/currency-exchange-rate.event.js.map +1 -0
  42. package/dist/exchange-rate-source-error.d.ts +16 -0
  43. package/dist/exchange-rate-source-error.d.ts.map +1 -0
  44. package/dist/exchange-rate-source-error.js +18 -0
  45. package/dist/exchange-rate-source-error.js.map +1 -0
  46. package/dist/exchange-rate-sync.task.d.ts +3 -0
  47. package/dist/exchange-rate-sync.task.d.ts.map +1 -0
  48. package/dist/exchange-rate-sync.task.js +18 -0
  49. package/dist/exchange-rate-sync.task.js.map +1 -0
  50. package/dist/exchange-rates.plugin.d.ts +24 -0
  51. package/dist/exchange-rates.plugin.d.ts.map +1 -0
  52. package/dist/exchange-rates.plugin.js +101 -0
  53. package/dist/exchange-rates.plugin.js.map +1 -0
  54. package/dist/index.d.ts +15 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +31 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/services/currency-exchange-rate-sync.service.d.ts +30 -0
  59. package/dist/services/currency-exchange-rate-sync.service.d.ts.map +1 -0
  60. package/dist/services/currency-exchange-rate-sync.service.js +145 -0
  61. package/dist/services/currency-exchange-rate-sync.service.js.map +1 -0
  62. package/dist/services/currency-exchange-rate.service.d.ts +30 -0
  63. package/dist/services/currency-exchange-rate.service.d.ts.map +1 -0
  64. package/dist/services/currency-exchange-rate.service.js +93 -0
  65. package/dist/services/currency-exchange-rate.service.js.map +1 -0
  66. package/dist/sources/ecb-exchange-rate-source.d.ts +20 -0
  67. package/dist/sources/ecb-exchange-rate-source.d.ts.map +1 -0
  68. package/dist/sources/ecb-exchange-rate-source.js +33 -0
  69. package/dist/sources/ecb-exchange-rate-source.js.map +1 -0
  70. package/dist/sources/fetch-body.d.ts +9 -0
  71. package/dist/sources/fetch-body.d.ts.map +1 -0
  72. package/dist/sources/fetch-body.js +59 -0
  73. package/dist/sources/fetch-body.js.map +1 -0
  74. package/dist/sources/frankfurter-exchange-rate-source.d.ts +23 -0
  75. package/dist/sources/frankfurter-exchange-rate-source.d.ts.map +1 -0
  76. package/dist/sources/frankfurter-exchange-rate-source.js +35 -0
  77. package/dist/sources/frankfurter-exchange-rate-source.js.map +1 -0
  78. package/dist/sources/monobank-exchange-rate-source.d.ts +29 -0
  79. package/dist/sources/monobank-exchange-rate-source.d.ts.map +1 -0
  80. package/dist/sources/monobank-exchange-rate-source.js +46 -0
  81. package/dist/sources/monobank-exchange-rate-source.js.map +1 -0
  82. package/dist/sources/nbu-exchange-rate-source.d.ts +20 -0
  83. package/dist/sources/nbu-exchange-rate-source.d.ts.map +1 -0
  84. package/dist/sources/nbu-exchange-rate-source.js +25 -0
  85. package/dist/sources/nbu-exchange-rate-source.js.map +1 -0
  86. package/dist/sources/static-exchange-rate-source.d.ts +20 -0
  87. package/dist/sources/static-exchange-rate-source.d.ts.map +1 -0
  88. package/dist/sources/static-exchange-rate-source.js +23 -0
  89. package/dist/sources/static-exchange-rate-source.js.map +1 -0
  90. package/dist/types.d.ts +64 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +3 -0
  93. package/dist/types.js.map +1 -0
  94. package/package.json +96 -0
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_RATE = exports.DEFAULT_SOURCE_TIMEOUT = exports.SYNC_TASK_ID = exports.DEFAULT_SYNC_SCHEDULE = exports.loggerCtx = exports.EXCHANGE_RATES_PLUGIN_OPTIONS = void 0;
4
+ exports.EXCHANGE_RATES_PLUGIN_OPTIONS = Symbol('EXCHANGE_RATES_PLUGIN_OPTIONS');
5
+ exports.loggerCtx = 'ExchangeRatesPlugin';
6
+ /** Every 3 hours at minute 40. */
7
+ exports.DEFAULT_SYNC_SCHEDULE = '40 2-23/3 * * *';
8
+ exports.SYNC_TASK_ID = 'currency-exchange-rate-updater';
9
+ /** How long a source request may take before it is aborted, in milliseconds. */
10
+ exports.DEFAULT_SOURCE_TIMEOUT = 10_000;
11
+ /** decimal(19, 8) holds 11 integer digits. */
12
+ exports.MAX_RATE = 1e11;
13
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,6BAA6B,GAAG,MAAM,CAAC,+BAA+B,CAAC,CAAC;AACxE,QAAA,SAAS,GAAG,qBAAqB,CAAC;AAE/C,kCAAkC;AACrB,QAAA,qBAAqB,GAAG,iBAAiB,CAAC;AAE1C,QAAA,YAAY,GAAG,gCAAgC,CAAC;AAE7D,gFAAgF;AACnE,QAAA,sBAAsB,GAAG,MAAM,CAAC;AAE7C,8CAA8C;AACjC,QAAA,QAAQ,GAAG,IAAI,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** Rates span many magnitudes (JPY in USD is 0.0063…), so a fixed number of decimals would hide them. */
2
+ const listFormat: Intl.NumberFormatOptions = { maximumSignificantDigits: 6 };
3
+ /** Every decimal a rate is stored with. */
4
+ const fullFormat: Intl.NumberFormatOptions = { maximumFractionDigits: 8 };
5
+
6
+ export function formatRate(locale: string, value: number, precision: 'list' | 'full' = 'list'): string {
7
+ return new Intl.NumberFormat(locale, precision === 'full' ? fullFormat : listFormat).format(value);
8
+ }
@@ -0,0 +1,74 @@
1
+ import { graphql } from '@vendure/dashboard';
2
+ import type { DocumentNode } from 'graphql';
3
+
4
+ // The dashboard's `graphql()` is typed against core's schema only, so these documents carry
5
+ // hand-written result types instead. The shape matches `TypedDocumentNode`, which the pages expect.
6
+ type TypedDocument<Result, Variables> = DocumentNode & { __apiType?: (variables: Variables) => Result };
7
+
8
+ const typedDocument = <Result, Variables>(source: string) =>
9
+ (graphql as (source: string) => DocumentNode)(source) as TypedDocument<Result, Variables>;
10
+
11
+ export interface CurrencyExchangeRateFields {
12
+ id: string;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ code: string;
16
+ baseCurrency: string;
17
+ rate: number;
18
+ enabled: boolean;
19
+ useCustomRate: boolean;
20
+ customRate: number | null;
21
+ }
22
+
23
+ const FIELDS = `
24
+ fragment CurrencyExchangeRateFields on CurrencyExchangeRate {
25
+ id
26
+ createdAt
27
+ updatedAt
28
+ code
29
+ baseCurrency
30
+ rate
31
+ enabled
32
+ useCustomRate
33
+ customRate
34
+ }
35
+ `;
36
+
37
+ export const getCurrencyExchangeRateListDocument = typedDocument<
38
+ { currencyExchangeRates: { items: CurrencyExchangeRateFields[]; totalItems: number } },
39
+ { options?: Record<string, unknown> }
40
+ >(`
41
+ query GetCurrencyExchangeRateList($options: CurrencyExchangeRateListOptions) {
42
+ currencyExchangeRates(options: $options) {
43
+ items {
44
+ ...CurrencyExchangeRateFields
45
+ }
46
+ totalItems
47
+ }
48
+ }
49
+ ${FIELDS}
50
+ `);
51
+
52
+ export const getCurrencyExchangeRateDetailDocument = typedDocument<
53
+ { currencyExchangeRate: CurrencyExchangeRateFields | null },
54
+ { id: string }
55
+ >(`
56
+ query GetCurrencyExchangeRateDetail($id: ID!) {
57
+ currencyExchangeRate(id: $id) {
58
+ ...CurrencyExchangeRateFields
59
+ }
60
+ }
61
+ ${FIELDS}
62
+ `);
63
+
64
+ export const updateCurrencyExchangeRateDocument = typedDocument<
65
+ { updateCurrencyExchangeRate: CurrencyExchangeRateFields },
66
+ { input: { id: string; enabled: boolean; useCustomRate: boolean; customRate?: number | null } }
67
+ >(`
68
+ mutation UpdateCurrencyExchangeRate($input: UpdateCurrencyExchangeRateInput!) {
69
+ updateCurrencyExchangeRate(input: $input) {
70
+ ...CurrencyExchangeRateFields
71
+ }
72
+ }
73
+ ${FIELDS}
74
+ `);
@@ -0,0 +1,109 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "POT-Creation-Date: 2025-11-24 15:07+0200\n"
4
+ "MIME-Version: 1.0\n"
5
+ "Content-Type: text/plain; charset=utf-8\n"
6
+ "Content-Transfer-Encoding: 8bit\n"
7
+ "X-Generator: @lingui/cli\n"
8
+ "Language: en\n"
9
+ "Project-Id-Version: \n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "PO-Revision-Date: \n"
12
+ "Last-Translator: \n"
13
+ "Language-Team: \n"
14
+ "Plural-Forms: \n"
15
+
16
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
17
+ msgid "Code"
18
+ msgstr "Code"
19
+
20
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
21
+ msgid "Currency exchange rate"
22
+ msgstr "Currency exchange rate"
23
+
24
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
25
+ msgid "Currency exchange rate updated"
26
+ msgstr "Currency exchange rate updated"
27
+
28
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
29
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
30
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
31
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
32
+ msgid "Currency exchange rates"
33
+ msgstr "Currency exchange rates"
34
+
35
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
36
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
37
+ msgid "Custom rate"
38
+ msgstr "Custom rate"
39
+
40
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
41
+ msgid "Details"
42
+ msgstr "Details"
43
+
44
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
45
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
46
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
47
+ msgid "Disabled"
48
+ msgstr "Disabled"
49
+
50
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
51
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
52
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
53
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
54
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
55
+ msgid "Enabled"
56
+ msgstr "Enabled"
57
+
58
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
59
+ msgid "Enter a custom rate greater than 0"
60
+ msgstr "Enter a custom rate greater than 0"
61
+
62
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
63
+ msgid "Failed to update currency exchange rate"
64
+ msgstr "Failed to update currency exchange rate"
65
+
66
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
67
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
68
+ msgid "Fetched rate"
69
+ msgstr "Fetched rate"
70
+
71
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
72
+ msgid "Last updated"
73
+ msgstr "Last updated"
74
+
75
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
76
+ msgid "Reference information for this currency"
77
+ msgstr "Reference information for this currency"
78
+
79
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
80
+ msgid "Status"
81
+ msgstr "Status"
82
+
83
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
84
+ msgid "Update"
85
+ msgstr "Update"
86
+
87
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
88
+ msgid "Use custom rate"
89
+ msgstr "Use custom rate"
90
+
91
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
92
+ msgid "Using custom rate"
93
+ msgstr "Using custom rate"
94
+
95
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
96
+ msgid "Base currency"
97
+ msgstr "Base currency"
98
+
99
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
100
+ msgid "{0} per 1 {1}"
101
+ msgstr "{0} per 1 {1}"
102
+
103
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
104
+ msgid "In the same units, used instead of the fetched rate while “Use custom rate” is on"
105
+ msgstr "In the same units, used instead of the fetched rate while “Use custom rate” is on"
106
+
107
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
108
+ msgid "Unknown error"
109
+ msgstr "Unknown error"
@@ -0,0 +1,109 @@
1
+ msgid ""
2
+ msgstr ""
3
+ "POT-Creation-Date: 2025-11-24 15:07+0200\n"
4
+ "MIME-Version: 1.0\n"
5
+ "Content-Type: text/plain; charset=utf-8\n"
6
+ "Content-Transfer-Encoding: 8bit\n"
7
+ "X-Generator: @lingui/cli\n"
8
+ "Language: uk\n"
9
+ "Project-Id-Version: \n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "PO-Revision-Date: \n"
12
+ "Last-Translator: \n"
13
+ "Language-Team: \n"
14
+ "Plural-Forms: \n"
15
+
16
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
17
+ msgid "Code"
18
+ msgstr "Код"
19
+
20
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
21
+ msgid "Currency exchange rate"
22
+ msgstr "Курс обміну валюти"
23
+
24
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
25
+ msgid "Currency exchange rate updated"
26
+ msgstr "Курс обміну валюти оновлено"
27
+
28
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
29
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
30
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
31
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
32
+ msgid "Currency exchange rates"
33
+ msgstr "Курси обміну валют"
34
+
35
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
36
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
37
+ msgid "Custom rate"
38
+ msgstr "Власний курс"
39
+
40
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
41
+ msgid "Details"
42
+ msgstr "Деталі"
43
+
44
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
45
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
46
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
47
+ msgid "Disabled"
48
+ msgstr "Вимкнено"
49
+
50
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
51
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
52
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
53
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
54
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
55
+ msgid "Enabled"
56
+ msgstr "Увімкнено"
57
+
58
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
59
+ msgid "Enter a custom rate greater than 0"
60
+ msgstr "Введіть власний курс, більший за 0"
61
+
62
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
63
+ msgid "Failed to update currency exchange rate"
64
+ msgstr "Не вдалося оновити курс обміну валюти"
65
+
66
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
67
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
68
+ msgid "Fetched rate"
69
+ msgstr "Курс із джерела"
70
+
71
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
72
+ msgid "Last updated"
73
+ msgstr "Останнє оновлення"
74
+
75
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
76
+ msgid "Reference information for this currency"
77
+ msgstr "Довідкова інформація для цієї валюти"
78
+
79
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
80
+ msgid "Status"
81
+ msgstr "Статус"
82
+
83
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
84
+ msgid "Update"
85
+ msgstr "Оновити"
86
+
87
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
88
+ msgid "Use custom rate"
89
+ msgstr "Використовувати власний курс"
90
+
91
+ #: src/dashboard/routes/currency-exchange-rate-list.tsx
92
+ msgid "Using custom rate"
93
+ msgstr "Використовується власний курс"
94
+
95
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
96
+ msgid "Base currency"
97
+ msgstr "Базова валюта"
98
+
99
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
100
+ msgid "{0} per 1 {1}"
101
+ msgstr "{0} за 1 {1}"
102
+
103
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
104
+ msgid "In the same units, used instead of the fetched rate while “Use custom rate” is on"
105
+ msgstr "У тих самих одиницях; застосовується замість курсу із джерела, поки ввімкнено «Використовувати власний курс»"
106
+
107
+ #: src/dashboard/routes/currency-exchange-rate-detail.tsx
108
+ msgid "Unknown error"
109
+ msgstr "Невідома помилка"
@@ -0,0 +1,7 @@
1
+ import { defineDashboardExtension } from '@vendure/dashboard';
2
+ import { currencyExchangeRateDetailRoute } from './routes/currency-exchange-rate-detail';
3
+ import { currencyExchangeRateListRoute } from './routes/currency-exchange-rate-list';
4
+
5
+ defineDashboardExtension({
6
+ routes: [currencyExchangeRateListRoute, currencyExchangeRateDetailRoute],
7
+ });
@@ -0,0 +1,194 @@
1
+ import { Trans, useLingui } from '@lingui/react/macro';
2
+ import {
3
+ Badge,
4
+ Button,
5
+ DashboardRouteDefinition,
6
+ DetailFormGrid,
7
+ FormFieldWrapper,
8
+ Input,
9
+ Page,
10
+ PageActionBar,
11
+ PageActionBarRight,
12
+ PageBlock,
13
+ PageLayout,
14
+ PageTitle,
15
+ PermissionGuard,
16
+ Switch,
17
+ detailPageRouteLoader,
18
+ toast,
19
+ useDetailPage,
20
+ useLocalFormat,
21
+ } from '@vendure/dashboard';
22
+ import { useMemo } from 'react';
23
+ import { formatRate } from '../format-rate';
24
+ import { getCurrencyExchangeRateDetailDocument, updateCurrencyExchangeRateDocument } from '../graphql';
25
+
26
+ const pageId = 'currency-exchange-rate-detail';
27
+
28
+ export const currencyExchangeRateDetailRoute: DashboardRouteDefinition = {
29
+ path: '/currency-exchange-rates/$id',
30
+ loader: detailPageRouteLoader({
31
+ pageId,
32
+ queryDocument: getCurrencyExchangeRateDetailDocument,
33
+ breadcrumb: (_isNew, entity) => [
34
+ { path: '/currency-exchange-rates', label: <Trans>Currency exchange rates</Trans> },
35
+ entity?.code ?? '',
36
+ ],
37
+ }),
38
+ component: (route) => <CurrencyExchangeRateDetailPage route={route} />,
39
+ };
40
+
41
+ type Route = Parameters<DashboardRouteDefinition['component']>[0];
42
+
43
+ function CurrencyExchangeRateDetailPage({ route }: { route: Route }) {
44
+ const { t, i18n } = useLingui();
45
+ const params = route.useParams();
46
+ const { formatDate } = useLocalFormat();
47
+
48
+ const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
49
+ pageId,
50
+ entityName: 'CurrencyExchangeRate',
51
+ queryDocument: getCurrencyExchangeRateDetailDocument,
52
+ updateDocument: updateCurrencyExchangeRateDocument,
53
+ setValuesForUpdate: (rate) => ({
54
+ id: rate.id,
55
+ enabled: rate.enabled,
56
+ useCustomRate: rate.useCustomRate,
57
+ customRate: rate.customRate ?? null,
58
+ }),
59
+ params: { id: params.id },
60
+ onSuccess: () => {
61
+ toast.success(t`Currency exchange rate updated`);
62
+ resetForm();
63
+ },
64
+ onError: (error) => {
65
+ toast.error(t`Failed to update currency exchange rate`, {
66
+ description: error instanceof Error ? error.message : t`Unknown error`,
67
+ });
68
+ },
69
+ });
70
+
71
+ const useCustomRate = form.watch('useCustomRate');
72
+
73
+ const customRateRules = useMemo(
74
+ () => ({
75
+ validate: (value: number | null | undefined) => {
76
+ if (!form.getValues('useCustomRate')) {
77
+ return true;
78
+ }
79
+ if (!value || value <= 0) {
80
+ return t`Enter a custom rate greater than 0`;
81
+ }
82
+ return true;
83
+ },
84
+ }),
85
+ [form, t],
86
+ );
87
+
88
+ return (
89
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
90
+ <PageTitle>{entity?.code ?? t`Currency exchange rate`}</PageTitle>
91
+ <PageActionBar>
92
+ <PageActionBarRight>
93
+ <PermissionGuard requires={['UpdateSettings']}>
94
+ <Button type="submit" disabled={!form.formState.isDirty || !form.formState.isValid || isPending}>
95
+ <Trans>Update</Trans>
96
+ </Button>
97
+ </PermissionGuard>
98
+ </PageActionBarRight>
99
+ </PageActionBar>
100
+ <PageLayout>
101
+ <PageBlock
102
+ column="side"
103
+ blockId="currency-exchange-rate-info"
104
+ title={<Trans>Details</Trans>}
105
+ description={<Trans>Reference information for this currency</Trans>}
106
+ >
107
+ <dl className="space-y-3 text-sm">
108
+ <div className="flex flex-col">
109
+ <dt className="text-muted-foreground">
110
+ <Trans>Code</Trans>
111
+ </dt>
112
+ <dd className="font-mono text-base">{entity?.code ?? '\u2014'}</dd>
113
+ </div>
114
+ <div className="flex flex-col">
115
+ <dt className="text-muted-foreground">
116
+ <Trans>Base currency</Trans>
117
+ </dt>
118
+ <dd className="font-mono text-base">{entity?.baseCurrency ?? '\u2014'}</dd>
119
+ </div>
120
+ <div className="flex flex-col">
121
+ <dt className="text-muted-foreground">
122
+ <Trans>Status</Trans>
123
+ </dt>
124
+ <dd>
125
+ <Badge variant={entity?.enabled ? 'success' : 'secondary'}>
126
+ {entity?.enabled ? t`Enabled` : t`Disabled`}
127
+ </Badge>
128
+ </dd>
129
+ </div>
130
+ <div className="flex flex-col">
131
+ <dt className="text-muted-foreground">
132
+ <Trans>Last updated</Trans>
133
+ </dt>
134
+ <dd>{entity?.updatedAt ? formatDate(entity.updatedAt) : '\u2014'}</dd>
135
+ </div>
136
+ </dl>
137
+ </PageBlock>
138
+ <PageBlock column="main" blockId="currency-exchange-rate-form">
139
+ <div className="space-y-6">
140
+ <div>
141
+ <p className="text-sm font-medium text-muted-foreground">
142
+ <Trans>Fetched rate</Trans>
143
+ </p>
144
+ <p className="text-2xl font-semibold">
145
+ {entity ? formatRate(i18n.locale, entity.rate, 'full') : '\u2014'}
146
+ </p>
147
+ {entity && (
148
+ <p className="text-sm text-muted-foreground">
149
+ <Trans>
150
+ {entity.baseCurrency} per 1 {entity.code}
151
+ </Trans>
152
+ </p>
153
+ )}
154
+ </div>
155
+ <DetailFormGrid>
156
+ <FormFieldWrapper
157
+ control={form.control}
158
+ name="enabled"
159
+ label={<Trans>Enabled</Trans>}
160
+ render={({ field }) => <Switch checked={!!field.value} onCheckedChange={field.onChange} />}
161
+ />
162
+ <FormFieldWrapper
163
+ control={form.control}
164
+ name="useCustomRate"
165
+ label={<Trans>Use custom rate</Trans>}
166
+ rules={{ deps: ['customRate'] }}
167
+ render={({ field }) => <Switch checked={!!field.value} onCheckedChange={field.onChange} />}
168
+ />
169
+ <FormFieldWrapper
170
+ control={form.control}
171
+ name="customRate"
172
+ label={<Trans>Custom rate</Trans>}
173
+ description={
174
+ <Trans>In the same units, used instead of the fetched rate while “Use custom rate” is on</Trans>
175
+ }
176
+ rules={customRateRules}
177
+ render={({ field }) => (
178
+ <Input
179
+ type="number"
180
+ min="0"
181
+ step="any"
182
+ value={field.value ?? ''}
183
+ onChange={(event) => field.onChange(event.target.value === '' ? null : Number(event.target.value))}
184
+ disabled={!useCustomRate}
185
+ />
186
+ )}
187
+ />
188
+ </DetailFormGrid>
189
+ </div>
190
+ </PageBlock>
191
+ </PageLayout>
192
+ </Page>
193
+ );
194
+ }
@@ -0,0 +1,105 @@
1
+ import { Trans, useLingui } from '@lingui/react/macro';
2
+ import { Badge, DashboardRouteDefinition, DetailPageButton, ListPage, useLocalFormat } from '@vendure/dashboard';
3
+ import { useMemo } from 'react';
4
+ import { formatRate } from '../format-rate';
5
+ import { getCurrencyExchangeRateListDocument } from '../graphql';
6
+
7
+ const pageId = 'currency-exchange-rate-list';
8
+
9
+ export const currencyExchangeRateListRoute: DashboardRouteDefinition = {
10
+ path: '/currency-exchange-rates',
11
+ navMenuItem: {
12
+ sectionId: 'settings',
13
+ id: 'currency-exchange-rates',
14
+ title: 'Currency exchange rates',
15
+ requiresPermission: 'ReadSettings',
16
+ },
17
+ loader: () => ({
18
+ breadcrumb: () => <Trans>Currency exchange rates</Trans>,
19
+ title: () => <Trans>Currency exchange rates</Trans>,
20
+ }),
21
+ component: (route) => <CurrencyExchangeRateListPage route={route} />,
22
+ };
23
+
24
+ function CurrencyExchangeRateListPage({ route }: { route: Parameters<DashboardRouteDefinition['component']>[0] }) {
25
+ const { t, i18n } = useLingui();
26
+ const { formatDate } = useLocalFormat();
27
+
28
+ const booleanOptions = useMemo(
29
+ () => [
30
+ { value: true, label: t`Enabled` },
31
+ { value: false, label: t`Disabled` },
32
+ ],
33
+ [t],
34
+ );
35
+
36
+ return (
37
+ <ListPage
38
+ pageId={pageId}
39
+ title={<Trans>Currency exchange rates</Trans>}
40
+ listQuery={getCurrencyExchangeRateListDocument}
41
+ route={route}
42
+ defaultSort={[{ id: 'code', desc: false }]}
43
+ defaultVisibility={{
44
+ code: true,
45
+ rate: true,
46
+ enabled: true,
47
+ useCustomRate: true,
48
+ // Shown inside the rate cell.
49
+ baseCurrency: false,
50
+ customRate: false,
51
+ // The dashboard pins `updatedAt` to the front; the detail page shows it instead.
52
+ updatedAt: false,
53
+ }}
54
+ defaultColumnOrder={['code', 'rate', 'enabled', 'useCustomRate']}
55
+ onSearchTermChange={(term) => (term ? { code: { contains: term } } : {})}
56
+ facetedFilters={{
57
+ enabled: {
58
+ title: t`Enabled`,
59
+ options: booleanOptions,
60
+ },
61
+ useCustomRate: {
62
+ title: t`Custom rate`,
63
+ options: booleanOptions,
64
+ },
65
+ }}
66
+ customizeColumns={{
67
+ code: {
68
+ cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.code} />,
69
+ },
70
+ rate: {
71
+ meta: { dependencies: ['customRate', 'useCustomRate', 'baseCurrency'] },
72
+ cell: ({ row }) => {
73
+ const value = row.original.useCustomRate
74
+ ? (row.original.customRate ?? row.original.rate)
75
+ : row.original.rate;
76
+ return (
77
+ <span>
78
+ {formatRate(i18n.locale, value ?? 0)}{' '}
79
+ <span className="text-muted-foreground">{row.original.baseCurrency}</span>
80
+ </span>
81
+ );
82
+ },
83
+ },
84
+ enabled: {
85
+ cell: ({ row }) => (
86
+ <Badge variant={row.original.enabled ? 'success' : 'secondary'}>
87
+ {row.original.enabled ? t`Enabled` : t`Disabled`}
88
+ </Badge>
89
+ ),
90
+ },
91
+ useCustomRate: {
92
+ header: () => <Trans>Custom rate</Trans>,
93
+ cell: ({ row }) => (
94
+ <Badge variant={row.original.useCustomRate ? 'success' : 'secondary'}>
95
+ {row.original.useCustomRate ? t`Using custom rate` : t`Fetched rate`}
96
+ </Badge>
97
+ ),
98
+ },
99
+ updatedAt: {
100
+ cell: ({ row }) => formatDate(row.original.updatedAt),
101
+ },
102
+ }}
103
+ />
104
+ );
105
+ }
@@ -0,0 +1,13 @@
1
+ import type { CurrencyCode } from '@vendure/core';
2
+ import type { ExchangeRateQuote } from './types';
3
+ /** The 8 decimals a rate is stored with. */
4
+ export declare const round8: (value: number) => number;
5
+ /**
6
+ * @description
7
+ * Re-bases `quotes` (units of `sourceBase` per unit) onto `shopBase`. With a different base the cross
8
+ * rate is `rate(c) / rate(shopBase)`, so the source must quote the shop's base currency; it throws when
9
+ * it does not. A quote for the shop's base itself is dropped, and so is a result that rounds to 0 or does
10
+ * not fit decimal(19, 8).
11
+ */
12
+ export declare function deriveRates(source: string, shopBase: CurrencyCode, sourceBase: CurrencyCode, quotes: ExchangeRateQuote[]): ExchangeRateQuote[];
13
+ //# sourceMappingURL=derive-rates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-rates.d.ts","sourceRoot":"","sources":["../src/derive-rates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,4CAA4C;AAC5C,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,KAAG,MAAuC,CAAC;AAE/E;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,YAAY,EACxB,MAAM,EAAE,iBAAiB,EAAE,GAC1B,iBAAiB,EAAE,CAwBrB"}