mockpay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +207 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +270 -0
  4. package/dist/core/config.d.ts +13 -0
  5. package/dist/core/config.js +30 -0
  6. package/dist/core/db.d.ts +9 -0
  7. package/dist/core/db.js +104 -0
  8. package/dist/core/logger.d.ts +11 -0
  9. package/dist/core/logger.js +47 -0
  10. package/dist/core/runtime.d.ts +10 -0
  11. package/dist/core/runtime.js +34 -0
  12. package/dist/core/state.d.ts +18 -0
  13. package/dist/core/state.js +70 -0
  14. package/dist/core/utils.d.ts +2 -0
  15. package/dist/core/utils.js +8 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.js +3 -0
  18. package/dist/providers/flutterwave/index.d.ts +10 -0
  19. package/dist/providers/flutterwave/index.js +231 -0
  20. package/dist/providers/paystack/index.d.ts +10 -0
  21. package/dist/providers/paystack/index.js +226 -0
  22. package/dist/routes/logs.d.ts +2 -0
  23. package/dist/routes/logs.js +20 -0
  24. package/dist/server/index.d.ts +1 -0
  25. package/dist/server/index.js +59 -0
  26. package/dist/server/middleware/errorSimulation.d.ts +2 -0
  27. package/dist/server/middleware/errorSimulation.js +47 -0
  28. package/dist/server/middleware/logging.d.ts +2 -0
  29. package/dist/server/middleware/logging.js +7 -0
  30. package/dist/types/index.d.ts +59 -0
  31. package/dist/types/index.js +1 -0
  32. package/dist/webhooks/sender.d.ts +8 -0
  33. package/dist/webhooks/sender.js +94 -0
  34. package/package.json +31 -0
  35. package/src/cli/index.ts +291 -0
  36. package/src/core/config.ts +47 -0
  37. package/src/core/db.ts +123 -0
  38. package/src/core/logger.ts +57 -0
  39. package/src/core/runtime.ts +42 -0
  40. package/src/core/state.ts +91 -0
  41. package/src/core/utils.ts +10 -0
  42. package/src/index.ts +3 -0
  43. package/src/providers/flutterwave/index.ts +254 -0
  44. package/src/providers/paystack/index.ts +249 -0
  45. package/src/routes/logs.ts +28 -0
  46. package/src/server/index.ts +69 -0
  47. package/src/server/middleware/errorSimulation.ts +60 -0
  48. package/src/server/middleware/logging.ts +10 -0
  49. package/src/types/index.ts +64 -0
  50. package/src/webhooks/sender.ts +108 -0
  51. package/template/App.tsx +25 -0
  52. package/template/components/Button.tsx +45 -0
  53. package/template/components/Card.tsx +16 -0
  54. package/template/components/Input.tsx +27 -0
  55. package/template/components/PaymentMethodIcon.tsx +40 -0
  56. package/template/components/StatusScreen.tsx +117 -0
  57. package/template/hooks/useQueryParams.ts +22 -0
  58. package/template/index.html +29 -0
  59. package/template/index.tsx +16 -0
  60. package/template/package.json +25 -0
  61. package/template/pages/CancelledPage.tsx +20 -0
  62. package/template/pages/CheckoutPage.tsx +370 -0
  63. package/template/pages/FailedPage.tsx +20 -0
  64. package/template/pages/SuccessPage.tsx +20 -0
  65. package/template/pnpm-lock.yaml +1192 -0
  66. package/template/react-icons.d.ts +8 -0
  67. package/template/tsconfig.json +31 -0
  68. package/template/types.ts +25 -0
  69. package/template/vite.config.ts +23 -0
  70. package/tsconfig.json +16 -0
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # mockpay
2
+
3
+ Local mock Paystack and Flutterwave servers for offline/local testing. Use this in development to replace live gateway URLs and to simulate payment outcomes, errors, and webhooks.
4
+
5
+ Default base URLs:
6
+ - Paystack-like: `http://localhost:4010`
7
+ - Flutterwave-like: `http://localhost:4020`
8
+
9
+ Hosted checkout (served by mockpay):
10
+ - http://localhost:4010/checkout
11
+ - http://localhost:4020/checkout
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ npm i -g mockpay
17
+ mockpay start
18
+ ```
19
+
20
+ Servers:
21
+ - Paystack: http://localhost:4010
22
+ - Flutterwave: http://localhost:4020
23
+
24
+ ## Integration Goal
25
+
26
+ Replace live gateway URLs in development:
27
+ - Instead of `https://api.paystack.co`, use `http://localhost:4010`
28
+ - Instead of `https://api.flutterwave.com/v3`, use `http://localhost:4020`
29
+
30
+ Typical flow:
31
+ 1. Initialize payment from your backend.
32
+ 2. Open the hosted checkout link in the browser.
33
+ 3. Complete payment in the mock checkout UI.
34
+ 4. Verify from your backend.
35
+
36
+ ## CLI Commands
37
+
38
+ ```bash
39
+ mockpay start
40
+ mockpay stop
41
+ mockpay status
42
+ mockpay pay success|fail|cancel
43
+ mockpay error 500|timeout|network
44
+ mockpay webhook resend
45
+ mockpay webhook config --delay 1000 --retry 2 --retry-delay 2000 --duplicate --drop
46
+ mockpay reset
47
+ mockpay logs
48
+ ```
49
+
50
+ Notes:
51
+ - `mockpay pay ...` applies to the next payment only, then resets to `success`.
52
+ - `mockpay error ...` applies to the next API request only, then resets to `none`.
53
+ - `mockpay logs` streams live logs over SSE from the Paystack server.
54
+
55
+ ## Environment
56
+
57
+ Copy `.env.example` to `.env` and adjust if needed.
58
+
59
+ Key settings:
60
+ - `MOCKPAY_PAYSTACK_PORT`
61
+ - `MOCKPAY_FLUTTERWAVE_PORT`
62
+ - `MOCKPAY_FRONTEND_URL`
63
+ - `MOCKPAY_DATA_DIR`
64
+ - `MOCKPAY_DEFAULT_WEBHOOK_URL`
65
+
66
+ Webhook controls:
67
+ - `MOCKPAY_WEBHOOK_DELAY_MS`
68
+ - `MOCKPAY_WEBHOOK_RETRY_COUNT`
69
+ - `MOCKPAY_WEBHOOK_RETRY_DELAY_MS`
70
+ - `MOCKPAY_WEBHOOK_DUPLICATE`
71
+ - `MOCKPAY_WEBHOOK_DROP`
72
+
73
+ ## API Coverage
74
+
75
+ ### Paystack
76
+ - `POST /transaction/initialize`
77
+ - `GET /transaction/verify/:reference`
78
+ - `POST /transaction/verify/:reference`
79
+ - `POST /transfer`
80
+ - `GET /banks`
81
+
82
+ ### Flutterwave
83
+ - `POST /payments`
84
+ - `GET /transactions/verify_by_reference?tx_ref=...`
85
+ - `GET /transactions/:id/verify`
86
+ - `POST /transfers`
87
+
88
+ ## Payment Flow
89
+
90
+ ### Paystack-style flow
91
+ 1. `POST /transaction/initialize`
92
+ 2. Open `data.authorization_url`
93
+ 3. Complete payment on checkout (`success` / `failed` / `cancelled`)
94
+ 4. Verify on your backend with `/transaction/verify/:reference`
95
+
96
+ ### Flutterwave-style flow
97
+ 1. `POST /payments`
98
+ 2. Open `data.link`
99
+ 3. Complete payment on checkout (`success` / `failed` / `cancelled`)
100
+ 4. Verify on your backend using:
101
+ - `/transactions/verify_by_reference?tx_ref=...` or
102
+ - `/transactions/:id/verify`
103
+
104
+ ## Example Requests
105
+
106
+ ### Paystack initialize
107
+
108
+ ```bash
109
+ curl -X POST http://localhost:4010/transaction/initialize \
110
+ -H "Content-Type: application/json" \
111
+ -d '{
112
+ "amount": 5000,
113
+ "currency": "NGN",
114
+ "email": "test@example.com",
115
+ "name": "Ada Lovelace",
116
+ "callback_url": "http://localhost:3000/paystack/callback"
117
+ }'
118
+ ```
119
+
120
+ ### Paystack verify
121
+
122
+ ```bash
123
+ curl http://localhost:4010/transaction/verify/PSK_1234567890_abcdef
124
+ ```
125
+
126
+ ### Flutterwave payments
127
+
128
+ ```bash
129
+ curl -X POST http://localhost:4020/payments \
130
+ -H "Content-Type: application/json" \
131
+ -d '{
132
+ "amount": 5000,
133
+ "currency": "NGN",
134
+ "customer": {
135
+ "email": "test@example.com",
136
+ "name": "Ada Lovelace"
137
+ },
138
+ "redirect_url": "http://localhost:3000/flutterwave/callback"
139
+ }'
140
+ ```
141
+
142
+ ### Flutterwave verify by reference
143
+
144
+ ```bash
145
+ curl "http://localhost:4020/transactions/verify_by_reference?tx_ref=FLW_1234567890_abcdef"
146
+ ```
147
+
148
+ ### Flutterwave verify by id
149
+
150
+ ```bash
151
+ curl "http://localhost:4020/transactions/<transaction_id>/verify"
152
+ ```
153
+
154
+ ## Error Simulation
155
+
156
+ Simulate one request failure:
157
+
158
+ ```bash
159
+ mockpay error 500
160
+ mockpay error timeout
161
+ mockpay error network
162
+ ```
163
+
164
+ Notes:
165
+ - `network` will drop the socket without a response.
166
+ - `timeout` waits 15 seconds before responding with `504`.
167
+
168
+ ## Webhooks
169
+
170
+ Set a default webhook URL:
171
+
172
+ ```
173
+ MOCKPAY_DEFAULT_WEBHOOK_URL=http://localhost:3000/webhooks/mockpay
174
+ ```
175
+
176
+ Configure behavior at runtime:
177
+
178
+ ```bash
179
+ mockpay webhook config --delay 1000 --retry 2 --retry-delay 2000 --duplicate --drop
180
+ ```
181
+
182
+ Resend last webhook:
183
+
184
+ ```bash
185
+ mockpay webhook resend
186
+ ```
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ npm install
192
+ npm run dev
193
+ ```
194
+
195
+ ## Building the Hosted Checkout
196
+
197
+ The checkout UI is in `template/` and should be built before publishing:
198
+
199
+ ```bash
200
+ npm --prefix template install
201
+ npm --prefix template run build
202
+ ```
203
+
204
+ ## Notes
205
+
206
+ - ChronoDB is used for file-based persistence. Data is stored under `MOCKPAY_DATA_DIR` (default `.mockpay/data`).
207
+ - Hosted checkout URLs include transaction details (`ref`, `amount`, `currency`, `email`, `name`, provider).
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { spawn } from "child_process";
7
+ import fs from "fs";
8
+ import { setNextPaymentResult, setNextError, getWebhookConfig, setWebhookConfig } from "../core/state.js";
9
+ import { readRuntime, writeRuntime, clearRuntime, isPidRunning } from "../core/runtime.js";
10
+ import { resendLastWebhook } from "../webhooks/sender.js";
11
+ import { getCollections, getDb } from "../core/db.js";
12
+ import { getConfig } from "../core/config.js";
13
+ const program = new Command();
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ program
17
+ .name("mockpay")
18
+ .description("Local Paystack + Flutterwave mock servers")
19
+ .version("0.1.0");
20
+ program
21
+ .command("start")
22
+ .description("Start mock servers")
23
+ .action(async () => {
24
+ const runtime = await readRuntime();
25
+ if (runtime && isPidRunning(runtime.pid)) {
26
+ console.log(chalk.yellow(`Mockpay already running (pid ${runtime.pid})`));
27
+ return;
28
+ }
29
+ const config = getConfig();
30
+ const isHealthy = async () => {
31
+ const targets = [
32
+ `http://localhost:${config.paystackPort}/__health`,
33
+ `http://localhost:${config.flutterwavePort}/__health`
34
+ ];
35
+ for (const url of targets) {
36
+ try {
37
+ const controller = new AbortController();
38
+ const timeout = setTimeout(() => controller.abort(), 1000);
39
+ const res = await fetch(url, { signal: controller.signal });
40
+ clearTimeout(timeout);
41
+ if (res.ok)
42
+ return true;
43
+ }
44
+ catch {
45
+ // ignore
46
+ }
47
+ }
48
+ return false;
49
+ };
50
+ if (await isHealthy()) {
51
+ console.log(chalk.yellow("Mockpay already running"));
52
+ return;
53
+ }
54
+ const jsServerPath = path.resolve(__dirname, "..", "server", "index.js");
55
+ const tsServerPath = path.resolve(__dirname, "..", "server", "index.ts");
56
+ const serverPath = fs.existsSync(jsServerPath) ? jsServerPath : tsServerPath;
57
+ const child = spawn(process.execPath, [...process.execArgv, serverPath], {
58
+ detached: true,
59
+ stdio: "ignore",
60
+ env: {
61
+ ...process.env,
62
+ MOCKPAY_DATA_DIR: config.dataDir
63
+ }
64
+ });
65
+ child.unref();
66
+ if (!child.pid) {
67
+ console.log(chalk.red("Failed to start mock servers"));
68
+ return;
69
+ }
70
+ await writeRuntime(child.pid, config.dataDir);
71
+ console.log(chalk.green(`Mockpay started (pid ${child.pid})`));
72
+ console.log(chalk.gray(`Paystack: http://localhost:${config.paystackPort}`));
73
+ console.log(chalk.gray(`Flutterwave: http://localhost:${config.flutterwavePort}`));
74
+ });
75
+ program
76
+ .command("stop")
77
+ .description("Stop mock servers")
78
+ .action(async () => {
79
+ const runtime = await readRuntime();
80
+ if (!runtime || !isPidRunning(runtime.pid)) {
81
+ console.log(chalk.yellow("Mockpay is not running"));
82
+ await clearRuntime();
83
+ return;
84
+ }
85
+ try {
86
+ process.kill(runtime.pid);
87
+ await clearRuntime();
88
+ console.log(chalk.green("Mockpay stopped"));
89
+ }
90
+ catch {
91
+ console.log(chalk.red(`Failed to stop process ${runtime.pid}`));
92
+ }
93
+ });
94
+ program
95
+ .command("status")
96
+ .description("Show running services")
97
+ .action(async () => {
98
+ const config = getConfig();
99
+ const targets = [
100
+ { name: "Paystack", url: `http://localhost:${config.paystackPort}/__health` },
101
+ { name: "Flutterwave", url: `http://localhost:${config.flutterwavePort}/__health` }
102
+ ];
103
+ const results = await Promise.all(targets.map(async (target) => {
104
+ try {
105
+ const controller = new AbortController();
106
+ const timeout = setTimeout(() => controller.abort(), 1500);
107
+ const res = await fetch(target.url, { signal: controller.signal });
108
+ clearTimeout(timeout);
109
+ return { name: target.name, ok: res.ok };
110
+ }
111
+ catch {
112
+ return { name: target.name, ok: false };
113
+ }
114
+ }));
115
+ const running = results.filter((r) => r.ok);
116
+ if (running.length === 0) {
117
+ console.log(chalk.yellow("Not running"));
118
+ return;
119
+ }
120
+ running.forEach((r) => console.log(chalk.green(`${r.name} running`)));
121
+ });
122
+ program
123
+ .command("pay")
124
+ .description("Set next payment result")
125
+ .argument("<result>", "success|fail|cancel")
126
+ .action(async (result) => {
127
+ const runtime = await readRuntime();
128
+ if (runtime?.dataDir)
129
+ process.env.MOCKPAY_DATA_DIR = runtime.dataDir;
130
+ const map = {
131
+ success: "success",
132
+ fail: "failed",
133
+ cancel: "cancelled"
134
+ };
135
+ const mapped = map[result];
136
+ if (!mapped) {
137
+ console.log(chalk.red("Expected success|fail|cancel"));
138
+ return;
139
+ }
140
+ await setNextPaymentResult(mapped);
141
+ console.log(chalk.green(`Next payment result set to ${mapped}`));
142
+ });
143
+ program
144
+ .command("error")
145
+ .description("Simulate next request failure")
146
+ .argument("<type>", "500|timeout|network")
147
+ .action(async (type) => {
148
+ const runtime = await readRuntime();
149
+ if (runtime?.dataDir)
150
+ process.env.MOCKPAY_DATA_DIR = runtime.dataDir;
151
+ const allowed = ["500", "timeout", "network"];
152
+ if (!allowed.includes(type)) {
153
+ console.log(chalk.red("Expected 500|timeout|network"));
154
+ return;
155
+ }
156
+ await setNextError(type);
157
+ console.log(chalk.green(`Next error set to ${type}`));
158
+ });
159
+ const webhook = program.command("webhook").description("Webhook actions");
160
+ webhook
161
+ .command("resend")
162
+ .description("Resend last webhook")
163
+ .action(async () => {
164
+ const runtime = await readRuntime();
165
+ if (runtime?.dataDir)
166
+ process.env.MOCKPAY_DATA_DIR = runtime.dataDir;
167
+ const ok = await resendLastWebhook();
168
+ if (ok) {
169
+ console.log(chalk.green("Webhook resent"));
170
+ }
171
+ else {
172
+ console.log(chalk.yellow("No webhook to resend"));
173
+ }
174
+ });
175
+ webhook
176
+ .command("config")
177
+ .description("View or update webhook behavior")
178
+ .option("--delay <ms>", "Delay before sending")
179
+ .option("--retry <count>", "Retry count")
180
+ .option("--retry-delay <ms>", "Retry delay")
181
+ .option("--duplicate", "Send duplicate webhook")
182
+ .option("--no-duplicate", "Disable duplicate webhook")
183
+ .option("--drop", "Drop webhook")
184
+ .option("--no-drop", "Disable drop")
185
+ .action(async (options) => {
186
+ const runtime = await readRuntime();
187
+ if (runtime?.dataDir)
188
+ process.env.MOCKPAY_DATA_DIR = runtime.dataDir;
189
+ const current = await getWebhookConfig();
190
+ const updated = {
191
+ delayMs: options.delay ? Number(options.delay) : current.delayMs,
192
+ retryCount: options.retry ? Number(options.retry) : current.retryCount,
193
+ retryDelayMs: options.retryDelay ? Number(options.retryDelay) : current.retryDelayMs,
194
+ duplicate: typeof options.duplicate === "boolean" ? options.duplicate : current.duplicate,
195
+ drop: typeof options.drop === "boolean" ? options.drop : current.drop
196
+ };
197
+ await setWebhookConfig(updated);
198
+ console.log(chalk.green("Webhook config updated"));
199
+ console.log(updated);
200
+ });
201
+ program
202
+ .command("reset")
203
+ .description("Clear ChronoDB data")
204
+ .action(async () => {
205
+ const runtime = await readRuntime();
206
+ if (runtime?.dataDir)
207
+ process.env.MOCKPAY_DATA_DIR = runtime.dataDir;
208
+ const { transactions, transfers, webhooks, settings, logs } = await getCollections();
209
+ await transactions.deleteAll();
210
+ await transfers.deleteAll();
211
+ await webhooks.deleteAll();
212
+ await settings.deleteAll();
213
+ await logs.deleteAll();
214
+ const db = await getDb();
215
+ if (db?.snapshots?.deleteAll) {
216
+ await db.snapshots.deleteAll();
217
+ }
218
+ console.log(chalk.green("Database cleared"));
219
+ });
220
+ program
221
+ .command("logs")
222
+ .description("Stream live logs")
223
+ .action(async () => {
224
+ const runtime = await readRuntime();
225
+ if (runtime?.dataDir)
226
+ process.env.MOCKPAY_DATA_DIR = runtime.dataDir;
227
+ const config = getConfig();
228
+ const url = `http://localhost:${config.paystackPort}/__logs`;
229
+ console.log(chalk.gray(`Streaming logs from ${url}`));
230
+ try {
231
+ const res = await fetch(url, {
232
+ headers: {
233
+ Accept: "text/event-stream"
234
+ }
235
+ });
236
+ if (!res.body) {
237
+ console.log(chalk.red("No log stream available"));
238
+ return;
239
+ }
240
+ const reader = res.body.getReader();
241
+ let buffer = "";
242
+ while (true) {
243
+ const { done, value } = await reader.read();
244
+ if (done)
245
+ break;
246
+ buffer += new TextDecoder().decode(value);
247
+ const parts = buffer.split("\n\n");
248
+ buffer = parts.pop() ?? "";
249
+ for (const part of parts) {
250
+ const line = part.trim();
251
+ if (!line.startsWith("data:"))
252
+ continue;
253
+ const payload = line.replace(/^data:\s*/, "");
254
+ try {
255
+ const entry = JSON.parse(payload);
256
+ const time = new Date(entry.timestamp).toLocaleTimeString();
257
+ const prefix = entry.source ? `[${entry.source}] ` : "";
258
+ console.log(`${chalk.gray(time)} ${prefix}${entry.message}`);
259
+ }
260
+ catch {
261
+ // ignore parse issues
262
+ }
263
+ }
264
+ }
265
+ }
266
+ catch {
267
+ console.log(chalk.red("Unable to connect to log stream. Is mockpay running?"));
268
+ }
269
+ });
270
+ program.parseAsync(process.argv);
@@ -0,0 +1,13 @@
1
+ export interface Config {
2
+ paystackPort: number;
3
+ flutterwavePort: number;
4
+ dataDir: string;
5
+ frontendUrl?: string;
6
+ defaultWebhookUrl?: string;
7
+ webhookDelayMs: number;
8
+ webhookRetryCount: number;
9
+ webhookRetryDelayMs: number;
10
+ webhookDuplicate: boolean;
11
+ webhookDrop: boolean;
12
+ }
13
+ export declare function getConfig(): Config;
@@ -0,0 +1,30 @@
1
+ import dotenv from "dotenv";
2
+ import path from "path";
3
+ dotenv.config();
4
+ function toBool(value, fallback) {
5
+ if (value === undefined)
6
+ return fallback;
7
+ return value.toLowerCase() === "true";
8
+ }
9
+ function toNum(value, fallback) {
10
+ const parsed = Number(value);
11
+ return Number.isFinite(parsed) ? parsed : fallback;
12
+ }
13
+ export function getConfig() {
14
+ const baseDir = process.cwd();
15
+ const dataDir = process.env.MOCKPAY_DATA_DIR
16
+ ? path.resolve(baseDir, process.env.MOCKPAY_DATA_DIR)
17
+ : path.resolve(baseDir, ".mockpay", "data");
18
+ return {
19
+ paystackPort: toNum(process.env.MOCKPAY_PAYSTACK_PORT, 4010),
20
+ flutterwavePort: toNum(process.env.MOCKPAY_FLUTTERWAVE_PORT, 4020),
21
+ dataDir,
22
+ frontendUrl: process.env.MOCKPAY_FRONTEND_URL || undefined,
23
+ defaultWebhookUrl: process.env.MOCKPAY_DEFAULT_WEBHOOK_URL || undefined,
24
+ webhookDelayMs: toNum(process.env.MOCKPAY_WEBHOOK_DELAY_MS, 1500),
25
+ webhookRetryCount: toNum(process.env.MOCKPAY_WEBHOOK_RETRY_COUNT, 0),
26
+ webhookRetryDelayMs: toNum(process.env.MOCKPAY_WEBHOOK_RETRY_DELAY_MS, 2000),
27
+ webhookDuplicate: toBool(process.env.MOCKPAY_WEBHOOK_DUPLICATE, false),
28
+ webhookDrop: toBool(process.env.MOCKPAY_WEBHOOK_DROP, false)
29
+ };
30
+ }
@@ -0,0 +1,9 @@
1
+ export interface Collections {
2
+ transactions: any;
3
+ transfers: any;
4
+ webhooks: any;
5
+ settings: any;
6
+ logs: any;
7
+ }
8
+ export declare function getDb(): Promise<any>;
9
+ export declare function getCollections(): Promise<Collections>;
@@ -0,0 +1,104 @@
1
+ import ChronoDB from "chronodb";
2
+ import fs from "fs/promises";
3
+ import { getConfig } from "./config.js";
4
+ let dbPromise = null;
5
+ let collectionsPromise = null;
6
+ export async function getDb() {
7
+ if (!dbPromise) {
8
+ dbPromise = openDb();
9
+ }
10
+ return dbPromise;
11
+ }
12
+ export async function getCollections() {
13
+ if (!collectionsPromise) {
14
+ collectionsPromise = initCollections();
15
+ }
16
+ return collectionsPromise;
17
+ }
18
+ async function openDb() {
19
+ const { dataDir } = getConfig();
20
+ await fs.mkdir(dataDir, { recursive: true });
21
+ try {
22
+ return await ChronoDB.open({ path: dataDir, cloudSync: false });
23
+ }
24
+ catch {
25
+ const previous = process.cwd();
26
+ process.chdir(dataDir);
27
+ try {
28
+ return await ChronoDB.open({ cloudSync: false });
29
+ }
30
+ finally {
31
+ process.chdir(previous);
32
+ }
33
+ }
34
+ }
35
+ async function initCollections() {
36
+ const db = await getDb();
37
+ const transactions = db.col("transactions", {
38
+ schema: {
39
+ createdAt: { type: "string" },
40
+ updatedAt: { type: "string" },
41
+ provider: { type: "string", important: true },
42
+ reference: { type: "string", important: true, distinct: true },
43
+ status: { type: "string", important: true },
44
+ amount: { type: "number", important: true },
45
+ currency: { type: "string", default: "NGN" },
46
+ customerEmail: { type: "string", important: true },
47
+ customerName: { type: "string", nullable: true },
48
+ callbackUrl: { type: "string", nullable: true },
49
+ metadata: { type: "string", nullable: true }
50
+ },
51
+ indexes: ["provider", "reference", "status"]
52
+ });
53
+ const transfers = db.col("transfers", {
54
+ schema: {
55
+ createdAt: { type: "string" },
56
+ updatedAt: { type: "string" },
57
+ provider: { type: "string", important: true },
58
+ reference: { type: "string", important: true, distinct: true },
59
+ status: { type: "string", important: true },
60
+ amount: { type: "number", important: true },
61
+ currency: { type: "string", default: "NGN" },
62
+ bankCode: { type: "string", nullable: true },
63
+ accountNumber: { type: "string", nullable: true },
64
+ narration: { type: "string", nullable: true },
65
+ metadata: { type: "string", nullable: true }
66
+ },
67
+ indexes: ["provider", "reference", "status"]
68
+ });
69
+ const webhooks = db.col("webhooks", {
70
+ schema: {
71
+ createdAt: { type: "string" },
72
+ updatedAt: { type: "string" },
73
+ provider: { type: "string", important: true },
74
+ event: { type: "string", important: true },
75
+ url: { type: "string", important: true },
76
+ status: { type: "string", important: true },
77
+ attempts: { type: "number", default: 0 },
78
+ payload: { type: "string", important: true },
79
+ lastAttemptAt: { type: "number", nullable: true }
80
+ },
81
+ indexes: ["provider", "event", "status"]
82
+ });
83
+ const settings = db.col("settings", {
84
+ schema: {
85
+ createdAt: { type: "string" },
86
+ updatedAt: { type: "string" },
87
+ key: { type: "string", important: true, distinct: true },
88
+ value: { type: "string", important: true }
89
+ },
90
+ indexes: ["key"]
91
+ });
92
+ const logs = db.col("logs", {
93
+ schema: {
94
+ createdAt: { type: "string" },
95
+ updatedAt: { type: "string" },
96
+ level: { type: "string", important: true },
97
+ message: { type: "string", important: true },
98
+ source: { type: "string", nullable: true },
99
+ timestamp: { type: "number", important: true }
100
+ },
101
+ indexes: ["level", "timestamp"]
102
+ });
103
+ return { transactions, transfers, webhooks, settings, logs };
104
+ }
@@ -0,0 +1,11 @@
1
+ import { EventEmitter } from "events";
2
+ import type { LogEntry } from "../types/index.js";
3
+ export type LogLevel = "info" | "warn" | "error" | "http";
4
+ export declare const logger: {
5
+ info: (message: string, source?: string) => void;
6
+ warn: (message: string, source?: string) => void;
7
+ error: (message: string, source?: string) => void;
8
+ http: (message: string, source?: string) => void;
9
+ on: (handler: (entry: LogEntry) => void) => EventEmitter<[never]>;
10
+ off: (handler: (entry: LogEntry) => void) => EventEmitter<[never]>;
11
+ };
@@ -0,0 +1,47 @@
1
+ import chalk from "chalk";
2
+ import { EventEmitter } from "events";
3
+ import { getCollections } from "./db.js";
4
+ const emitter = new EventEmitter();
5
+ function colorize(level, message) {
6
+ switch (level) {
7
+ case "info":
8
+ return chalk.cyan(message);
9
+ case "warn":
10
+ return chalk.yellow(message);
11
+ case "error":
12
+ return chalk.red(message);
13
+ case "http":
14
+ return chalk.green(message);
15
+ default:
16
+ return message;
17
+ }
18
+ }
19
+ async function persist(entry) {
20
+ try {
21
+ const { logs } = await getCollections();
22
+ await logs.add(entry);
23
+ }
24
+ catch {
25
+ // Best-effort logging only.
26
+ }
27
+ }
28
+ function log(level, message, source) {
29
+ const entry = {
30
+ level,
31
+ message,
32
+ source,
33
+ timestamp: Date.now()
34
+ };
35
+ const prefix = source ? `[${source}] ` : "";
36
+ console.log(colorize(level, `${prefix}${message}`));
37
+ emitter.emit("log", entry);
38
+ void persist(entry);
39
+ }
40
+ export const logger = {
41
+ info: (message, source) => log("info", message, source),
42
+ warn: (message, source) => log("warn", message, source),
43
+ error: (message, source) => log("error", message, source),
44
+ http: (message, source) => log("http", message, source),
45
+ on: (handler) => emitter.on("log", handler),
46
+ off: (handler) => emitter.off("log", handler)
47
+ };