@fiatden/openpay 0.0.0-stage → 0.0.2

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 (119) hide show
  1. package/README.md +309 -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 +157 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +207 -0
  17. package/dist/index.test.d.ts +2 -0
  18. package/dist/index.test.d.ts.map +1 -0
  19. package/dist/index.test.js +55 -0
  20. package/dist/resources/analytics.d.ts +220 -0
  21. package/dist/resources/analytics.d.ts.map +1 -0
  22. package/dist/resources/analytics.js +70 -0
  23. package/dist/resources/apiKeys.d.ts +22 -0
  24. package/dist/resources/apiKeys.d.ts.map +1 -0
  25. package/dist/resources/apiKeys.js +40 -0
  26. package/dist/resources/apps.d.ts +142 -0
  27. package/dist/resources/apps.d.ts.map +1 -0
  28. package/dist/resources/apps.js +49 -0
  29. package/dist/resources/auth.d.ts +71 -0
  30. package/dist/resources/auth.d.ts.map +1 -0
  31. package/dist/resources/auth.js +71 -0
  32. package/dist/resources/billing.d.ts +148 -0
  33. package/dist/resources/billing.d.ts.map +1 -0
  34. package/dist/resources/billing.js +55 -0
  35. package/dist/resources/charges.d.ts +67 -0
  36. package/dist/resources/charges.d.ts.map +1 -0
  37. package/dist/resources/charges.js +103 -0
  38. package/dist/resources/checkout.d.ts +80 -0
  39. package/dist/resources/checkout.d.ts.map +1 -0
  40. package/dist/resources/checkout.js +36 -0
  41. package/dist/resources/coupons.d.ts +66 -0
  42. package/dist/resources/coupons.d.ts.map +1 -0
  43. package/dist/resources/coupons.js +39 -0
  44. package/dist/resources/currencies.d.ts +45 -0
  45. package/dist/resources/currencies.d.ts.map +1 -0
  46. package/dist/resources/currencies.js +48 -0
  47. package/dist/resources/currencyPortfolio.d.ts +62 -0
  48. package/dist/resources/currencyPortfolio.d.ts.map +1 -0
  49. package/dist/resources/currencyPortfolio.js +34 -0
  50. package/dist/resources/customers.d.ts +171 -0
  51. package/dist/resources/customers.d.ts.map +1 -0
  52. package/dist/resources/customers.js +72 -0
  53. package/dist/resources/disputes.d.ts +48 -0
  54. package/dist/resources/disputes.d.ts.map +1 -0
  55. package/dist/resources/disputes.js +47 -0
  56. package/dist/resources/emailTemplates.d.ts +55 -0
  57. package/dist/resources/emailTemplates.d.ts.map +1 -0
  58. package/dist/resources/emailTemplates.js +49 -0
  59. package/dist/resources/financialAccounts.d.ts +49 -0
  60. package/dist/resources/financialAccounts.d.ts.map +1 -0
  61. package/dist/resources/financialAccounts.js +22 -0
  62. package/dist/resources/fx.d.ts +34 -0
  63. package/dist/resources/fx.d.ts.map +1 -0
  64. package/dist/resources/fx.js +27 -0
  65. package/dist/resources/invoices.d.ts +32 -0
  66. package/dist/resources/invoices.d.ts.map +1 -0
  67. package/dist/resources/invoices.js +36 -0
  68. package/dist/resources/merchants.d.ts +95 -0
  69. package/dist/resources/merchants.d.ts.map +1 -0
  70. package/dist/resources/merchants.js +113 -0
  71. package/dist/resources/metering.d.ts +65 -0
  72. package/dist/resources/metering.d.ts.map +1 -0
  73. package/dist/resources/metering.js +36 -0
  74. package/dist/resources/paymentLinks.d.ts +45 -0
  75. package/dist/resources/paymentLinks.d.ts.map +1 -0
  76. package/dist/resources/paymentLinks.js +32 -0
  77. package/dist/resources/paymentMethods.d.ts +34 -0
  78. package/dist/resources/paymentMethods.d.ts.map +1 -0
  79. package/dist/resources/paymentMethods.js +17 -0
  80. package/dist/resources/payouts.d.ts +51 -0
  81. package/dist/resources/payouts.d.ts.map +1 -0
  82. package/dist/resources/payouts.js +62 -0
  83. package/dist/resources/platform.d.ts +72 -0
  84. package/dist/resources/platform.d.ts.map +1 -0
  85. package/dist/resources/platform.js +42 -0
  86. package/dist/resources/providers.d.ts +61 -0
  87. package/dist/resources/providers.d.ts.map +1 -0
  88. package/dist/resources/providers.js +42 -0
  89. package/dist/resources/reconciliation.d.ts +125 -0
  90. package/dist/resources/reconciliation.d.ts.map +1 -0
  91. package/dist/resources/reconciliation.js +55 -0
  92. package/dist/resources/referrals.d.ts +25 -0
  93. package/dist/resources/referrals.d.ts.map +1 -0
  94. package/dist/resources/referrals.js +16 -0
  95. package/dist/resources/riskReview.d.ts +96 -0
  96. package/dist/resources/riskReview.d.ts.map +1 -0
  97. package/dist/resources/riskReview.js +44 -0
  98. package/dist/resources/roles.d.ts +59 -0
  99. package/dist/resources/roles.d.ts.map +1 -0
  100. package/dist/resources/roles.js +42 -0
  101. package/dist/resources/status.d.ts +20 -0
  102. package/dist/resources/status.d.ts.map +1 -0
  103. package/dist/resources/status.js +14 -0
  104. package/dist/resources/subscriptions.d.ts +46 -0
  105. package/dist/resources/subscriptions.d.ts.map +1 -0
  106. package/dist/resources/subscriptions.js +69 -0
  107. package/dist/resources/totp.d.ts +36 -0
  108. package/dist/resources/totp.d.ts.map +1 -0
  109. package/dist/resources/totp.js +41 -0
  110. package/dist/resources/wallets.d.ts +128 -0
  111. package/dist/resources/wallets.d.ts.map +1 -0
  112. package/dist/resources/wallets.js +74 -0
  113. package/dist/resources/webhooks.d.ts +47 -0
  114. package/dist/resources/webhooks.d.ts.map +1 -0
  115. package/dist/resources/webhooks.js +41 -0
  116. package/dist/types.d.ts +484 -0
  117. package/dist/types.d.ts.map +1 -0
  118. package/dist/types.js +15 -0
  119. package/package.json +46 -4
