@fiatden/openpay 0.0.0-stage → 0.0.1
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 +269 -2
- package/dist/analytics.test.d.ts +2 -0
- package/dist/analytics.test.d.ts.map +1 -0
- package/dist/analytics.test.js +184 -0
- package/dist/apps.test.d.ts +2 -0
- package/dist/apps.test.d.ts.map +1 -0
- package/dist/apps.test.js +92 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +56 -0
- package/dist/http.test.d.ts +2 -0
- package/dist/http.test.d.ts.map +1 -0
- package/dist/http.test.js +47 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +190 -0
- package/dist/resources/analytics.d.ts +215 -0
- package/dist/resources/analytics.d.ts.map +1 -0
- package/dist/resources/analytics.js +66 -0
- package/dist/resources/apiKeys.d.ts +22 -0
- package/dist/resources/apiKeys.d.ts.map +1 -0
- package/dist/resources/apiKeys.js +40 -0
- package/dist/resources/apps.d.ts +108 -0
- package/dist/resources/apps.d.ts.map +1 -0
- package/dist/resources/apps.js +32 -0
- package/dist/resources/auth.d.ts +71 -0
- package/dist/resources/auth.d.ts.map +1 -0
- package/dist/resources/auth.js +71 -0
- package/dist/resources/billing.d.ts +148 -0
- package/dist/resources/billing.d.ts.map +1 -0
- package/dist/resources/billing.js +55 -0
- package/dist/resources/charges.d.ts +67 -0
- package/dist/resources/charges.d.ts.map +1 -0
- package/dist/resources/charges.js +103 -0
- package/dist/resources/checkout.d.ts +80 -0
- package/dist/resources/checkout.d.ts.map +1 -0
- package/dist/resources/checkout.js +36 -0
- package/dist/resources/coupons.d.ts +66 -0
- package/dist/resources/coupons.d.ts.map +1 -0
- package/dist/resources/coupons.js +39 -0
- package/dist/resources/currencies.d.ts +45 -0
- package/dist/resources/currencies.d.ts.map +1 -0
- package/dist/resources/currencies.js +48 -0
- package/dist/resources/customers.d.ts +135 -0
- package/dist/resources/customers.d.ts.map +1 -0
- package/dist/resources/customers.js +50 -0
- package/dist/resources/disputes.d.ts +48 -0
- package/dist/resources/disputes.d.ts.map +1 -0
- package/dist/resources/disputes.js +47 -0
- package/dist/resources/emailTemplates.d.ts +55 -0
- package/dist/resources/emailTemplates.d.ts.map +1 -0
- package/dist/resources/emailTemplates.js +49 -0
- package/dist/resources/fx.d.ts +34 -0
- package/dist/resources/fx.d.ts.map +1 -0
- package/dist/resources/fx.js +27 -0
- package/dist/resources/invoices.d.ts +32 -0
- package/dist/resources/invoices.d.ts.map +1 -0
- package/dist/resources/invoices.js +36 -0
- package/dist/resources/merchants.d.ts +77 -0
- package/dist/resources/merchants.d.ts.map +1 -0
- package/dist/resources/merchants.js +97 -0
- package/dist/resources/metering.d.ts +65 -0
- package/dist/resources/metering.d.ts.map +1 -0
- package/dist/resources/metering.js +36 -0
- package/dist/resources/paymentLinks.d.ts +45 -0
- package/dist/resources/paymentLinks.d.ts.map +1 -0
- package/dist/resources/paymentLinks.js +32 -0
- package/dist/resources/payouts.d.ts +49 -0
- package/dist/resources/payouts.d.ts.map +1 -0
- package/dist/resources/payouts.js +55 -0
- package/dist/resources/providers.d.ts +52 -0
- package/dist/resources/providers.d.ts.map +1 -0
- package/dist/resources/providers.js +38 -0
- package/dist/resources/reconciliation.d.ts +125 -0
- package/dist/resources/reconciliation.d.ts.map +1 -0
- package/dist/resources/reconciliation.js +50 -0
- package/dist/resources/referrals.d.ts +25 -0
- package/dist/resources/referrals.d.ts.map +1 -0
- package/dist/resources/referrals.js +16 -0
- package/dist/resources/riskReview.d.ts +96 -0
- package/dist/resources/riskReview.d.ts.map +1 -0
- package/dist/resources/riskReview.js +44 -0
- package/dist/resources/roles.d.ts +59 -0
- package/dist/resources/roles.d.ts.map +1 -0
- package/dist/resources/roles.js +42 -0
- package/dist/resources/subscriptions.d.ts +42 -0
- package/dist/resources/subscriptions.d.ts.map +1 -0
- package/dist/resources/subscriptions.js +61 -0
- package/dist/resources/totp.d.ts +36 -0
- package/dist/resources/totp.d.ts.map +1 -0
- package/dist/resources/totp.js +41 -0
- package/dist/resources/wallets.d.ts +124 -0
- package/dist/resources/wallets.d.ts.map +1 -0
- package/dist/resources/wallets.js +68 -0
- package/dist/resources/webhooks.d.ts +47 -0
- package/dist/resources/webhooks.d.ts.map +1 -0
- package/dist/resources/webhooks.js +41 -0
- package/dist/types.d.ts +484 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/package.json +46 -4
package/README.md
CHANGED
|
@@ -1,3 +1,270 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @openpay/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official TypeScript SDK for **OpenPay** — the Stripe-alternative payment orchestrator. One integration, multiple African payment providers (Paystack, Flutterwave, Korapay, Hyperswitch), multi-app workspaces, subscriptions, coupons, metering, payouts, and full platform analytics.
|
|
4
|
+
|
|
5
|
+
- **TypeScript-first** — full type definitions for every request and response
|
|
6
|
+
- **23 resources** — charges, checkout, subscriptions, coupons, customers, apps, payouts, analytics, and more
|
|
7
|
+
- **Node 18+** — uses the global `fetch`, zero runtime dependencies
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @openpay/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { OpenPayApi, OpenPayError } from '@openpay/sdk';
|
|
19
|
+
|
|
20
|
+
const openpay = new OpenPayApi('pk_your_api_key', {
|
|
21
|
+
baseUrl: 'https://api.your-domain.com', // default: http://localhost:3000
|
|
22
|
+
platformName: 'Open Pay', // shown in emails/receipts (optional)
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const charge = await openpay.charges.create({
|
|
27
|
+
amount: 500000, // ₦5,000 in kobo
|
|
28
|
+
email: 'customer@gmail.com',
|
|
29
|
+
merchant_id: 'merchant-uuid',
|
|
30
|
+
});
|
|
31
|
+
console.log(charge.authorization_url); // redirect the customer here
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err instanceof OpenPayError) {
|
|
34
|
+
console.error(`${err.status} ${err.code}: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
Pass any valid API key. Keys created with `openpay.apiKeys.create()` are returned **once** — store the secret immediately. Two capabilities are worth knowing:
|
|
42
|
+
|
|
43
|
+
- **Scopes** — keys carry `v1:read` / `v1:write` scopes; write operations require a key with `v1:write`.
|
|
44
|
+
- **Workspace binding** — a key can be bound to one app (workspace). Calls made with a bound key are automatically scoped to that workspace for app-aware resources (charges, checkout, coupons, plans, payouts). You never need to repeat `app_id` on every call, but an explicit `app_id` parameter always wins.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const { api_key, note } = await openpay.apiKeys.create({
|
|
48
|
+
name: 'Production server key',
|
|
49
|
+
scopes: ['v1:read', 'v1:write'],
|
|
50
|
+
// app_id: 'app-uuid', // optional: bind this key to one workspace
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Workspaces (apps)
|
|
55
|
+
|
|
56
|
+
Apps are the platform's **workspaces**: each one partitions API keys, webhook endpoints, hosted-checkout branding, transactions, subscription plans, subscriptions, coupons, disputes, and provider credentials. Use them to run a marketplace, a SaaS product, and a newsletter business on one merchant account with full isolation.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// 1. Create a workspace (one-time secret pair — save it immediately)
|
|
60
|
+
const app = await openpay.apps.create({ name: 'Marketplace', slug: 'marketplace' });
|
|
61
|
+
console.log(app.publishable_key, app.secret_key); // shown once
|
|
62
|
+
|
|
63
|
+
// 2. Every app-scoped resource accepts an explicit app_id
|
|
64
|
+
await openpay.coupons.create({
|
|
65
|
+
code: 'WELCOME10',
|
|
66
|
+
discount_type: 'percentage',
|
|
67
|
+
discount_value: 1000, // 10% (basis points)
|
|
68
|
+
app_id: app.id, // scoped to this workspace only
|
|
69
|
+
});
|
|
70
|
+
await openpay.subscriptions.createPlan({
|
|
71
|
+
merchant_id: 'merchant-uuid',
|
|
72
|
+
name: 'Pro monthly',
|
|
73
|
+
amount: 2500000,
|
|
74
|
+
currency: 'NGN',
|
|
75
|
+
interval_unit: 'month',
|
|
76
|
+
interval_count: 1,
|
|
77
|
+
app_id: app.id,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 3. Omit app_id → merchant-wide (usable by every workspace)
|
|
81
|
+
await openpay.coupons.create({ code: 'SITEWIDE10', /* … */ });
|
|
82
|
+
|
|
83
|
+
// 4. Workspace rollups
|
|
84
|
+
const stats = await openpay.apps.get(app.id); // volume, subs, disputes
|
|
85
|
+
const analytics = await openpay.apps.analytics(app.id, '30d');
|
|
86
|
+
|
|
87
|
+
// 5. Lost secret? Rotate it (old secret is revoked immediately)
|
|
88
|
+
const rotated = await openpay.apps.rotateSecret(app.id);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Coupon scoping rules** (enforced server-side): a coupon with `app_id` set is redeemable only in that workspace; a coupon with `app_id: null` is redeemable everywhere for that merchant. The same code may exist once merchant-wide and once per workspace.
|
|
92
|
+
|
|
93
|
+
## Resources
|
|
94
|
+
|
|
95
|
+
| Resource | Methods |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `openpay.charges` | `create`, `retrieve`, `refund`, `tokenize`, `invoice`, `sendInvoice` |
|
|
98
|
+
| `openpay.checkout` | `create`, `get` |
|
|
99
|
+
| `openpay.subscriptions` | `createPlan`, `listPlans`, `create`, `list`, `resume`, `cancel` |
|
|
100
|
+
| `openpay.coupons` | `create`, `list`, `get`, `update`, `deactivate`, `validate` |
|
|
101
|
+
| `openpay.customers` | `get`, `paymentMethods`, `transactions`, `subscriptions`, `disputes`, `recomputeRisk`, `update`, `exportData`, `erase` |
|
|
102
|
+
| `openpay.apps` | `create`, `list`, `get`, `update`, `rotateSecret`, `archive`, `analytics` |
|
|
103
|
+
| `openpay.payouts` | `create`, `retrieve`, `list`, `listScheduled`, `createBulk` |
|
|
104
|
+
| `openpay.disputes` | `list`, `get`, `listEvidenceSubmissions`, `markSubmitted` |
|
|
105
|
+
| `openpay.apiKeys` | `create`, `list`, `rotate`, `revoke`, `update` |
|
|
106
|
+
| `openpay.webhooks` | `list`, `retrieve`, `update`, `delete`, `deliveries`, `test` |
|
|
107
|
+
| `openpay.metering` | `record`, `report`, `history`, `createMeter`, `listMeters`, `deactivateMeter` |
|
|
108
|
+
| `openpay.reconciliation` | `upload`, `listReports`, `getReport`, `listUnreconciled`, `clearOperatorHold` |
|
|
109
|
+
| `openpay.fx` | `list`, `set` |
|
|
110
|
+
| `openpay.currencies` | `list`, `upsert`, `update`, `deactivate` |
|
|
111
|
+
| `openpay.analytics` | `getOverview`, `getMerchantAnalytics`, `getPnl`, `getTrialBalance`, `getCircuitStatus`, `listFxRates`, `getFraudAnalytics`, `getChargebackRatios` |
|
|
112
|
+
| `openpay.providers` | `listConfigs`, `deleteConfig`, `getFees` |
|
|
113
|
+
| `openpay.customers` (CRM) | see `exportData` / `erase` for GDPR flows |
|
|
114
|
+
| `openpay.paymentLinks` | `create`, `list`, `get`, `deactivate` |
|
|
115
|
+
| `openpay.referrals` | `myReferral`, `commissions` |
|
|
116
|
+
| `openpay.riskReview` | `list`, `get`, `approve`, `decline`, `stats` |
|
|
117
|
+
| `openpay.emailTemplates` | `get`, `create`, `update`, `delete`, `preview` |
|
|
118
|
+
| `openpay.totp` | `setup`, `verify`, `disable`, `status`, `submitChallenge` |
|
|
119
|
+
| `openpay.auth` | `register`, `login`, `me`, `refresh`, `logout`, `verifyEmail`, `resendVerification`, `changePassword`, `forgotPassword`, `resetPassword`, `deleteAccount` |
|
|
120
|
+
|
|
121
|
+
## Payments
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
// Direct charge (provider is auto-routed by currency and availability)
|
|
125
|
+
const charge = await openpay.charges.create({
|
|
126
|
+
amount: 500000,
|
|
127
|
+
email: 'customer@gmail.com',
|
|
128
|
+
merchant_id: 'merchant-uuid',
|
|
129
|
+
currency: 'NGN',
|
|
130
|
+
// app_id: 'app-uuid', // optional workspace attribution
|
|
131
|
+
// coupon_code: 'WELCOME10', // app-scoped coupons are honored here
|
|
132
|
+
// metadata: { order_id: '123' },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Hosted checkout (branded per workspace via the app's colors/logo)
|
|
136
|
+
const session = await openpay.checkout.create({
|
|
137
|
+
amount: 1000000,
|
|
138
|
+
email: 'customer@gmail.com',
|
|
139
|
+
success_url: 'https://yourapp.com/thanks',
|
|
140
|
+
cancel_url: 'https://yourapp.com/cancel',
|
|
141
|
+
});
|
|
142
|
+
window.open(session.checkout_url);
|
|
143
|
+
|
|
144
|
+
// Verify, refund
|
|
145
|
+
const c = await openpay.charges.retrieve(charge.reference);
|
|
146
|
+
await openpay.charges.refund(charge.reference, { amount: 100000 }); // partial
|
|
147
|
+
|
|
148
|
+
// Recurring: charge a saved authorization (from a prior payment)
|
|
149
|
+
await openpay.charges.tokenize({
|
|
150
|
+
amount: 2500000,
|
|
151
|
+
email: 'customer@gmail.com',
|
|
152
|
+
auth_token: 'AUTH_xxx',
|
|
153
|
+
reference: 'order-42-renewal',
|
|
154
|
+
currency: 'NGN',
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Subscriptions
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const plan = await openpay.subscriptions.createPlan({
|
|
162
|
+
merchant_id: 'merchant-uuid',
|
|
163
|
+
name: 'Pro monthly',
|
|
164
|
+
amount: 2500000,
|
|
165
|
+
currency: 'NGN',
|
|
166
|
+
interval_unit: 'month', // day | week | month | quarter | year
|
|
167
|
+
interval_count: 1,
|
|
168
|
+
trial_days: 14, // optional
|
|
169
|
+
// app_id: 'app-uuid',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const sub = await openpay.subscriptions.create({
|
|
173
|
+
plan_id: plan.id,
|
|
174
|
+
customer_id: 'customer-uuid',
|
|
175
|
+
payment_method_id: 'pm-uuid', // a reusable card from a prior charge
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await openpay.subscriptions.cancel(sub.id); // ends at period end
|
|
179
|
+
await openpay.subscriptions.resume(sub.id);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Billing runs server-side: the platform's durable billing-attempt ledger reserves wallet credit, charges only the uncovered remainder through the provider, and idempotently finalizes from provider webhooks — plan changes mid-cycle bank a proration credit instead of double-charging.
|
|
183
|
+
|
|
184
|
+
## Webhooks
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const hooks = await openpay.webhooks.list();
|
|
188
|
+
await openpay.webhooks.update(hooks.data[0].id, {
|
|
189
|
+
url: 'https://yourapp.com/api/webhooks',
|
|
190
|
+
events: ['charge.success', 'charge.failed'],
|
|
191
|
+
});
|
|
192
|
+
const deliveries = await openpay.webhooks.deliveries(hooks.data[0].id);
|
|
193
|
+
await openpay.webhooks.test(hooks.data[0].id); // sends a test event
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Inbound provider webhooks (Paystack/Korapay/Flutterwave/Hyperswitch → `POST /v1/webhooks/<provider>`) are verified with HMAC over the **raw request body** before any processing.
|
|
197
|
+
|
|
198
|
+
## Metering & usage-based billing
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
await openpay.metering.record('api_calls', 1, sub.id);
|
|
202
|
+
const report = await openpay.metering.report('api_calls', 'day');
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Analytics & finance
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
await openpay.analytics.getOverview();
|
|
209
|
+
await openpay.analytics.getPnl({ period_start: '2024-01-01', period_end: '2024-01-31' });
|
|
210
|
+
await openpay.analytics.getTrialBalance(); // double-entry integrity check
|
|
211
|
+
await openpay.analytics.getCircuitStatus(); // provider health
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Error handling
|
|
215
|
+
|
|
216
|
+
All errors throw `OpenPayError`:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { OpenPayError } from '@openpay/sdk';
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await openpay.charges.create({ /* … */ });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
if (err instanceof OpenPayError) {
|
|
225
|
+
switch (err.status) {
|
|
226
|
+
case 400: // validation_error — err.code carries the field detail
|
|
227
|
+
break;
|
|
228
|
+
case 401: // authentication_error — check the API key
|
|
229
|
+
break;
|
|
230
|
+
case 403: // authorization_error — scope or tenant violation
|
|
231
|
+
break;
|
|
232
|
+
case 409: // conflict — duplicate reference, etc.
|
|
233
|
+
break;
|
|
234
|
+
case 429: // rate_limit_error — back off and retry
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
| Status | Type | Meaning |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| 400 | `validation_error` | Invalid parameters; `err.code` has the zod detail |
|
|
244
|
+
| 401 | `authentication_error` | Missing/invalid API key or session |
|
|
245
|
+
| 403 | `authorization_error` | Scope missing, wrong tenant, or CSRF failure |
|
|
246
|
+
| 404 | `not_found` | Resource doesn't exist or belongs to another tenant |
|
|
247
|
+
| 409 | `conflict` | Duplicate (e.g. reference or code already used) |
|
|
248
|
+
| 429 | `rate_limit_error` | Too many requests |
|
|
249
|
+
| 500 | `api_error` | Server-side failure — safe to retry with the same idempotency key |
|
|
250
|
+
|
|
251
|
+
## Idempotency & safety notes
|
|
252
|
+
|
|
253
|
+
- Payouts accept `idempotency_key` — retries return the original payout instead of double-paying.
|
|
254
|
+
- Charge references are unique per merchant; retrying with the same reference is safe.
|
|
255
|
+
- Secrets (`secret_key`, API keys) are returned exactly once at creation/rotation.
|
|
256
|
+
- Platform provider credentials (env keys) are **test-only**: production merchant traffic must use merchant-configured provider credentials.
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npm install
|
|
262
|
+
npm run typecheck # tsc --noEmit
|
|
263
|
+
npm test # vitest
|
|
264
|
+
npm run build # emit dist/
|
|
265
|
+
npm publish # runs prepublishOnly: typecheck → test → build
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics.test.d.ts","sourceRoot":"","sources":["../src/analytics.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const analytics_1 = require("./resources/analytics");
|
|
5
|
+
// Mock global fetch
|
|
6
|
+
const mockFetch = vitest_1.vi.fn();
|
|
7
|
+
global.fetch = mockFetch;
|
|
8
|
+
function createMockClient(responseData, status = 200) {
|
|
9
|
+
mockFetch.mockResolvedValueOnce({
|
|
10
|
+
ok: status >= 200 && status < 300,
|
|
11
|
+
status,
|
|
12
|
+
json: async () => responseData,
|
|
13
|
+
});
|
|
14
|
+
const apiKey = 'pk_test_123';
|
|
15
|
+
// Inline a minimal HttpClient since we can't easily import it without
|
|
16
|
+
// triggering the actual fetch implementation
|
|
17
|
+
const client = {
|
|
18
|
+
request: async (path, options) => {
|
|
19
|
+
const { method = 'GET', body, params } = options || {};
|
|
20
|
+
let url = `http://localhost:3000${path}`;
|
|
21
|
+
if (params) {
|
|
22
|
+
const qs = Object.entries(params)
|
|
23
|
+
.filter(([, v]) => v !== undefined)
|
|
24
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
25
|
+
.join('&');
|
|
26
|
+
if (qs)
|
|
27
|
+
url += `?${qs}`;
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method,
|
|
31
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
32
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
33
|
+
});
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
throw new Error(data.message || `HTTP ${response.status}`);
|
|
37
|
+
return data;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return { client };
|
|
41
|
+
}
|
|
42
|
+
(0, vitest_1.describe)('AnalyticsResource', () => {
|
|
43
|
+
(0, vitest_1.beforeEach)(() => {
|
|
44
|
+
vitest_1.vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)('getOverview returns platform overview metrics', async () => {
|
|
47
|
+
const overview = {
|
|
48
|
+
total_transactions: 1500,
|
|
49
|
+
total_volume: 750000000,
|
|
50
|
+
active_merchants: 42,
|
|
51
|
+
active_subscriptions: 88,
|
|
52
|
+
platform_revenue: 18750000,
|
|
53
|
+
};
|
|
54
|
+
const { client } = createMockClient(overview);
|
|
55
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
56
|
+
const result = await analytics.getOverview();
|
|
57
|
+
(0, vitest_1.expect)(result).toEqual(overview);
|
|
58
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/overview', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('getMerchantAnalytics fetches per-merchant data', async () => {
|
|
61
|
+
const merchantData = { merchant_id: 'uuid-123', total_volume: 500000 };
|
|
62
|
+
const { client } = createMockClient(merchantData);
|
|
63
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
64
|
+
const result = await analytics.getMerchantAnalytics('uuid-123');
|
|
65
|
+
(0, vitest_1.expect)(result).toEqual(merchantData);
|
|
66
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/merchant/uuid-123', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)('getPnl returns P&L statement with date range', async () => {
|
|
69
|
+
const pnl = {
|
|
70
|
+
period: { start: '2024-01-01T00:00:00Z', end: '2024-01-31T23:59:59Z' },
|
|
71
|
+
income: { platform_commissions: 500000, fx_revenue: 12000, total_revenue: 512000 },
|
|
72
|
+
expenses: { provider_fees: 80000, coupon_discounts: 25000, referral_commissions: 15000, total_expenses: 120000 },
|
|
73
|
+
net_profit: 392000,
|
|
74
|
+
metrics: { gross_volume: 20000000, transaction_count: 400, take_rate_pct: 2.5, profit_margin_pct: 76.56 },
|
|
75
|
+
currency: 'NGN',
|
|
76
|
+
};
|
|
77
|
+
const { client } = createMockClient(pnl);
|
|
78
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
79
|
+
const result = await analytics.getPnl({ period_start: '2024-01-01', period_end: '2024-01-31' });
|
|
80
|
+
(0, vitest_1.expect)(result).toEqual(pnl);
|
|
81
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/pnl?period_start=2024-01-01&period_end=2024-01-31', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
82
|
+
});
|
|
83
|
+
(0, vitest_1.it)('getPnl works without params', async () => {
|
|
84
|
+
const pnl = { period: {}, net_profit: 0, currency: 'NGN' };
|
|
85
|
+
const { client } = createMockClient(pnl);
|
|
86
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
87
|
+
const result = await analytics.getPnl();
|
|
88
|
+
(0, vitest_1.expect)(result).toEqual(pnl);
|
|
89
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/pnl', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
90
|
+
});
|
|
91
|
+
(0, vitest_1.it)('getTrialBalance returns balanced trial balance', async () => {
|
|
92
|
+
const tb = {
|
|
93
|
+
as_of: '2024-01-15T00:00:00Z',
|
|
94
|
+
credits: [{ category: 'Payment Revenue', total: 1000000 }],
|
|
95
|
+
debits: [{ category: 'Merchant Settlements', total: 1000000 }],
|
|
96
|
+
summary: { total_credits: 1000000, total_debits: 1000000, net: 0, balanced: true },
|
|
97
|
+
wallet_liabilities: { available_balance: 500000, pending_balance: 100000, locked_balance: 0, total: 600000 },
|
|
98
|
+
currency: 'NGN',
|
|
99
|
+
};
|
|
100
|
+
const { client } = createMockClient(tb);
|
|
101
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
102
|
+
const result = await analytics.getTrialBalance('2024-01-15');
|
|
103
|
+
(0, vitest_1.expect)(result).toEqual(tb);
|
|
104
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/trial-balance?as_of=2024-01-15', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
105
|
+
});
|
|
106
|
+
(0, vitest_1.it)('getTrialBalance works without date', async () => {
|
|
107
|
+
const tb = { summary: { balanced: true } };
|
|
108
|
+
const { client } = createMockClient(tb);
|
|
109
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
110
|
+
const result = await analytics.getTrialBalance();
|
|
111
|
+
(0, vitest_1.expect)(result).toEqual(tb);
|
|
112
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/trial-balance', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)('getCircuitStatus returns provider states', async () => {
|
|
115
|
+
const circuits = {
|
|
116
|
+
data: [
|
|
117
|
+
{ provider: 'paystack', circuit: 'closed', open_until: null, failure_count: 0, last_failure_at: null },
|
|
118
|
+
{ provider: 'flutterwave', circuit: 'open', open_until: '2024-01-01T00:00:00Z', failure_count: 7, last_failure_at: '2023-12-31T23:59:00Z' },
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
const { client } = createMockClient(circuits);
|
|
122
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
123
|
+
const result = await analytics.getCircuitStatus();
|
|
124
|
+
(0, vitest_1.expect)(result).toEqual(circuits);
|
|
125
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/circuits', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.it)('listFxRates returns active rates', async () => {
|
|
128
|
+
const rates = {
|
|
129
|
+
data: [
|
|
130
|
+
{ base: 'USD', target: 'NGN', rate: 1550.25, source: 'manual', spread: 0.5 },
|
|
131
|
+
{ base: 'NGN', target: 'GHS', rate: 0.0083, source: 'auto', spread: 1.0 },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
const { client } = createMockClient(rates);
|
|
135
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
136
|
+
const result = await analytics.listFxRates();
|
|
137
|
+
(0, vitest_1.expect)(result).toEqual(rates);
|
|
138
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/fx-rates', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.it)('setFxRate creates a new rate', async () => {
|
|
141
|
+
const newRate = { base: 'USD', target: 'NGN', rate: 1600, source: 'manual', spread: 0.5 };
|
|
142
|
+
const { client } = createMockClient(newRate, 201);
|
|
143
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
144
|
+
const result = await analytics.setFxRate({
|
|
145
|
+
base_currency: 'USD',
|
|
146
|
+
target_currency: 'NGN',
|
|
147
|
+
rate: 1600,
|
|
148
|
+
source: 'manual',
|
|
149
|
+
});
|
|
150
|
+
(0, vitest_1.expect)(result).toEqual(newRate);
|
|
151
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/analytics/fx-rates', vitest_1.expect.objectContaining({
|
|
152
|
+
method: 'POST',
|
|
153
|
+
body: JSON.stringify({ base_currency: 'USD', target_currency: 'NGN', rate: 1600, source: 'manual' }),
|
|
154
|
+
}));
|
|
155
|
+
});
|
|
156
|
+
(0, vitest_1.it)('getAuditLogs fetches with filters', async () => {
|
|
157
|
+
const logs = { data: [{ id: '1', action: 'charge.success' }], total: 1, has_more: false };
|
|
158
|
+
const { client } = createMockClient(logs);
|
|
159
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
160
|
+
const result = await analytics.getAuditLogs({ action: 'charge.success', limit: 10 });
|
|
161
|
+
(0, vitest_1.expect)(result).toEqual(logs);
|
|
162
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/audit-logs?action=charge.success&limit=10', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
163
|
+
});
|
|
164
|
+
(0, vitest_1.it)('getAuditLogs works without params', async () => {
|
|
165
|
+
const logs = { data: [], total: 0, has_more: false };
|
|
166
|
+
const { client } = createMockClient(logs);
|
|
167
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
168
|
+
const result = await analytics.getAuditLogs();
|
|
169
|
+
(0, vitest_1.expect)(result).toEqual(logs);
|
|
170
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/audit-logs', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
171
|
+
});
|
|
172
|
+
(0, vitest_1.it)('skips undefined params in query string', async () => {
|
|
173
|
+
const logs = { data: [], total: 0, has_more: false };
|
|
174
|
+
const { client } = createMockClient(logs);
|
|
175
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
176
|
+
await analytics.getAuditLogs({ action: 'charge.success', resource_type: undefined, limit: 20 });
|
|
177
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/audit-logs?action=charge.success&limit=20', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
178
|
+
});
|
|
179
|
+
(0, vitest_1.it)('propagates API errors', async () => {
|
|
180
|
+
const { client } = createMockClient({ message: 'Not found' }, 404);
|
|
181
|
+
const analytics = new analytics_1.AnalyticsResource(client);
|
|
182
|
+
await (0, vitest_1.expect)(analytics.getOverview()).rejects.toThrow('Not found');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apps.test.d.ts","sourceRoot":"","sources":["../src/apps.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const apps_1 = require("./resources/apps");
|
|
5
|
+
// Mock global fetch
|
|
6
|
+
const mockFetch = vitest_1.vi.fn();
|
|
7
|
+
global.fetch = mockFetch;
|
|
8
|
+
function createMockClient(responseData, status = 200) {
|
|
9
|
+
mockFetch.mockResolvedValueOnce({
|
|
10
|
+
ok: status >= 200 && status < 300,
|
|
11
|
+
status,
|
|
12
|
+
json: async () => responseData,
|
|
13
|
+
});
|
|
14
|
+
const client = {
|
|
15
|
+
request: async (path, options) => {
|
|
16
|
+
const { method = 'GET', body } = options || {};
|
|
17
|
+
const response = await fetch(`http://localhost:3000${path}`, {
|
|
18
|
+
method,
|
|
19
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': 'pk_test_123' },
|
|
20
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
21
|
+
});
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
throw new Error(data.message || `HTTP ${response.status}`);
|
|
25
|
+
return data;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return { client };
|
|
29
|
+
}
|
|
30
|
+
(0, vitest_1.describe)('AppsResource', () => {
|
|
31
|
+
(0, vitest_1.beforeEach)(() => {
|
|
32
|
+
vitest_1.vi.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.it)('create posts params and returns the one-time secret pair', async () => {
|
|
35
|
+
const created = {
|
|
36
|
+
id: 'app-1', name: 'Marketplace', slug: 'marketplace',
|
|
37
|
+
publishable_key: 'pk_app1_abc', secret_key: 'sk_app1_xyz',
|
|
38
|
+
warning: 'Save the secret_key now',
|
|
39
|
+
};
|
|
40
|
+
const { client } = createMockClient(created, 201);
|
|
41
|
+
const apps = new apps_1.AppsResource(client);
|
|
42
|
+
const result = await apps.create({ name: 'Marketplace', slug: 'marketplace' });
|
|
43
|
+
(0, vitest_1.expect)(result.secret_key).toBe('sk_app1_xyz');
|
|
44
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/apps', vitest_1.expect.objectContaining({ method: 'POST' }));
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)('list appends status filter as query string', async () => {
|
|
47
|
+
const { client } = createMockClient({ data: [], has_more: false });
|
|
48
|
+
const apps = new apps_1.AppsResource(client);
|
|
49
|
+
await apps.list({ status: 'archived', limit: 10 });
|
|
50
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/apps?status=archived&limit=10', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)('get fetches app detail with stats', async () => {
|
|
53
|
+
const { client } = createMockClient({ id: 'app-1', stats: { transaction_count: 5 } });
|
|
54
|
+
const apps = new apps_1.AppsResource(client);
|
|
55
|
+
const result = await apps.get('app-1');
|
|
56
|
+
(0, vitest_1.expect)(result.stats.transaction_count).toBe(5);
|
|
57
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/apps/app-1', vitest_1.expect.anything());
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)('update PATCHes partial fields', async () => {
|
|
60
|
+
const { client } = createMockClient({ id: 'app-1', name: 'Renamed' });
|
|
61
|
+
const apps = new apps_1.AppsResource(client);
|
|
62
|
+
const result = await apps.update('app-1', { name: 'Renamed' });
|
|
63
|
+
(0, vitest_1.expect)(result.name).toBe('Renamed');
|
|
64
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/apps/app-1', vitest_1.expect.objectContaining({ method: 'PATCH' }));
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.it)('rotateSecret POSTs the rotate endpoint', async () => {
|
|
67
|
+
const { client } = createMockClient({ app_id: 'app-1', secret_key: 'sk_new', warning: 'save it' });
|
|
68
|
+
const apps = new apps_1.AppsResource(client);
|
|
69
|
+
const result = await apps.rotateSecret('app-1');
|
|
70
|
+
(0, vitest_1.expect)(result.secret_key).toBe('sk_new');
|
|
71
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/apps/app-1/rotate-secret', vitest_1.expect.objectContaining({ method: 'POST' }));
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('archive DELETEs the app', async () => {
|
|
74
|
+
const { client } = createMockClient({ id: 'app-1', is_active: false });
|
|
75
|
+
const apps = new apps_1.AppsResource(client);
|
|
76
|
+
const result = await apps.archive('app-1');
|
|
77
|
+
(0, vitest_1.expect)(result.is_active).toBe(false);
|
|
78
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/apps/app-1', vitest_1.expect.objectContaining({ method: 'DELETE' }));
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)('analytics defaults to the 30d period', async () => {
|
|
81
|
+
const { client } = createMockClient({ app_id: 'app-1', period: '30d' });
|
|
82
|
+
const apps = new apps_1.AppsResource(client);
|
|
83
|
+
const result = await apps.analytics('app-1');
|
|
84
|
+
(0, vitest_1.expect)(result.period).toBe('30d');
|
|
85
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('http://localhost:3000/v1/apps/app-1/analytics?period=30d', vitest_1.expect.anything());
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)('propagates API errors', async () => {
|
|
88
|
+
const { client } = createMockClient({ message: 'Validation failed: slug: Invalid' }, 400);
|
|
89
|
+
const apps = new apps_1.AppsResource(client);
|
|
90
|
+
await (0, vitest_1.expect)(apps.create({ name: 'X', slug: 'BAD' })).rejects.toThrow('Validation failed');
|
|
91
|
+
});
|
|
92
|
+
});
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type Params = Record<string, any>;
|
|
2
|
+
export interface RequestOptions {
|
|
3
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
4
|
+
body?: unknown;
|
|
5
|
+
params?: Params;
|
|
6
|
+
/** Sent as the Stripe-compatible Idempotency-Key header for mutations. */
|
|
7
|
+
idempotencyKey?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class FTDNApiError extends Error {
|
|
10
|
+
status: number;
|
|
11
|
+
code?: string;
|
|
12
|
+
constructor(message: string, status: number, code?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class HttpClient {
|
|
15
|
+
/** Exposed for resources that need raw fetch (CSV export, HTML invoice). */
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
constructor(apiKey: string, baseUrl: string);
|
|
19
|
+
request<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAGA,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAElC,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAUD,qBAAa,YAAa,SAAQ,KAAK;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;gBACF,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAM3D;AAED,qBAAa,UAAU;IACrB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;gBAEH,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAKrC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC;CAwCzE"}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── HTTP Client ──────────────────────────────────────────────
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.HttpClient = exports.FTDNApiError = void 0;
|
|
5
|
+
class FTDNApiError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, status, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'FTDNApiError';
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.FTDNApiError = FTDNApiError;
|
|
16
|
+
class HttpClient {
|
|
17
|
+
/** Exposed for resources that need raw fetch (CSV export, HTML invoice). */
|
|
18
|
+
baseUrl;
|
|
19
|
+
apiKey;
|
|
20
|
+
constructor(apiKey, baseUrl) {
|
|
21
|
+
this.apiKey = apiKey;
|
|
22
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
23
|
+
}
|
|
24
|
+
async request(path, options = {}) {
|
|
25
|
+
const { method = 'GET', body, params, idempotencyKey } = options;
|
|
26
|
+
const bodyIdempotencyKey = body && typeof body === 'object' && 'idempotency_key' in body
|
|
27
|
+
? String(body.idempotency_key || '')
|
|
28
|
+
: undefined;
|
|
29
|
+
const effectiveIdempotencyKey = idempotencyKey || bodyIdempotencyKey || undefined;
|
|
30
|
+
let url = `${this.baseUrl}${path}`;
|
|
31
|
+
if (params) {
|
|
32
|
+
const qs = Object.entries(params)
|
|
33
|
+
.filter(([, v]) => v !== undefined)
|
|
34
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
35
|
+
.join('&');
|
|
36
|
+
if (qs)
|
|
37
|
+
url += `?${qs}`;
|
|
38
|
+
}
|
|
39
|
+
const headers = {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
'x-api-key': this.apiKey,
|
|
42
|
+
...(effectiveIdempotencyKey ? { 'Idempotency-Key': effectiveIdempotencyKey } : {}),
|
|
43
|
+
};
|
|
44
|
+
const response = await fetch(url, {
|
|
45
|
+
method,
|
|
46
|
+
headers,
|
|
47
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
48
|
+
});
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new FTDNApiError(data.message || data.error || `HTTP ${response.status}`, response.status, data.code);
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.HttpClient = HttpClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.test.d.ts","sourceRoot":"","sources":["../src/http.test.ts"],"names":[],"mappings":""}
|