payment-kit 1.24.3 → 1.25.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/api/src/crons/overdue-detection.ts +10 -1
- package/api/src/index.ts +3 -0
- package/api/src/libs/credit-utils.ts +21 -0
- package/api/src/libs/discount/discount.ts +13 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/error.ts +14 -0
- package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
- package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
- package/api/src/libs/exchange-rate/index.ts +5 -0
- package/api/src/libs/exchange-rate/service.ts +583 -0
- package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
- package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
- package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
- package/api/src/libs/exchange-rate/types.ts +114 -0
- package/api/src/libs/exchange-rate/validator.ts +319 -0
- package/api/src/libs/invoice-quote.ts +158 -0
- package/api/src/libs/invoice.ts +143 -7
- package/api/src/libs/math-utils.ts +46 -0
- package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
- package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
- package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
- package/api/src/libs/payment.ts +1 -1
- package/api/src/libs/price.ts +4 -1
- package/api/src/libs/queue/index.ts +8 -0
- package/api/src/libs/quote-service.ts +1132 -0
- package/api/src/libs/quote-validation.ts +388 -0
- package/api/src/libs/session.ts +686 -39
- package/api/src/libs/slippage.ts +135 -0
- package/api/src/libs/subscription.ts +185 -15
- package/api/src/libs/util.ts +64 -3
- package/api/src/locales/en.ts +50 -0
- package/api/src/locales/zh.ts +48 -0
- package/api/src/queues/auto-recharge.ts +295 -21
- package/api/src/queues/exchange-rate-health.ts +242 -0
- package/api/src/queues/invoice.ts +48 -1
- package/api/src/queues/notification.ts +190 -3
- package/api/src/queues/payment.ts +177 -7
- package/api/src/queues/subscription.ts +436 -6
- package/api/src/routes/auto-recharge-configs.ts +71 -6
- package/api/src/routes/checkout-sessions.ts +1730 -81
- package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
- package/api/src/routes/connect/change-payer.ts +2 -0
- package/api/src/routes/connect/change-payment.ts +61 -8
- package/api/src/routes/connect/change-plan.ts +161 -17
- package/api/src/routes/connect/collect.ts +9 -6
- package/api/src/routes/connect/delegation.ts +1 -0
- package/api/src/routes/connect/pay.ts +157 -0
- package/api/src/routes/connect/setup.ts +32 -10
- package/api/src/routes/connect/shared.ts +159 -13
- package/api/src/routes/connect/subscribe.ts +32 -9
- package/api/src/routes/credit-grants.ts +99 -0
- package/api/src/routes/exchange-rate-providers.ts +248 -0
- package/api/src/routes/exchange-rates.ts +87 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +280 -2
- package/api/src/routes/meter-events.ts +3 -0
- package/api/src/routes/payment-links.ts +13 -0
- package/api/src/routes/prices.ts +84 -2
- package/api/src/routes/subscriptions.ts +526 -15
- package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
- package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
- package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
- package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
- package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
- package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
- package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
- package/api/src/store/models/auto-recharge-config.ts +12 -0
- package/api/src/store/models/checkout-session.ts +7 -0
- package/api/src/store/models/exchange-rate-provider.ts +225 -0
- package/api/src/store/models/index.ts +6 -0
- package/api/src/store/models/payment-intent.ts +6 -0
- package/api/src/store/models/price-quote.ts +284 -0
- package/api/src/store/models/price.ts +53 -5
- package/api/src/store/models/subscription.ts +11 -0
- package/api/src/store/models/types.ts +61 -1
- package/api/tests/libs/change-payment-plan.spec.ts +282 -0
- package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
- package/api/tests/libs/quote-service.spec.ts +199 -0
- package/api/tests/libs/session.spec.ts +464 -0
- package/api/tests/libs/slippage.spec.ts +109 -0
- package/api/tests/libs/token-data-provider.spec.ts +267 -0
- package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
- package/api/tests/models/price-dynamic.spec.ts +100 -0
- package/api/tests/models/price-quote.spec.ts +112 -0
- package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
- package/api/tests/routes/subscription-slippage.spec.ts +254 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +14 -0
- package/src/components/discount/discount-info.tsx +8 -2
- package/src/components/invoice/list.tsx +146 -16
- package/src/components/invoice/table.tsx +276 -71
- package/src/components/invoice-pdf/template.tsx +3 -7
- package/src/components/metadata/form.tsx +6 -8
- package/src/components/price/form.tsx +519 -149
- package/src/components/promotion/active-redemptions.tsx +5 -3
- package/src/components/quote/info.tsx +234 -0
- package/src/hooks/subscription.ts +132 -2
- package/src/locales/en.tsx +145 -0
- package/src/locales/zh.tsx +143 -1
- package/src/pages/admin/billing/invoices/detail.tsx +41 -4
- package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
- package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
- package/src/pages/admin/products/index.tsx +12 -1
- package/src/pages/customer/invoice/detail.tsx +36 -12
- package/src/pages/customer/subscription/change-payment.tsx +65 -3
- package/src/pages/customer/subscription/change-plan.tsx +207 -38
- package/src/pages/customer/subscription/detail.tsx +599 -419
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import pick from 'lodash/pick';
|
|
3
|
+
import { Op } from 'sequelize';
|
|
4
|
+
|
|
5
|
+
import { authenticate } from '../libs/security';
|
|
6
|
+
import { ExchangeRateProvider } from '../store/models/exchange-rate-provider';
|
|
7
|
+
import { TokenDataProvider } from '../libs/exchange-rate/token-data-provider';
|
|
8
|
+
import { CoinGeckoProvider } from '../libs/exchange-rate/coingecko-provider';
|
|
9
|
+
import { CoinMarketCapProvider } from '../libs/exchange-rate/coinmarketcap-provider';
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
const auth = authenticate({ component: true, roles: ['owner', 'admin'], ensureLogin: true });
|
|
13
|
+
const allowedStatuses = ['active', 'degraded', 'paused', 'inactive'];
|
|
14
|
+
|
|
15
|
+
router.get('/', auth, async (_req, res) => {
|
|
16
|
+
const providers = await ExchangeRateProvider.findAll({
|
|
17
|
+
order: [
|
|
18
|
+
['priority', 'ASC'],
|
|
19
|
+
['created_at', 'ASC'],
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Mask sensitive config data (API keys) before sending to client
|
|
24
|
+
const maskedProviders = providers.map((p) => ({
|
|
25
|
+
...p.toJSON(),
|
|
26
|
+
config: ExchangeRateProvider.maskConfig(p.config),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
res.json({ data: maskedProviders });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.post('/', auth, async (req, res) => {
|
|
33
|
+
const {
|
|
34
|
+
name,
|
|
35
|
+
type = 'token-data',
|
|
36
|
+
enabled = true,
|
|
37
|
+
priority = 1,
|
|
38
|
+
status = 'active',
|
|
39
|
+
config = {},
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
41
|
+
paused_reason = null,
|
|
42
|
+
} = req.body;
|
|
43
|
+
if (!name) {
|
|
44
|
+
res.status(400).json({ error: 'name is required' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (Number.isNaN(Number(priority))) {
|
|
48
|
+
res.status(400).json({ error: 'priority must be a number' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!allowedStatuses.includes(status)) {
|
|
52
|
+
res.status(400).json({ error: 'invalid status' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const provider = await ExchangeRateProvider.create({
|
|
57
|
+
name,
|
|
58
|
+
type,
|
|
59
|
+
enabled: !!enabled,
|
|
60
|
+
priority: Number(priority),
|
|
61
|
+
status,
|
|
62
|
+
paused_reason: status === 'paused' ? paused_reason : null,
|
|
63
|
+
config: ExchangeRateProvider.encryptConfig(config),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
res.json({ data: provider });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
router.put('/:id', auth, async (req, res) => {
|
|
70
|
+
const provider = await ExchangeRateProvider.findByPk(req.params.id);
|
|
71
|
+
if (!provider) {
|
|
72
|
+
res.status(404).json({ error: 'Provider not found' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Note: 'type' is intentionally excluded - cannot be changed after creation
|
|
77
|
+
const updates = pick(req.body, ['name', 'enabled', 'priority', 'status', 'paused_reason', 'config']);
|
|
78
|
+
if (updates.priority !== undefined && Number.isNaN(Number(updates.priority))) {
|
|
79
|
+
res.status(400).json({ error: 'priority must be a number' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (updates.status && !allowedStatuses.includes(updates.status)) {
|
|
83
|
+
res.status(400).json({ error: 'invalid status' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Encrypt config if it's being updated
|
|
88
|
+
if (updates.config) {
|
|
89
|
+
const existingConfig = ExchangeRateProvider.decryptConfig(provider.config) || {};
|
|
90
|
+
const mergedConfig = { ...existingConfig, ...updates.config };
|
|
91
|
+
if (updates.config.api_key === '') {
|
|
92
|
+
delete mergedConfig.api_key;
|
|
93
|
+
}
|
|
94
|
+
updates.config = ExchangeRateProvider.encryptConfig(mergedConfig);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await provider.update(updates);
|
|
98
|
+
res.json({ data: provider });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// TECHNICAL DEBT: Exchange rate provider management bypasses AFS layer
|
|
102
|
+
// TODO: Migrate to AFS-based provider management when AFS layer is available
|
|
103
|
+
// Context: This CRUD API was built before AFS implementation
|
|
104
|
+
// Impact: UI is tightly coupled to backend schema, not replayable via AFS paths
|
|
105
|
+
// Migration Path: Implement $afs:/admin/providers/*.json views
|
|
106
|
+
// Decision: Accepted as pragmatic completion of existing system (2026-01-12)
|
|
107
|
+
// Reference: ai/intent/20260112-token-data-provider-management-alignment.md
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* DELETE endpoint - Remove an exchange rate provider
|
|
111
|
+
* Safety constraints:
|
|
112
|
+
* 1. Cannot delete provider that is currently in use
|
|
113
|
+
* 2. Cannot delete the last enabled provider
|
|
114
|
+
*/
|
|
115
|
+
router.delete('/:id', auth, async (req, res) => {
|
|
116
|
+
const provider = await ExchangeRateProvider.findByPk(req.params.id);
|
|
117
|
+
if (!provider) {
|
|
118
|
+
res.status(404).json({ error: 'Provider not found' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Safety Check 1: Get active provider (same logic as frontend)
|
|
123
|
+
const allProviders = await ExchangeRateProvider.findAll({
|
|
124
|
+
order: [
|
|
125
|
+
['priority', 'ASC'],
|
|
126
|
+
['created_at', 'ASC'],
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const activeProviders = allProviders
|
|
131
|
+
.filter((p) => p.enabled && p.status !== 'paused')
|
|
132
|
+
.sort((a, b) => a.priority - b.priority);
|
|
133
|
+
|
|
134
|
+
const activeProviderId = activeProviders[0]?.id || null;
|
|
135
|
+
|
|
136
|
+
if (provider.id === activeProviderId) {
|
|
137
|
+
res.status(400).json({
|
|
138
|
+
error: 'Cannot delete provider that is currently in use',
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Safety Check 2: Ensure we're not deleting the last enabled provider
|
|
144
|
+
const otherEnabledCount = await ExchangeRateProvider.count({
|
|
145
|
+
where: {
|
|
146
|
+
id: { [Op.ne]: req.params.id },
|
|
147
|
+
enabled: true,
|
|
148
|
+
status: { [Op.ne]: 'paused' },
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (otherEnabledCount === 0 && provider.enabled && provider.status !== 'paused') {
|
|
153
|
+
res.status(400).json({
|
|
154
|
+
error:
|
|
155
|
+
'Cannot delete the last enabled provider. Please ensure at least one other provider is enabled before deleting this one.',
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// All checks passed, safe to delete
|
|
161
|
+
await provider.destroy();
|
|
162
|
+
|
|
163
|
+
res.json({ success: true });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* POST /test-connection - Test provider connection without saving
|
|
168
|
+
* Validates provider configuration by attempting to fetch a test rate
|
|
169
|
+
*/
|
|
170
|
+
router.post('/test-connection', auth, async (req, res) => {
|
|
171
|
+
const { type, config = {}, provider_id: providerId } = req.body;
|
|
172
|
+
|
|
173
|
+
// Validate provider type
|
|
174
|
+
const allowedTypes = ['token-data', 'coingecko', 'coinmarketcap'];
|
|
175
|
+
if (!type || !allowedTypes.includes(type)) {
|
|
176
|
+
res.status(400).json({ error: 'Invalid provider type' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Decrypt config if it contains encrypted api_key
|
|
181
|
+
const decryptedConfig = ExchangeRateProvider.decryptConfig(config) || {};
|
|
182
|
+
let effectiveConfig = decryptedConfig;
|
|
183
|
+
if (providerId) {
|
|
184
|
+
const provider = await ExchangeRateProvider.findByPk(providerId);
|
|
185
|
+
if (provider) {
|
|
186
|
+
const existingConfig = ExchangeRateProvider.decryptConfig(provider.config) || {};
|
|
187
|
+
if (decryptedConfig.api_key === '') {
|
|
188
|
+
delete existingConfig.api_key;
|
|
189
|
+
}
|
|
190
|
+
const mergedConfig = {
|
|
191
|
+
...existingConfig,
|
|
192
|
+
...decryptedConfig,
|
|
193
|
+
};
|
|
194
|
+
effectiveConfig = mergedConfig;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Create provider instance based on type
|
|
199
|
+
let provider;
|
|
200
|
+
try {
|
|
201
|
+
switch (type) {
|
|
202
|
+
case 'token-data':
|
|
203
|
+
provider = new TokenDataProvider(effectiveConfig || undefined);
|
|
204
|
+
break;
|
|
205
|
+
case 'coingecko':
|
|
206
|
+
provider = new CoinGeckoProvider(effectiveConfig || undefined);
|
|
207
|
+
break;
|
|
208
|
+
case 'coinmarketcap':
|
|
209
|
+
provider = new CoinMarketCapProvider(effectiveConfig || undefined);
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
res.status(400).json({ error: 'Invalid provider type' });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
} catch (error: any) {
|
|
216
|
+
res.status(200).json({
|
|
217
|
+
success: false,
|
|
218
|
+
error: error.message || 'Failed to initialize provider',
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const testSymbol = type === 'token-data' ? 'ABT' : 'ETH';
|
|
224
|
+
const startTime = Date.now();
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const result = await provider.fetch(testSymbol);
|
|
228
|
+
const responseTime = Date.now() - startTime;
|
|
229
|
+
|
|
230
|
+
res.json({
|
|
231
|
+
success: true,
|
|
232
|
+
responseTime,
|
|
233
|
+
rate: result.rate,
|
|
234
|
+
timestamp: result.timestamp_ms,
|
|
235
|
+
symbol: testSymbol,
|
|
236
|
+
});
|
|
237
|
+
} catch (error: any) {
|
|
238
|
+
const responseTime = Date.now() - startTime;
|
|
239
|
+
res.status(200).json({
|
|
240
|
+
success: false,
|
|
241
|
+
responseTime,
|
|
242
|
+
error: error.message || 'Connection test failed',
|
|
243
|
+
symbol: testSymbol,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export default router;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
import { getExchangeRateService } from '../libs/exchange-rate/service';
|
|
4
|
+
import { getExchangeRateSymbol, hasTokenAddress } from '../libs/exchange-rate/token-address-mapping';
|
|
5
|
+
import { authenticate } from '../libs/security';
|
|
6
|
+
import { ChainType, PaymentCurrency, PaymentMethod } from '../store/models';
|
|
7
|
+
|
|
8
|
+
const router = Router();
|
|
9
|
+
const auth = authenticate({ component: true, roles: ['owner', 'admin'], ensureLogin: true });
|
|
10
|
+
const exchangeRateService = getExchangeRateService();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate if exchange rate can be fetched for a currency
|
|
14
|
+
* This is used during price creation/editing to validate the base currency
|
|
15
|
+
*/
|
|
16
|
+
router.post('/validate', auth, async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const { currency: currencyId } = req.body;
|
|
19
|
+
|
|
20
|
+
if (!currencyId) {
|
|
21
|
+
return res.status(400).json({ error: 'currency is required' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const paymentCurrency = (await PaymentCurrency.findByPk(currencyId, {
|
|
25
|
+
include: [
|
|
26
|
+
{
|
|
27
|
+
model: PaymentMethod,
|
|
28
|
+
as: 'payment_method',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
})) as PaymentCurrency & { payment_method: PaymentMethod };
|
|
32
|
+
|
|
33
|
+
if (!paymentCurrency) {
|
|
34
|
+
return res.status(400).json({ error: 'Currency not found' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (paymentCurrency.payment_method?.type === 'stripe') {
|
|
38
|
+
return res.status(400).json({
|
|
39
|
+
error: `Currency ${paymentCurrency.symbol} is not supported.`,
|
|
40
|
+
supported: false,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if token has address mapping
|
|
45
|
+
if (!hasTokenAddress(paymentCurrency.symbol, paymentCurrency.payment_method?.type as ChainType)) {
|
|
46
|
+
return res.status(400).json({
|
|
47
|
+
error: `Currency ${paymentCurrency.symbol} is not supported.`,
|
|
48
|
+
supported: false,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try to fetch exchange rate
|
|
53
|
+
try {
|
|
54
|
+
const rateSymbol = getExchangeRateSymbol(
|
|
55
|
+
paymentCurrency.symbol,
|
|
56
|
+
paymentCurrency.payment_method?.type as ChainType
|
|
57
|
+
);
|
|
58
|
+
const rateResult = await exchangeRateService.getRate(rateSymbol);
|
|
59
|
+
|
|
60
|
+
// Return full rate info consistent with checkout-sessions exchange-rate endpoint
|
|
61
|
+
return res.json({
|
|
62
|
+
supported: true,
|
|
63
|
+
currency: paymentCurrency.symbol,
|
|
64
|
+
base_currency: 'USD',
|
|
65
|
+
rate: rateResult.rate,
|
|
66
|
+
timestamp_ms: rateResult.timestamp_ms,
|
|
67
|
+
fetched_at: rateResult.fetched_at,
|
|
68
|
+
provider_id: rateResult.provider_id,
|
|
69
|
+
provider_name: rateResult.provider_name,
|
|
70
|
+
provider_display: rateResult.provider_display,
|
|
71
|
+
providers: rateResult.providers,
|
|
72
|
+
consensus_method: rateResult.consensus_method,
|
|
73
|
+
degraded: rateResult.degraded,
|
|
74
|
+
degraded_reason: rateResult.degraded_reason,
|
|
75
|
+
});
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
return res.status(400).json({
|
|
78
|
+
error: `Failed to fetch exchange rate for ${paymentCurrency.symbol}: ${error.message}`,
|
|
79
|
+
supported: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch (error: any) {
|
|
83
|
+
return res.status(400).json({ error: error.message });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export default router;
|
package/api/src/routes/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ import products from './products';
|
|
|
28
28
|
import promotionCodes from './promotion-codes';
|
|
29
29
|
import redirect from './redirect';
|
|
30
30
|
import refunds from './refunds';
|
|
31
|
+
import exchangeRateProviders from './exchange-rate-providers';
|
|
32
|
+
import exchangeRates from './exchange-rates';
|
|
31
33
|
import settings from './settings';
|
|
32
34
|
import subscriptionItems from './subscription-items';
|
|
33
35
|
import subscriptions from './subscriptions';
|
|
@@ -82,6 +84,8 @@ router.use('/pricing-tables', pricingTables);
|
|
|
82
84
|
router.use('/tax-rates', taxRates);
|
|
83
85
|
router.use('/products', products);
|
|
84
86
|
router.use('/promotion-codes', promotionCodes);
|
|
87
|
+
router.use('/exchange-rate-providers', exchangeRateProviders);
|
|
88
|
+
router.use('/exchange-rates', exchangeRates);
|
|
85
89
|
router.use('/payouts', payouts);
|
|
86
90
|
router.use('/redirect', redirect);
|
|
87
91
|
router.use('/refunds', refunds);
|