@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.
Files changed (101) hide show
  1. package/README.md +269 -2
  2. package/dist/analytics.test.d.ts +2 -0
  3. package/dist/analytics.test.d.ts.map +1 -0
  4. package/dist/analytics.test.js +184 -0
  5. package/dist/apps.test.d.ts +2 -0
  6. package/dist/apps.test.d.ts.map +1 -0
  7. package/dist/apps.test.js +92 -0
  8. package/dist/http.d.ts +22 -0
  9. package/dist/http.d.ts.map +1 -0
  10. package/dist/http.js +56 -0
  11. package/dist/http.test.d.ts +2 -0
  12. package/dist/http.test.d.ts.map +1 -0
  13. package/dist/http.test.js +47 -0
  14. package/dist/index.d.ts +145 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +190 -0
  17. package/dist/resources/analytics.d.ts +215 -0
  18. package/dist/resources/analytics.d.ts.map +1 -0
  19. package/dist/resources/analytics.js +66 -0
  20. package/dist/resources/apiKeys.d.ts +22 -0
  21. package/dist/resources/apiKeys.d.ts.map +1 -0
  22. package/dist/resources/apiKeys.js +40 -0
  23. package/dist/resources/apps.d.ts +108 -0
  24. package/dist/resources/apps.d.ts.map +1 -0
  25. package/dist/resources/apps.js +32 -0
  26. package/dist/resources/auth.d.ts +71 -0
  27. package/dist/resources/auth.d.ts.map +1 -0
  28. package/dist/resources/auth.js +71 -0
  29. package/dist/resources/billing.d.ts +148 -0
  30. package/dist/resources/billing.d.ts.map +1 -0
  31. package/dist/resources/billing.js +55 -0
  32. package/dist/resources/charges.d.ts +67 -0
  33. package/dist/resources/charges.d.ts.map +1 -0
  34. package/dist/resources/charges.js +103 -0
  35. package/dist/resources/checkout.d.ts +80 -0
  36. package/dist/resources/checkout.d.ts.map +1 -0
  37. package/dist/resources/checkout.js +36 -0
  38. package/dist/resources/coupons.d.ts +66 -0
  39. package/dist/resources/coupons.d.ts.map +1 -0
  40. package/dist/resources/coupons.js +39 -0
  41. package/dist/resources/currencies.d.ts +45 -0
  42. package/dist/resources/currencies.d.ts.map +1 -0
  43. package/dist/resources/currencies.js +48 -0
  44. package/dist/resources/customers.d.ts +135 -0
  45. package/dist/resources/customers.d.ts.map +1 -0
  46. package/dist/resources/customers.js +50 -0
  47. package/dist/resources/disputes.d.ts +48 -0
  48. package/dist/resources/disputes.d.ts.map +1 -0
  49. package/dist/resources/disputes.js +47 -0
  50. package/dist/resources/emailTemplates.d.ts +55 -0
  51. package/dist/resources/emailTemplates.d.ts.map +1 -0
  52. package/dist/resources/emailTemplates.js +49 -0
  53. package/dist/resources/fx.d.ts +34 -0
  54. package/dist/resources/fx.d.ts.map +1 -0
  55. package/dist/resources/fx.js +27 -0
  56. package/dist/resources/invoices.d.ts +32 -0
  57. package/dist/resources/invoices.d.ts.map +1 -0
  58. package/dist/resources/invoices.js +36 -0
  59. package/dist/resources/merchants.d.ts +77 -0
  60. package/dist/resources/merchants.d.ts.map +1 -0
  61. package/dist/resources/merchants.js +97 -0
  62. package/dist/resources/metering.d.ts +65 -0
  63. package/dist/resources/metering.d.ts.map +1 -0
  64. package/dist/resources/metering.js +36 -0
  65. package/dist/resources/paymentLinks.d.ts +45 -0
  66. package/dist/resources/paymentLinks.d.ts.map +1 -0
  67. package/dist/resources/paymentLinks.js +32 -0
  68. package/dist/resources/payouts.d.ts +49 -0
  69. package/dist/resources/payouts.d.ts.map +1 -0
  70. package/dist/resources/payouts.js +55 -0
  71. package/dist/resources/providers.d.ts +52 -0
  72. package/dist/resources/providers.d.ts.map +1 -0
  73. package/dist/resources/providers.js +38 -0
  74. package/dist/resources/reconciliation.d.ts +125 -0
  75. package/dist/resources/reconciliation.d.ts.map +1 -0
  76. package/dist/resources/reconciliation.js +50 -0
  77. package/dist/resources/referrals.d.ts +25 -0
  78. package/dist/resources/referrals.d.ts.map +1 -0
  79. package/dist/resources/referrals.js +16 -0
  80. package/dist/resources/riskReview.d.ts +96 -0
  81. package/dist/resources/riskReview.d.ts.map +1 -0
  82. package/dist/resources/riskReview.js +44 -0
  83. package/dist/resources/roles.d.ts +59 -0
  84. package/dist/resources/roles.d.ts.map +1 -0
  85. package/dist/resources/roles.js +42 -0
  86. package/dist/resources/subscriptions.d.ts +42 -0
  87. package/dist/resources/subscriptions.d.ts.map +1 -0
  88. package/dist/resources/subscriptions.js +61 -0
  89. package/dist/resources/totp.d.ts +36 -0
  90. package/dist/resources/totp.d.ts.map +1 -0
  91. package/dist/resources/totp.js +41 -0
  92. package/dist/resources/wallets.d.ts +124 -0
  93. package/dist/resources/wallets.d.ts.map +1 -0
  94. package/dist/resources/wallets.js +68 -0
  95. package/dist/resources/webhooks.d.ts +47 -0
  96. package/dist/resources/webhooks.d.ts.map +1 -0
  97. package/dist/resources/webhooks.js +41 -0
  98. package/dist/types.d.ts +484 -0
  99. package/dist/types.d.ts.map +1 -0
  100. package/dist/types.js +15 -0
  101. package/package.json +46 -4
package/README.md CHANGED
@@ -1,3 +1,270 @@
1
- # Temporary Holding Version
1
+ # @openpay/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=analytics.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=apps.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=http.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.test.d.ts","sourceRoot":"","sources":["../src/http.test.ts"],"names":[],"mappings":""}