package/README.md CHANGED
@@ -1,3 +1,310 @@
1
- # Temporary Holding Version
1
+ # @fiatden/openpay
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
+ - **32 resources** — charges, checkout, subscriptions, coupons, customers, apps, payouts, wallets, financial accounts, analytics, and more
7
+ - **Node 18+** — uses the global `fetch`, zero runtime dependencies
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @fiatden/openpay
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { OpenPayApi, OpenPayError } from '@fiatden/openpay';
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`, `getPlan`, `updatePlan`, `create`, `list`, `pause`, `resume`, `changePlan`, `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`, `listCustomFieldSchemas`, `upsertCustomFieldSchema`, `updateCustomFieldSchema`, `deleteCustomFieldSchema` |
103
+ | `openpay.payouts` | `create`, `retrieve`, `list`, `listScheduled`, `createBulk`, `listAwaitingApproval`, `approve`, `reject`, `cancel` |
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`, `listRecords`, `reprocess`, `summary`, `listUnreconciled`, `clearOperatorHold` |
109
+ | `openpay.fx` | `list`, `set` |
110
+ | `openpay.currencies` | `list`, `upsert`, `update`, `deactivate` |
111
+ | `openpay.currencyPortfolio` | `getPortfolio`, `getCatalog`, `enable`, `update`, `remove`, `setBase` |
112
+ | `openpay.analytics` | `getOverview`, `getMerchantAnalytics`, `getReferralAnalytics`, `getPnl`, `getTrialBalance`, `getCircuitStatus`, `listFxRates`, `getFraudAnalytics`, `getChargebackRatios`, `getCustomerOverlap`, `getMarketplace` |
113
+ | `openpay.providers` | `listConfigs`, `getHealth`, `deleteConfig`, `getFees`, `getRouterWeights`, `updateRouterWeights` |
114
+ | `openpay.customers` (CRM) | see `exportData` / `erase` for GDPR flows |
115
+ | `openpay.customers` (fields) | `listCustomFields`, `setCustomField`, `deleteCustomField`, `listConsents`, `recordConsent` |
116
+ | `openpay.paymentMethods` | `list`, `remove` |
117
+ | `openpay.billing` | `createMetric`, `listMetrics`, `getMetric`, `updateMetric`, `deleteMetric`, `ingestEvent`, `ingestEventsBatch`, `listEvents`, `getEvent`, `aggregateMetric`, `aggregateMetricByCode`, `aggregateAll` |
118
+ | `openpay.wallets` | `list`, `get`, `topup`, `createCheckout`, `debit`, `check`, `transactions`, `exportCsv`, `exportLedgerCsv`, `terminate` |
119
+ | `openpay.financialAccounts` | `list`, `get`, `statement` |
120
+ | `openpay.platform` | `getSettings`, `updateSettings`, `getSmtp`, `updateSmtp`, `testSmtp`, `getStatus`, `exportData`, `importData` |
121
+ | `openpay.status` | `get` |
122
+ | `openpay.paymentLinks` | `create`, `list`, `get`, `deactivate` |
123
+ | `openpay.referrals` | `myReferral`, `commissions` |
124
+ | `openpay.riskReview` | `list`, `get`, `approve`, `decline`, `stats` |
125
+ | `openpay.emailTemplates` | `get`, `create`, `update`, `delete`, `preview` |
126
+ | `openpay.totp` | `setup`, `verify`, `disable`, `status`, `submitChallenge` |
127
+ | `openpay.auth` | `register`, `login`, `me`, `refresh`, `logout`, `verifyEmail`, `resendVerification`, `changePassword`, `forgotPassword`, `resetPassword`, `deleteAccount` |
128
+
129
+ ## Payments
130
+
131
+ ```ts
132
+ // Direct charge (provider is auto-routed by currency and availability)
133
+ const charge = await openpay.charges.create({
134
+ amount: 500000,
135
+ email: 'customer@gmail.com',
136
+ merchant_id: 'merchant-uuid',
137
+ currency: 'NGN',
138
+ // app_id: 'app-uuid', // optional workspace attribution
139
+ // coupon_code: 'WELCOME10', // app-scoped coupons are honored here
140
+ // metadata: { order_id: '123' },
141
+ });
142
+
143
+ // Hosted checkout (branded per workspace via the app's colors/logo)
144
+ const session = await openpay.checkout.create({
145
+ amount: 1000000,
146
+ email: 'customer@gmail.com',
147
+ success_url: 'https://yourapp.com/thanks',
148
+ cancel_url: 'https://yourapp.com/cancel',
149
+ });
150
+ window.open(session.checkout_url);
151
+
152
+ // Verify, refund
153
+ const c = await openpay.charges.retrieve(charge.reference);
154
+ await openpay.charges.refund(charge.reference, { amount: 100000 }); // partial
155
+
156
+ // Recurring: charge a saved authorization (from a prior payment)
157
+ await openpay.charges.tokenize({
158
+ amount: 2500000,
159
+ email: 'customer@gmail.com',
160
+ auth_token: 'AUTH_xxx',
161
+ reference: 'order-42-renewal',
162
+ currency: 'NGN',
163
+ });
164
+ ```
165
+
166
+ ## Subscriptions
167
+
168
+ ```ts
169
+ const plan = await openpay.subscriptions.createPlan({
170
+ merchant_id: 'merchant-uuid',
171
+ name: 'Pro monthly',
172
+ amount: 2500000,
173
+ currency: 'NGN',
174
+ interval_unit: 'month', // day | week | month | quarter | year
175
+ interval_count: 1,
176
+ trial_days: 14, // optional
177
+ // app_id: 'app-uuid',
178
+ });
179
+
180
+ const sub = await openpay.subscriptions.create({
181
+ plan_id: plan.id,
182
+ customer_id: 'customer-uuid',
183
+ payment_method_id: 'pm-uuid', // a reusable card from a prior charge
184
+ });
185
+
186
+ await openpay.subscriptions.cancel(sub.id); // ends at period end
187
+ await openpay.subscriptions.resume(sub.id);
188
+ ```
189
+
190
+ 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.
191
+
192
+ ## Wallets (Wallet-as-a-Service)
193
+
194
+ WaaS lets you embed a pre-paid balance in **your** product for **your** end-users. The platform does the heavy lifting — ledger, custody, top-up payments, usage debits — and mirrors every movement back to your platform via webhooks, so the wallet state shows up on your side like a Stripe-issued account.
195
+
196
+ Wallets are **scoped per workspace (app)**: one wallet per `(app_id, customer_id)`. Each workspace with wallets gets a platform-managed **financial account** whose balance is the live sum of that workspace's wallet balances.
197
+
198
+ ```ts
199
+ const appId = 'app-uuid';
200
+ const customerId = 'customer-uuid';
201
+
202
+ // Fetch (auto-creates on first read) — one wallet per (app, customer)
203
+ const wallet = await openpay.wallets.get(appId, customerId);
204
+
205
+ // Top up immediately via API credit (source_type=api_credit)
206
+ await openpay.wallets.topup(appId, customerId, { amount: 500000 });
207
+
208
+ // Or mint a checkout the customer pays — the webhook credits the wallet idempotently
209
+ const { checkout } = await openpay.wallets.createCheckout(appId, customerId, { amount: 500000 });
210
+ window.open(checkout.checkout_url);
211
+
212
+ // Usage gate: check, then debit. A 402 means “top up required”.
213
+ if ((await openpay.wallets.check(appId, customerId, { amount: 10000 })).allowed) {
214
+ await openpay.wallets.debit(appId, customerId, { amount: 10000, description: 'API usage' });
215
+ }
216
+
217
+ // Workspace financial account + full statement
218
+ const account = await openpay.financialAccounts.get(appId);
219
+ const statement = await openpay.financialAccounts.statement(appId, { limit: 50 });
220
+ ```
221
+
222
+ Subscribe your webhook to `wallet.created`, `wallet.credited`, `wallet.debited`, `wallet.topup_completed`, and `financial_account.created` to mirror the results into your platform. App-bound webhooks (`app_id` set) only receive their workspace's events.
223
+
224
+ ## Webhooks
225
+
226
+ ```ts
227
+ const hooks = await openpay.webhooks.list();
228
+ await openpay.webhooks.update(hooks.data[0].id, {
229
+ url: 'https://yourapp.com/api/webhooks',
230
+ events: ['charge.success', 'charge.failed', 'wallet.credited', 'wallet.debited'],
231
+ });
232
+ const deliveries = await openpay.webhooks.deliveries(hooks.data[0].id);
233
+ await openpay.webhooks.test(hooks.data[0].id); // sends a test event
234
+ ```
235
+
236
+ Inbound provider webhooks (Paystack/Korapay/Flutterwave/Hyperswitch → `POST /v1/webhooks/<provider>`) are verified with HMAC over the **raw request body** before any processing.
237
+
238
+ ## Metering & usage-based billing
239
+
240
+ ```ts
241
+ await openpay.metering.record('api_calls', 1, sub.id);
242
+ const report = await openpay.metering.report('api_calls', 'day');
243
+ ```
244
+
245
+ ## Analytics & finance
246
+
247
+ ```ts
248
+ await openpay.analytics.getOverview();
249
+ await openpay.analytics.getPnl({ period_start: '2024-01-01', period_end: '2024-01-31' });
250
+ await openpay.analytics.getTrialBalance(); // double-entry integrity check
251
+ await openpay.analytics.getCircuitStatus(); // provider health
252
+ ```
253
+
254
+ ## Error handling
255
+
256
+ All errors throw `OpenPayError`:
257
+
258
+ ```ts
259
+ import { OpenPayError } from '@fiatden/openpay';
260
+
261
+ try {
262
+ await openpay.charges.create({ /* … */ });
263
+ } catch (err) {
264
+ if (err instanceof OpenPayError) {
265
+ switch (err.status) {
266
+ case 400: // validation_error — err.code carries the field detail
267
+ break;
268
+ case 401: // authentication_error — check the API key
269
+ break;
270
+ case 403: // authorization_error — scope or tenant violation
271
+ break;
272
+ case 409: // conflict — duplicate reference, etc.
273
+ break;
274
+ case 429: // rate_limit_error — back off and retry
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ ```
280
+
281
+ | Status | Type | Meaning |
282
+ |---|---|---|
283
+ | 400 | `validation_error` | Invalid parameters; `err.code` has the zod detail |
284
+ | 401 | `authentication_error` | Missing/invalid API key or session |
285
+ | 403 | `authorization_error` | Scope missing, wrong tenant, or CSRF failure |
286
+ | 404 | `not_found` | Resource doesn't exist or belongs to another tenant |
287
+ | 409 | `conflict` | Duplicate (e.g. reference or code already used) |
288
+ | 429 | `rate_limit_error` | Too many requests |
289
+ | 500 | `api_error` | Server-side failure — safe to retry with the same idempotency key |
290
+
291
+ ## Idempotency & safety notes
292
+
293
+ - Payouts accept `idempotency_key` — retries return the original payout instead of double-paying.
294
+ - Charge references are unique per merchant; retrying with the same reference is safe.
295
+ - Secrets (`secret_key`, API keys) are returned exactly once at creation/rotation.
296
+ - Platform provider credentials (env keys) are **test-only**: production merchant traffic must use merchant-configured provider credentials.
297
+
298
+ ## Development
299
+
300
+ ```bash
301
+ npm install
302
+ npm run typecheck # tsc --noEmit
303
+ npm test # vitest
304
+ npm run build # emit dist/
305
+ npm publish # runs prepublishOnly: typecheck → test → build
306
+ ```
307
+
308
+ ## License
309
+
310
+ 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"}