@miniduckco/stash 0.1.1 → 0.1.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.
@@ -1,5 +1,5 @@
1
1
  import type { ParsedWebhook, Payment, PaymentCreateInput, PaymentVerifyInput, PaymentRequest, PaymentResponse, StashConfig, VerificationResult, WebhookParseInput, WebhookVerifyInput, WebhookVerifyResult } from "./types.js";
2
- export type { OzowProviderOptions, ParsedWebhook, Payment, PaymentCreateInput, PaymentProvider, PaymentVerifyInput, PaymentRequest, PaymentResponse, PaystackProviderOptions, PayfastProviderOptions, ProviderOptions, StashConfig, VerificationResult, WebhookEvent, WebhookParseInput, WebhookVerifyInput, WebhookVerifyResult, } from "./types.js";
2
+ export type { OzowProviderOptions, ParsedWebhook, LogEvent, Logger, Payment, PaymentCreateInput, PaymentProvider, PaymentVerifyInput, PaymentRequest, PaymentResponse, PaystackProviderOptions, PayfastProviderOptions, ProviderOptions, StashConfig, VerificationResult, WebhookEvent, WebhookParseInput, WebhookVerifyInput, WebhookVerifyResult, } from "./types.js";
3
3
  export { StashError } from "./errors.js";
4
4
  export { buildFormEncoded, parseFormBody, parseFormEncoded, pairsToRecord } from "./internal/form.js";
5
5
  export declare function createStash(config: StashConfig): {
package/dist/src/index.js CHANGED
@@ -10,11 +10,36 @@ export { buildFormEncoded, parseFormBody, parseFormEncoded, pairsToRecord } from
10
10
  export function createStash(config) {
11
11
  const provider = config.provider;
12
12
  const testMode = config.testMode ?? false;
13
+ const logger = config.logger;
14
+ const emit = (event) => {
15
+ if (!logger)
16
+ return;
17
+ logger.log({
18
+ timestamp: new Date().toISOString(),
19
+ ...event,
20
+ });
21
+ };
13
22
  return {
14
23
  payments: {
15
24
  create: async (input) => {
25
+ const correlationId = randomUUID();
26
+ const startedAt = Date.now();
16
27
  const currency = input.currency ?? config.defaults?.currency ?? "ZAR";
17
28
  const amountNumber = Number(formatAmount(input.amount));
29
+ emit({
30
+ event: "payments.create.request",
31
+ provider,
32
+ action: "create",
33
+ stage: "request",
34
+ correlation_id: correlationId,
35
+ status: "success",
36
+ metadata: {
37
+ amount: input.amount,
38
+ currency,
39
+ reference: input.reference,
40
+ testMode,
41
+ },
42
+ });
18
43
  const paymentRequest = {
19
44
  provider,
20
45
  amount: input.amount,
@@ -30,38 +55,183 @@ export function createStash(config) {
30
55
  secrets: buildSecrets(provider, config.credentials),
31
56
  };
32
57
  const adapter = providerAdapters[provider];
33
- const response = await adapter.createPayment(paymentRequest);
34
- return {
35
- id: randomUUID(),
36
- status: "pending",
37
- amount: amountNumber,
38
- currency,
39
- redirectUrl: response.redirectUrl,
40
- provider,
41
- providerRef: response.paymentRequestId,
42
- raw: response.raw ?? response,
43
- };
58
+ try {
59
+ const response = await adapter.createPayment(paymentRequest);
60
+ const payment = {
61
+ id: randomUUID(),
62
+ status: "pending",
63
+ amount: amountNumber,
64
+ currency,
65
+ redirectUrl: response.redirectUrl,
66
+ provider,
67
+ providerRef: response.paymentRequestId,
68
+ correlationId,
69
+ raw: response.raw ?? response,
70
+ };
71
+ emit({
72
+ event: "payments.create.response",
73
+ provider,
74
+ action: "create",
75
+ stage: "response",
76
+ correlation_id: correlationId,
77
+ status: "success",
78
+ duration_ms: Date.now() - startedAt,
79
+ metadata: {
80
+ amount: amountNumber,
81
+ currency,
82
+ reference: input.reference,
83
+ provider_ref: response.paymentRequestId,
84
+ testMode,
85
+ },
86
+ });
87
+ return payment;
88
+ }
89
+ catch (error) {
90
+ emit({
91
+ event: "payments.create.error",
92
+ provider,
93
+ action: "create",
94
+ stage: "error",
95
+ correlation_id: correlationId,
96
+ status: "failure",
97
+ duration_ms: Date.now() - startedAt,
98
+ metadata: {
99
+ amount: input.amount,
100
+ currency,
101
+ reference: input.reference,
102
+ testMode,
103
+ },
104
+ error: {
105
+ code: error instanceof StashError ? error.code : "unknown",
106
+ message: error instanceof Error ? error.message : "Unknown error",
107
+ },
108
+ });
109
+ throw error;
110
+ }
44
111
  },
45
112
  verify: async (input) => {
113
+ const correlationId = randomUUID();
114
+ const startedAt = Date.now();
115
+ emit({
116
+ event: "payments.verify.request",
117
+ provider,
118
+ action: "verify",
119
+ stage: "request",
120
+ correlation_id: correlationId,
121
+ status: "success",
122
+ metadata: {
123
+ reference: input.reference,
124
+ testMode,
125
+ },
126
+ });
46
127
  const adapter = providerAdapters[provider];
47
128
  if (!adapter?.verifyPayment) {
48
- throw new StashError("unsupported_capability", `payments.verify is not supported for ${provider}`);
129
+ const error = new StashError("unsupported_capability", `payments.verify is not supported for ${provider}`);
130
+ emit({
131
+ event: "payments.verify.error",
132
+ provider,
133
+ action: "verify",
134
+ stage: "error",
135
+ correlation_id: correlationId,
136
+ status: "failure",
137
+ duration_ms: Date.now() - startedAt,
138
+ metadata: {
139
+ reference: input.reference,
140
+ testMode,
141
+ },
142
+ error: {
143
+ code: error.code,
144
+ message: error.message,
145
+ },
146
+ });
147
+ throw error;
148
+ }
149
+ try {
150
+ const result = await adapter.verifyPayment({
151
+ reference: input.reference,
152
+ secrets: buildSecrets(provider, config.credentials),
153
+ testMode,
154
+ });
155
+ const enriched = {
156
+ ...result,
157
+ correlationId,
158
+ };
159
+ emit({
160
+ event: "payments.verify.response",
161
+ provider,
162
+ action: "verify",
163
+ stage: "response",
164
+ correlation_id: correlationId,
165
+ status: "success",
166
+ duration_ms: Date.now() - startedAt,
167
+ metadata: {
168
+ reference: input.reference,
169
+ provider_ref: result.providerRef,
170
+ testMode,
171
+ },
172
+ });
173
+ return enriched;
174
+ }
175
+ catch (error) {
176
+ emit({
177
+ event: "payments.verify.error",
178
+ provider,
179
+ action: "verify",
180
+ stage: "error",
181
+ correlation_id: correlationId,
182
+ status: "failure",
183
+ duration_ms: Date.now() - startedAt,
184
+ metadata: {
185
+ reference: input.reference,
186
+ testMode,
187
+ },
188
+ error: {
189
+ code: error instanceof StashError ? error.code : "unknown",
190
+ message: error instanceof Error ? error.message : "Unknown error",
191
+ },
192
+ });
193
+ throw error;
49
194
  }
50
- return adapter.verifyPayment({
51
- reference: input.reference,
52
- secrets: buildSecrets(provider, config.credentials),
53
- testMode,
54
- });
55
195
  },
56
196
  },
57
197
  webhooks: {
58
198
  parse: (input) => {
199
+ const correlationId = randomUUID();
200
+ const startedAt = Date.now();
59
201
  const resolvedProvider = input.provider ?? provider;
60
202
  const secrets = buildSecrets(resolvedProvider, config.credentials);
61
203
  const rawBody = input.rawBody;
204
+ emit({
205
+ event: "webhooks.parse.request",
206
+ provider: resolvedProvider,
207
+ action: "parse",
208
+ stage: "request",
209
+ correlation_id: correlationId,
210
+ status: "success",
211
+ metadata: {
212
+ testMode,
213
+ },
214
+ });
62
215
  const adapter = providerAdapters[resolvedProvider];
63
216
  if (!adapter) {
64
- throw new Error(`Unsupported provider: ${resolvedProvider}`);
217
+ const error = new Error(`Unsupported provider: ${resolvedProvider}`);
218
+ emit({
219
+ event: "webhooks.parse.error",
220
+ provider: resolvedProvider,
221
+ action: "parse",
222
+ stage: "error",
223
+ correlation_id: correlationId,
224
+ status: "failure",
225
+ duration_ms: Date.now() - startedAt,
226
+ metadata: {
227
+ testMode,
228
+ },
229
+ error: {
230
+ code: "unsupported_provider",
231
+ message: error.message,
232
+ },
233
+ });
234
+ throw error;
65
235
  }
66
236
  const parsed = adapter.parseWebhook({
67
237
  rawBody,
@@ -69,13 +239,48 @@ export function createStash(config) {
69
239
  secrets,
70
240
  });
71
241
  if (!parsed.isValid) {
72
- throw new StashError("invalid_signature", `Invalid ${resolvedProvider} signature`);
242
+ const error = new StashError("invalid_signature", `Invalid ${resolvedProvider} signature`);
243
+ emit({
244
+ event: "webhooks.parse.error",
245
+ provider: resolvedProvider,
246
+ action: "parse",
247
+ stage: "error",
248
+ correlation_id: correlationId,
249
+ status: "failure",
250
+ duration_ms: Date.now() - startedAt,
251
+ metadata: {
252
+ testMode,
253
+ },
254
+ error: {
255
+ code: error.code,
256
+ message: error.message,
257
+ },
258
+ });
259
+ throw error;
73
260
  }
74
- return {
261
+ const response = {
75
262
  event: parsed.event,
76
263
  provider: resolvedProvider,
264
+ correlationId,
77
265
  raw: parsed.raw,
78
266
  };
267
+ emit({
268
+ event: "webhooks.parse.response",
269
+ provider: resolvedProvider,
270
+ action: "parse",
271
+ stage: "response",
272
+ correlation_id: correlationId,
273
+ status: "success",
274
+ duration_ms: Date.now() - startedAt,
275
+ metadata: {
276
+ amount: parsed.event.data.amount,
277
+ currency: parsed.event.data.currency,
278
+ reference: parsed.event.data.reference,
279
+ provider_ref: parsed.event.data.providerRef,
280
+ testMode,
281
+ },
282
+ });
283
+ return response;
79
284
  },
80
285
  },
81
286
  };
@@ -38,6 +38,31 @@ export type StashConfig = {
38
38
  defaults?: {
39
39
  currency?: string;
40
40
  };
41
+ logger?: Logger;
42
+ };
43
+ export type LogEvent = {
44
+ event: string;
45
+ timestamp: string;
46
+ provider: PaymentProvider;
47
+ action: "create" | "verify" | "parse";
48
+ stage: "request" | "response" | "error";
49
+ correlation_id: string;
50
+ status: "success" | "failure";
51
+ duration_ms?: number;
52
+ metadata?: {
53
+ amount?: number | string;
54
+ currency?: string;
55
+ reference?: string;
56
+ provider_ref?: string;
57
+ testMode?: boolean;
58
+ };
59
+ error?: {
60
+ code: string;
61
+ message: string;
62
+ };
63
+ };
64
+ export type Logger = {
65
+ log: (event: LogEvent) => void;
41
66
  };
42
67
  export type PaymentCreateInput = {
43
68
  amount: string | number;
@@ -68,6 +93,7 @@ export type Payment = {
68
93
  redirectUrl?: string;
69
94
  provider: PaymentProvider;
70
95
  providerRef?: string;
96
+ correlationId?: string;
71
97
  raw?: unknown;
72
98
  };
73
99
  export type PaymentVerifyInput = {
@@ -77,6 +103,7 @@ export type VerificationResult = {
77
103
  provider: PaymentProvider;
78
104
  status: "pending" | "paid" | "failed" | "unknown";
79
105
  providerRef?: string;
106
+ correlationId?: string;
80
107
  raw?: unknown;
81
108
  };
82
109
  export type WebhookEvent = {
@@ -99,6 +126,7 @@ export type WebhookParseInput = {
99
126
  export type ParsedWebhook = {
100
127
  event: WebhookEvent;
101
128
  provider: PaymentProvider;
129
+ correlationId?: string;
102
130
  raw: Record<string, unknown>;
103
131
  };
104
132
  export type PaymentRequest = {
@@ -22,6 +22,34 @@ test("createStash payments.create returns canonical payment", async () => {
22
22
  assert.equal(payment.provider, "payfast");
23
23
  assert.match(payment.id, /^[0-9a-f-]{36}$/i);
24
24
  assert.ok(payment.redirectUrl);
25
+ assert.match(payment.correlationId ?? "", /^[0-9a-f-]{36}$/i);
26
+ });
27
+ test("createStash emits canonical logs for payments.create", async () => {
28
+ const events = [];
29
+ const stash = createStash({
30
+ provider: "payfast",
31
+ credentials: {
32
+ merchantId: "merchant",
33
+ merchantKey: "key",
34
+ },
35
+ testMode: true,
36
+ logger: {
37
+ log: (event) => events.push(event),
38
+ },
39
+ });
40
+ const payment = await stash.payments.create({
41
+ amount: "100.00",
42
+ currency: "ZAR",
43
+ reference: "ORDER-1",
44
+ });
45
+ assert.equal(events.length, 2);
46
+ assert.equal(events[0].event, "payments.create.request");
47
+ assert.equal(events[1].event, "payments.create.response");
48
+ assert.equal(events[0].provider, "payfast");
49
+ assert.equal(events[1].provider, "payfast");
50
+ assert.equal(events[0].correlation_id, payment.correlationId);
51
+ assert.equal(events[1].correlation_id, payment.correlationId);
52
+ assert.equal(events[1].metadata.testMode, true);
25
53
  });
26
54
  test("createStash payments.create maps ozow response", async () => {
27
55
  const originalFetch = globalThis.fetch;
@@ -127,6 +155,7 @@ test("webhooks.parse returns canonical event for payfast", () => {
127
155
  assert.equal(parsed.event.type, "payment.completed");
128
156
  assert.equal(parsed.event.data.reference, "ORDER-100");
129
157
  assert.equal(parsed.provider, "payfast");
158
+ assert.match(parsed.correlationId ?? "", /^[0-9a-f-]{36}$/i);
130
159
  });
131
160
  test("webhooks.parse throws invalid_signature for payfast", () => {
132
161
  const stash = createStash({
@@ -172,6 +201,7 @@ test("webhooks.parse returns canonical event for paystack", () => {
172
201
  assert.equal(parsed.provider, "paystack");
173
202
  assert.equal(parsed.event.type, "payment.completed");
174
203
  assert.equal(parsed.event.data.reference, "REF-3");
204
+ assert.match(parsed.correlationId ?? "", /^[0-9a-f-]{36}$/i);
175
205
  });
176
206
  test("payments.verify throws unsupported_capability for payfast", async () => {
177
207
  const stash = createStash({
@@ -205,5 +235,6 @@ test("payments.verify returns paid for paystack", async () => {
205
235
  });
206
236
  const result = await stash.payments.verify({ reference: "REF-1" });
207
237
  assert.equal(result.status, "paid");
238
+ assert.match(result.correlationId ?? "", /^[0-9a-f-]{36}$/i);
208
239
  globalThis.fetch = originalFetch;
209
240
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miniduckco/stash",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "integrate payments. switch once.",
5
5
  "license": "MIT",
6
6
  "type": "module",