better-ship 0.20.0 → 0.22.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.
- package/dist/{application-Dg1jIKPe.js → application-D5tNECiC.js} +2 -2
- package/dist/application-D5tNECiC.js.map +1 -0
- package/dist/application.d.ts +2 -2
- package/dist/application.js +1 -1
- package/dist/better-auth/apple-client-secret.js +1 -1
- package/dist/better-auth/auth-client.js +1 -1
- package/dist/better-auth/rate-limit.js +1 -1
- package/dist/better-auth.js +1 -1
- package/dist/billing/client.js +1 -1
- package/dist/cloudflare/auth-secondary-storage.js +1 -1
- package/dist/cloudflare/durable-object-client.js +1 -1
- package/dist/cloudflare/durable-object-errors.js +1 -1
- package/dist/cloudflare/kv-client.js +1 -1
- package/dist/cloudflare/kv-errors.js +1 -1
- package/dist/cloudflare/queue.d.ts +1 -1
- package/dist/cloudflare/queue.js +2 -2
- package/dist/cloudflare/r2-client.js +1 -1
- package/dist/cloudflare/r2-errors.js +1 -1
- package/dist/cloudflare/scheduled.js +1 -1
- package/dist/cloudflare.d.ts +2 -2
- package/dist/cloudflare.js +3 -3
- package/dist/{core-BT1dSEQI.js → core-BIas38R0.js} +2 -2
- package/dist/core-BIas38R0.js.map +1 -0
- package/dist/core.js +1 -1
- package/dist/http.js +2 -2
- package/dist/{index-CYHXOH08.d.ts → index-tNZHSCpJ.d.ts} +6 -2
- package/dist/index-tNZHSCpJ.d.ts.map +1 -0
- package/dist/{outbox-repository-options-B8TnAE1h.js → outbox-repository-options-D0R8z1yS.js} +2 -2
- package/dist/{outbox-repository-options-B8TnAE1h.js.map → outbox-repository-options-D0R8z1yS.js.map} +1 -1
- package/dist/{outbox-repository-options-DjBgFhMi.d.ts → outbox-repository-options-_B8l8l6l.d.ts} +2 -2
- package/dist/{outbox-repository-options-DjBgFhMi.d.ts.map → outbox-repository-options-_B8l8l6l.d.ts.map} +1 -1
- package/dist/postgres.d.ts +2 -2
- package/dist/postgres.js +3 -3
- package/dist/posthog/metrics.d.ts +1 -1
- package/dist/posthog/server.d.ts +1 -1
- package/dist/posthog/server.js +1 -1
- package/dist/resend.d.ts +14 -16
- package/dist/resend.d.ts.map +1 -1
- package/dist/resend.js +42 -19
- package/dist/resend.js.map +1 -1
- package/dist/sentry/client.js +1 -1
- package/dist/sentry/server.d.ts +1 -1
- package/dist/stripe/client.d.ts +13 -0
- package/dist/stripe/client.d.ts.map +1 -0
- package/dist/stripe/client.js +20 -0
- package/dist/stripe/client.js.map +1 -0
- package/dist/stripe/event-map.d.ts +14 -0
- package/dist/stripe/event-map.d.ts.map +1 -0
- package/dist/stripe/event-map.js +19 -0
- package/dist/stripe/event-map.js.map +1 -0
- package/dist/stripe/payment-service.d.ts +4 -5
- package/dist/stripe/payment-service.d.ts.map +1 -1
- package/dist/stripe/payment-service.error.js +2 -2
- package/dist/stripe/payment-service.error.js.map +1 -1
- package/dist/stripe/payment-service.js +11 -35
- package/dist/stripe/payment-service.js.map +1 -1
- package/dist/stripe/payment-webhook.js +1 -1
- package/dist/stripe/request.d.ts +9 -0
- package/dist/stripe/request.d.ts.map +1 -0
- package/dist/stripe/request.js +21 -0
- package/dist/stripe/request.js.map +1 -0
- package/dist/tanstack.js +1 -1
- package/dist/testing/outbox-repository.d.ts +1 -1
- package/dist/testing/payment-service.d.ts.map +1 -1
- package/dist/testing/payment-service.js +12 -12
- package/dist/testing/payment-service.js.map +1 -1
- package/dist/testing/stripe.d.ts +21 -0
- package/dist/testing/stripe.d.ts.map +1 -0
- package/dist/testing/stripe.js +33 -0
- package/dist/testing/stripe.js.map +1 -0
- package/dist/{webhook-verification-qSFXv5H3.js → webhook-verification-CEUARY9X.js} +2 -2
- package/dist/{webhook-verification-qSFXv5H3.js.map → webhook-verification-CEUARY9X.js.map} +1 -1
- package/package.json +5 -1
- package/src/application/email-service.ts +3 -0
- package/src/application/index.ts +1 -0
- package/src/core/email-service.error.ts +1 -1
- package/src/infrastructure/resend/email-event.ts +3 -0
- package/src/infrastructure/resend/index.ts +2 -2
- package/src/infrastructure/resend/resend-email-service.ts +39 -17
- package/src/infrastructure/resend/resend-webhook.ts +15 -16
- package/src/infrastructure/stripe/client.ts +16 -0
- package/src/infrastructure/stripe/event-map.ts +25 -0
- package/src/infrastructure/stripe/payment-service.error.ts +2 -1
- package/src/infrastructure/stripe/payment-service.ts +15 -39
- package/src/infrastructure/stripe/request.ts +17 -0
- package/src/infrastructure/testing/payment-service-contract.ts +11 -14
- package/src/infrastructure/testing/stripe-fixture.ts +42 -0
- package/dist/application-Dg1jIKPe.js.map +0 -1
- package/dist/core-BT1dSEQI.js.map +0 -1
- package/dist/index-CYHXOH08.d.ts.map +0 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/infrastructure/stripe/request.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Invoke one SDK operation; the SDK owns retries configured on its client or request.
|
|
4
|
+
* Convert every Stripe error to PaymentServiceError without logging; preserve the original error in cause.
|
|
5
|
+
* Non-Stripe failures propagate unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export declare function stripeRequest<T>(operation: string, work: () => Promise<T>): Promise<T>;
|
|
8
|
+
//#endregion
|
|
9
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.ts","names":[],"sources":["../../src/infrastructure/stripe/request.ts"],"mappings":";;;;;;wBASsB,cAAc,GAAG,mBAAmB,YAAY,QAAQ,KAAK,QAAQ"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PaymentServiceError } from "./payment-service.error.js";
|
|
2
|
+
import Stripe from "stripe";
|
|
3
|
+
|
|
4
|
+
//#region src/infrastructure/stripe/request.ts
|
|
5
|
+
/**
|
|
6
|
+
* Invoke one SDK operation; the SDK owns retries configured on its client or request.
|
|
7
|
+
* Convert every Stripe error to PaymentServiceError without logging; preserve the original error in cause.
|
|
8
|
+
* Non-Stripe failures propagate unchanged.
|
|
9
|
+
*/
|
|
10
|
+
async function stripeRequest(operation, work) {
|
|
11
|
+
try {
|
|
12
|
+
return await work();
|
|
13
|
+
} catch (cause) {
|
|
14
|
+
if (!(cause instanceof Stripe.errors.StripeError)) throw cause;
|
|
15
|
+
throw new PaymentServiceError(operation, cause);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { stripeRequest };
|
|
21
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.js","names":[],"sources":["../../src/infrastructure/stripe/request.ts"],"sourcesContent":["import Stripe from 'stripe'\n\nimport { PaymentServiceError } from './payment-service.error.ts'\n\n/**\n * Invoke one SDK operation; the SDK owns retries configured on its client or request.\n * Convert every Stripe error to PaymentServiceError without logging; preserve the original error in cause.\n * Non-Stripe failures propagate unchanged.\n */\nexport async function stripeRequest<T>(operation: string, work: () => Promise<T>): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n if (!(cause instanceof Stripe.errors.StripeError)) throw cause\n throw new PaymentServiceError(operation, cause)\n }\n}\n"],"mappings":";;;;;;;;;AASA,eAAsB,cAAiB,WAAmB,MAAoC;CAC5F,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,OAAO,OAAO,cAAc,MAAM;EACzD,MAAM,IAAI,oBAAoB,WAAW,KAAK;CAChD;AACF"}
|
package/dist/tanstack.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as StaleSessionError, R as ValidationError, W as AuthenticationError, nt as AppError, r as createLogger } from "./core-
|
|
1
|
+
import { H as StaleSessionError, R as ValidationError, W as AuthenticationError, nt as AppError, r as createLogger } from "./core-BIas38R0.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getReasonPhrase } from "http-status-codes";
|
|
4
4
|
import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { W as DomainEvent } from "../index-DUFS0SJ_.js";
|
|
2
|
-
import { A as IOutboxRepository } from "../index-
|
|
2
|
+
import { A as IOutboxRepository } from "../index-tNZHSCpJ.js";
|
|
3
3
|
//#region src/infrastructure/testing/outbox-repository-contract.d.ts
|
|
4
4
|
/** Stored fields observed by the adapter contract, independent of a database library. */
|
|
5
5
|
export type OutboxContractRow = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-service.d.ts","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"payment-service.d.ts","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"mappings":";;;;;;;wBAYgB,uBACd,OAAO,cAAc,WAAW,wBAChC,SAAS,QAAQ,WAAW"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { PaymentServiceError } from "../stripe/payment-service.error.js";
|
|
2
|
+
import { stripeFixture } from "./stripe.js";
|
|
1
3
|
import Stripe from "stripe";
|
|
2
|
-
import { deepStrictEqual, equal, rejects } from "node:assert/strict";
|
|
4
|
+
import { deepStrictEqual, equal, ok, rejects } from "node:assert/strict";
|
|
3
5
|
|
|
4
6
|
//#region src/infrastructure/testing/payment-service-contract.ts
|
|
5
7
|
/**
|
|
@@ -39,7 +41,7 @@ function paymentServiceContract(test, create) {
|
|
|
39
41
|
status: "requires_capture"
|
|
40
42
|
});
|
|
41
43
|
});
|
|
42
|
-
test("card failures retain the native error
|
|
44
|
+
test("card failures retain the native error in cause", async () => {
|
|
43
45
|
const fixture = paymentFixture(create, Response.json({ error: {
|
|
44
46
|
type: "card_error",
|
|
45
47
|
code: "authentication_required",
|
|
@@ -50,23 +52,21 @@ function paymentServiceContract(test, create) {
|
|
|
50
52
|
currency: "usd",
|
|
51
53
|
customer: "cus_contract",
|
|
52
54
|
payment_method: "pm_contract"
|
|
53
|
-
}, { idempotencyKey: "charge:contract" }),
|
|
55
|
+
}, { idempotencyKey: "charge:contract" }), (error) => {
|
|
56
|
+
ok(error instanceof PaymentServiceError);
|
|
57
|
+
ok(error.cause instanceof Stripe.errors.StripeCardError);
|
|
58
|
+
equal(error.cause.code, "authentication_required");
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
63
|
function paymentFixture(create, response = Response.json({
|
|
57
64
|
id: "pi_contract",
|
|
58
65
|
status: "requires_capture"
|
|
59
66
|
})) {
|
|
60
|
-
const requests =
|
|
67
|
+
const { stripe, requests } = stripeFixture(() => response.clone());
|
|
61
68
|
return {
|
|
62
|
-
service: create(
|
|
63
|
-
const request = new Request(input, init);
|
|
64
|
-
requests.push({
|
|
65
|
-
headers: request.headers,
|
|
66
|
-
body: new URLSearchParams(new TextDecoder().decode(await request.arrayBuffer()))
|
|
67
|
-
});
|
|
68
|
-
return response.clone();
|
|
69
|
-
}) })),
|
|
69
|
+
service: create(stripe),
|
|
70
70
|
requests
|
|
71
71
|
};
|
|
72
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-service.js","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"sourcesContent":["import { deepStrictEqual, equal, rejects } from 'node:assert/strict'\n\nimport Stripe from 'stripe'\n\nimport type { IPaymentService } from '../stripe/payment-service.ts'\n\n/**\n * Test a Stripe payment adapter through native fetch injection without live payments.\n * Each case constructs a fresh SDK instance and adapter; no Vitest dependency is required.\n */\nexport function paymentServiceContract(\n test: (name: string, run: () => Promise<void>) => void,\n create: (stripe: Stripe) => IPaymentService,\n): void {\n test('payment checkout selects payment mode and retains the supplied key', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.createCheckout(\n { customer: 'cus_contract' },\n { idempotencyKey: 'checkout:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'payment')\n equal(fixture.requests[0]?.headers.get('idempotency-key'), 'checkout:contract')\n })\n\n test('payment method setup selects setup mode', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.setupPaymentMethod(\n { customer: 'cus_contract' },\n { idempotencyKey: 'setup:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'setup')\n })\n\n test('saved method payments confirm off-session', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.chargeSavedMethod(\n { amount: 1000, currency: 'usd', customer: 'cus_contract', payment_method: 'pm_contract' },\n { idempotencyKey: 'charge:contract' },\n )\n equal(fixture.requests[0]?.body.get('confirm'), 'true')\n equal(fixture.requests[0]?.body.get('off_session'), 'true')\n })\n\n test('payment reads preserve native statuses and response fields', async () => {\n const fixture = paymentFixture(create)\n const payment = await fixture.service.getPayment('pi_contract')\n deepStrictEqual(\n { id: payment.id, status: payment.status },\n { id: 'pi_contract', status: 'requires_capture' },\n )\n })\n\n test('card failures retain the native error
|
|
1
|
+
{"version":3,"file":"payment-service.js","names":[],"sources":["../../src/infrastructure/testing/payment-service-contract.ts"],"sourcesContent":["import { deepStrictEqual, equal, ok, rejects } from 'node:assert/strict'\n\nimport Stripe from 'stripe'\n\nimport { PaymentServiceError } from '../stripe/payment-service.error.ts'\nimport type { IPaymentService } from '../stripe/payment-service.ts'\nimport { stripeFixture } from './stripe-fixture.ts'\n\n/**\n * Test a Stripe payment adapter through native fetch injection without live payments.\n * Each case constructs a fresh SDK instance and adapter; no Vitest dependency is required.\n */\nexport function paymentServiceContract(\n test: (name: string, run: () => Promise<void>) => void,\n create: (stripe: Stripe) => IPaymentService,\n): void {\n test('payment checkout selects payment mode and retains the supplied key', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.createCheckout(\n { customer: 'cus_contract' },\n { idempotencyKey: 'checkout:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'payment')\n equal(fixture.requests[0]?.headers.get('idempotency-key'), 'checkout:contract')\n })\n\n test('payment method setup selects setup mode', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.setupPaymentMethod(\n { customer: 'cus_contract' },\n { idempotencyKey: 'setup:contract' },\n )\n equal(fixture.requests[0]?.body.get('mode'), 'setup')\n })\n\n test('saved method payments confirm off-session', async () => {\n const fixture = paymentFixture(create)\n await fixture.service.chargeSavedMethod(\n { amount: 1000, currency: 'usd', customer: 'cus_contract', payment_method: 'pm_contract' },\n { idempotencyKey: 'charge:contract' },\n )\n equal(fixture.requests[0]?.body.get('confirm'), 'true')\n equal(fixture.requests[0]?.body.get('off_session'), 'true')\n })\n\n test('payment reads preserve native statuses and response fields', async () => {\n const fixture = paymentFixture(create)\n const payment = await fixture.service.getPayment('pi_contract')\n deepStrictEqual(\n { id: payment.id, status: payment.status },\n { id: 'pi_contract', status: 'requires_capture' },\n )\n })\n\n test('card failures retain the native error in cause', async () => {\n const response = Response.json(\n {\n error: {\n type: 'card_error',\n code: 'authentication_required',\n message: 'Authentication required',\n },\n },\n { status: 402 },\n )\n const fixture = paymentFixture(create, response)\n await rejects(\n fixture.service.chargeSavedMethod(\n { amount: 1000, currency: 'usd', customer: 'cus_contract', payment_method: 'pm_contract' },\n { idempotencyKey: 'charge:contract' },\n ),\n (error) => {\n ok(error instanceof PaymentServiceError)\n ok(error.cause instanceof Stripe.errors.StripeCardError)\n equal(error.cause.code, 'authentication_required')\n return true\n },\n )\n })\n}\n\nfunction paymentFixture(\n create: (stripe: Stripe) => IPaymentService,\n response = Response.json({ id: 'pi_contract', status: 'requires_capture' }),\n) {\n const { stripe, requests } = stripeFixture(() => response.clone())\n return { service: create(stripe), requests }\n}\n"],"mappings":";;;;;;;;;;AAYA,SAAgB,uBACd,MACA,QACM;CACN,KAAK,sEAAsE,YAAY;EACrF,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,eACpB,EAAE,UAAU,eAAe,GAC3B,EAAE,gBAAgB,oBAAoB,CACxC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,MAAM,GAAG,SAAS;EACtD,MAAM,QAAQ,SAAS,EAAE,EAAE,QAAQ,IAAI,iBAAiB,GAAG,mBAAmB;CAChF,CAAC;CAED,KAAK,2CAA2C,YAAY;EAC1D,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,mBACpB,EAAE,UAAU,eAAe,GAC3B,EAAE,gBAAgB,iBAAiB,CACrC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,MAAM,GAAG,OAAO;CACtD,CAAC;CAED,KAAK,6CAA6C,YAAY;EAC5D,MAAM,UAAU,eAAe,MAAM;EACrC,MAAM,QAAQ,QAAQ,kBACpB;GAAE,QAAQ;GAAM,UAAU;GAAO,UAAU;GAAgB,gBAAgB;EAAc,GACzF,EAAE,gBAAgB,kBAAkB,CACtC;EACA,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,SAAS,GAAG,MAAM;EACtD,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK,IAAI,aAAa,GAAG,MAAM;CAC5D,CAAC;CAED,KAAK,8DAA8D,YAAY;EAE7E,MAAM,UAAU,MADA,eAAe,MACH,CAAC,CAAC,QAAQ,WAAW,aAAa;EAC9D,gBACE;GAAE,IAAI,QAAQ;GAAI,QAAQ,QAAQ;EAAO,GACzC;GAAE,IAAI;GAAe,QAAQ;EAAmB,CAClD;CACF,CAAC;CAED,KAAK,kDAAkD,YAAY;EAWjE,MAAM,UAAU,eAAe,QAVd,SAAS,KACxB,EACE,OAAO;GACL,MAAM;GACN,MAAM;GACN,SAAS;EACX,EACF,GACA,EAAE,QAAQ,IAAI,CAE8B,CAAC;EAC/C,MAAM,QACJ,QAAQ,QAAQ,kBACd;GAAE,QAAQ;GAAM,UAAU;GAAO,UAAU;GAAgB,gBAAgB;EAAc,GACzF,EAAE,gBAAgB,kBAAkB,CACtC,IACC,UAAU;GACT,GAAG,iBAAiB,mBAAmB;GACvC,GAAG,MAAM,iBAAiB,OAAO,OAAO,eAAe;GACvD,MAAM,MAAM,MAAM,MAAM,yBAAyB;GACjD,OAAO;EACT,CACF;CACF,CAAC;AACH;AAEA,SAAS,eACP,QACA,WAAW,SAAS,KAAK;CAAE,IAAI;CAAe,QAAQ;AAAmB,CAAC,GAC1E;CACA,MAAM,EAAE,QAAQ,aAAa,oBAAoB,SAAS,MAAM,CAAC;CACjE,OAAO;EAAE,SAAS,OAAO,MAAM;EAAG;CAAS;AAC7C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
//#region src/infrastructure/testing/stripe-fixture.d.ts
|
|
3
|
+
/** One SDK HTTP attempt; query and body expose Stripe's URL-encoded parameters. */
|
|
4
|
+
export type StripeCall = {
|
|
5
|
+
readonly method: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly query: URLSearchParams;
|
|
8
|
+
readonly body: URLSearchParams;
|
|
9
|
+
readonly headers: Headers;
|
|
10
|
+
};
|
|
11
|
+
type StripeFixture = {
|
|
12
|
+
stripe: Stripe;
|
|
13
|
+
requests: readonly StripeCall[];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Construct the real SDK with a fake fetch transport and an ordered record of HTTP attempts.
|
|
17
|
+
* The responder receives an unread request. The shared client factory supplies retry defaults.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stripeFixture(respond: (request: Request) => Response | Promise<Response>): StripeFixture;
|
|
20
|
+
//#endregion
|
|
21
|
+
//# sourceMappingURL=stripe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.d.ts","names":[],"sources":["../../src/infrastructure/testing/stripe-fixture.ts"],"mappings":";;;YAKY;WACD;WACA;WACA,OAAO;WACP,MAAM;WACN,SAAS;;KAGf;EAAkB,QAAQ;EAAQ,mBAAmB;;;;;;wBAM1C,cACd,UAAU,SAAS,YAAY,WAAW,QAAQ,YACjD"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createStripeClient } from "../stripe/client.js";
|
|
2
|
+
import Stripe from "stripe";
|
|
3
|
+
|
|
4
|
+
//#region src/infrastructure/testing/stripe-fixture.ts
|
|
5
|
+
/**
|
|
6
|
+
* Construct the real SDK with a fake fetch transport and an ordered record of HTTP attempts.
|
|
7
|
+
* The responder receives an unread request. The shared client factory supplies retry defaults.
|
|
8
|
+
*/
|
|
9
|
+
function stripeFixture(respond) {
|
|
10
|
+
const requests = [];
|
|
11
|
+
return {
|
|
12
|
+
stripe: createStripeClient("sk_test_fixture", { httpClient: Stripe.createFetchHttpClient(async (input, init) => {
|
|
13
|
+
const request = new Request(input, init);
|
|
14
|
+
const url = new URL(request.url);
|
|
15
|
+
const call = {
|
|
16
|
+
method: request.method,
|
|
17
|
+
path: url.pathname,
|
|
18
|
+
query: url.searchParams,
|
|
19
|
+
body: new URLSearchParams(),
|
|
20
|
+
headers: new Headers(request.headers)
|
|
21
|
+
};
|
|
22
|
+
requests.push(call);
|
|
23
|
+
const body = new TextDecoder().decode(await request.clone().arrayBuffer());
|
|
24
|
+
for (const [key, value] of new URLSearchParams(body)) call.body.append(key, value);
|
|
25
|
+
return respond(request);
|
|
26
|
+
}) }),
|
|
27
|
+
requests
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { stripeFixture };
|
|
33
|
+
//# sourceMappingURL=stripe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe.js","names":[],"sources":["../../src/infrastructure/testing/stripe-fixture.ts"],"sourcesContent":["import Stripe from 'stripe'\n\nimport { createStripeClient } from '../stripe/client.ts'\n\n/** One SDK HTTP attempt; query and body expose Stripe's URL-encoded parameters. */\nexport type StripeCall = {\n readonly method: string\n readonly path: string\n readonly query: URLSearchParams\n readonly body: URLSearchParams\n readonly headers: Headers\n}\n\ntype StripeFixture = { stripe: Stripe; requests: readonly StripeCall[] }\n\n/**\n * Construct the real SDK with a fake fetch transport and an ordered record of HTTP attempts.\n * The responder receives an unread request. The shared client factory supplies retry defaults.\n */\nexport function stripeFixture(\n respond: (request: Request) => Response | Promise<Response>,\n): StripeFixture {\n const requests: StripeCall[] = []\n const stripe = createStripeClient('sk_test_fixture', {\n httpClient: Stripe.createFetchHttpClient(async (input, init) => {\n const request = new Request(input, init)\n const url = new URL(request.url)\n const call: StripeCall = {\n method: request.method,\n path: url.pathname,\n query: url.searchParams,\n body: new URLSearchParams(),\n headers: new Headers(request.headers),\n }\n requests.push(call)\n const body = new TextDecoder().decode(await request.clone().arrayBuffer())\n for (const [key, value] of new URLSearchParams(body)) call.body.append(key, value)\n return respond(request)\n }),\n })\n return { stripe, requests }\n}\n"],"mappings":";;;;;;;;AAmBA,SAAgB,cACd,SACe;CACf,MAAM,WAAyB,CAAC;CAkBhC,OAAO;EAAE,QAjBM,mBAAmB,mBAAmB,EACnD,YAAY,OAAO,sBAAsB,OAAO,OAAO,SAAS;GAC9D,MAAM,UAAU,IAAI,QAAQ,OAAO,IAAI;GACvC,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;GAC/B,MAAM,OAAmB;IACvB,QAAQ,QAAQ;IAChB,MAAM,IAAI;IACV,OAAO,IAAI;IACX,MAAM,IAAI,gBAAgB;IAC1B,SAAS,IAAI,QAAQ,QAAQ,OAAO;GACtC;GACA,SAAS,KAAK,IAAI;GAClB,MAAM,OAAO,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,QAAQ,MAAM,CAAC,CAAC,YAAY,CAAC;GACzE,KAAK,MAAM,CAAC,KAAK,UAAU,IAAI,gBAAgB,IAAI,GAAG,KAAK,KAAK,OAAO,KAAK,KAAK;GACjF,OAAO,QAAQ,OAAO;EACxB,CAAC,EACH,CACc;EAAG;CAAS;AAC5B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { nt as AppError } from "./core-
|
|
1
|
+
import { nt as AppError } from "./core-BIas38R0.js";
|
|
2
2
|
|
|
3
3
|
//#region src/infrastructure/http/webhook-verification.ts
|
|
4
4
|
/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */
|
|
@@ -15,4 +15,4 @@ var WebhookVerificationError = class extends AppError {
|
|
|
15
15
|
|
|
16
16
|
//#endregion
|
|
17
17
|
export { WebhookVerificationError as t };
|
|
18
|
-
//# sourceMappingURL=webhook-verification-
|
|
18
|
+
//# sourceMappingURL=webhook-verification-CEUARY9X.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook-verification-
|
|
1
|
+
{"version":3,"file":"webhook-verification-CEUARY9X.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Verified> {\n /**\n * Verify the exact body before parsing it. Return null for an ignored webhook.\n * @throws WebhookVerificationError when the request cannot be trusted or parsed.\n */\n verify(body: string, headers: Headers): Promise<Verified | null>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "better-ship",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.22.0",
|
|
5
5
|
"description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -44,13 +44,17 @@
|
|
|
44
44
|
"./sentry/client": "./dist/sentry/client.js",
|
|
45
45
|
"./sentry/server": "./dist/sentry/server.js",
|
|
46
46
|
"./sentry/vite": "./dist/sentry/vite.js",
|
|
47
|
+
"./stripe/client": "./dist/stripe/client.js",
|
|
48
|
+
"./stripe/event-map": "./dist/stripe/event-map.js",
|
|
47
49
|
"./stripe/payment-service": "./dist/stripe/payment-service.js",
|
|
48
50
|
"./stripe/payment-service.error": "./dist/stripe/payment-service.error.js",
|
|
49
51
|
"./stripe/payment-webhook": "./dist/stripe/payment-webhook.js",
|
|
52
|
+
"./stripe/request": "./dist/stripe/request.js",
|
|
50
53
|
"./tanstack": "./dist/tanstack.js",
|
|
51
54
|
"./tanstack/server": "./dist/tanstack/server.js",
|
|
52
55
|
"./testing/outbox-repository": "./dist/testing/outbox-repository.js",
|
|
53
56
|
"./testing/payment-service": "./dist/testing/payment-service.js",
|
|
57
|
+
"./testing/stripe": "./dist/testing/stripe.js",
|
|
54
58
|
"./ui": "./dist/ui.js",
|
|
55
59
|
"./package.json": "./package.json"
|
|
56
60
|
},
|
|
@@ -32,6 +32,9 @@ export type OutboundEmail = {
|
|
|
32
32
|
readonly attachments?: readonly EmailAttachment[]
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** Received email headers, without the body or attachments. */
|
|
36
|
+
export type ReceivedEmail = { readonly headers: Readonly<Record<string, string>> }
|
|
37
|
+
|
|
35
38
|
/** The provider accepted the email and assigned its id. */
|
|
36
39
|
export type EmailAccepted = { readonly providerId: string }
|
|
37
40
|
|
package/src/application/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export class EmailServiceError extends AppError {
|
|
|
40
40
|
readonly retryAfter: number | null
|
|
41
41
|
readonly cause: unknown
|
|
42
42
|
}) {
|
|
43
|
-
super(`Email
|
|
43
|
+
super(`Email request failed: ${args.provider} ${args.providerCode}`, { cause: args.cause })
|
|
44
44
|
this.reason = args.reason
|
|
45
45
|
this.classification = CLASSIFICATION_BY_REASON[args.reason]
|
|
46
46
|
this.provider = args.provider
|
|
@@ -37,6 +37,7 @@ const EMAIL_TYPES = {
|
|
|
37
37
|
|
|
38
38
|
const BaseDataSchema = z.object({
|
|
39
39
|
email_id: z.string().min(1),
|
|
40
|
+
message_id: z.string().min(1),
|
|
40
41
|
to: z.array(EmailAddressSchema).min(1),
|
|
41
42
|
subject: z.string(),
|
|
42
43
|
tags: z.record(z.string(), z.string()).default({}),
|
|
@@ -67,6 +68,7 @@ const EmailPayloadSchema = z.discriminatedUnion('type', [
|
|
|
67
68
|
type EmailEventBase = {
|
|
68
69
|
readonly id: string
|
|
69
70
|
readonly emailId: string
|
|
71
|
+
readonly messageId: string
|
|
70
72
|
readonly recipients: readonly string[]
|
|
71
73
|
readonly subject: string
|
|
72
74
|
readonly tags: Readonly<Record<string, string>>
|
|
@@ -100,6 +102,7 @@ export function parseEmailEvent(payload: WebhookEventPayload, id: string): Email
|
|
|
100
102
|
const base: EmailEventBase = {
|
|
101
103
|
id,
|
|
102
104
|
emailId: event.data.email_id,
|
|
105
|
+
messageId: event.data.message_id,
|
|
103
106
|
recipients: event.data.to,
|
|
104
107
|
subject: event.data.subject,
|
|
105
108
|
tags: event.data.tags,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { ResendEmailService
|
|
2
|
-
export { ResendWebhook
|
|
1
|
+
export { ResendEmailService } from './resend-email-service.ts'
|
|
2
|
+
export { ResendWebhook } from './resend-webhook.ts'
|
|
3
3
|
export type { EmailEvent, EmailEventType } from './email-event.ts'
|
|
@@ -2,7 +2,13 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
|
|
3
3
|
import type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
EmailAccepted,
|
|
7
|
+
EmailAttachment,
|
|
8
|
+
IEmailService,
|
|
9
|
+
OutboundEmail,
|
|
10
|
+
ReceivedEmail,
|
|
11
|
+
} from '@/application'
|
|
6
12
|
import { EmailServiceError, type EmailSendFailureReason } from '@/core'
|
|
7
13
|
|
|
8
14
|
// SDK additions must receive an explicit classification before the package builds.
|
|
@@ -67,37 +73,27 @@ function toAttachment(attachment: EmailAttachment): Attachment {
|
|
|
67
73
|
return result
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
/** The SDK operations required by the adapter; a test factory can return a plain object. */
|
|
71
|
-
export type ResendEmailClient = { readonly emails: Pick<Resend['emails'], 'send'> }
|
|
72
|
-
|
|
73
76
|
/**
|
|
74
77
|
* Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.
|
|
75
78
|
* The Resend SDK itself logs API errors outside production.
|
|
76
79
|
*/
|
|
77
80
|
export class ResendEmailService implements IEmailService {
|
|
78
|
-
/** Load the SDK on the first
|
|
81
|
+
/** Load the SDK on the first email operation. Constructing the service does not load it. */
|
|
79
82
|
static create(apiKey: string, fromAddress: string): ResendEmailService {
|
|
80
|
-
return new ResendEmailService(
|
|
81
|
-
() => import('resend').then(({ Resend }) => new Resend(apiKey)),
|
|
82
|
-
fromAddress,
|
|
83
|
-
)
|
|
83
|
+
return new ResendEmailService(apiKey, fromAddress)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
private client:
|
|
86
|
+
private client: Resend | undefined
|
|
87
87
|
|
|
88
|
-
/**
|
|
88
|
+
/** Store configuration. Load the SDK and create its client on the first email operation. */
|
|
89
89
|
constructor(
|
|
90
|
-
private readonly
|
|
90
|
+
private readonly apiKey: string,
|
|
91
91
|
private readonly fromAddress: string,
|
|
92
92
|
) {}
|
|
93
93
|
|
|
94
94
|
/** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */
|
|
95
95
|
async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {
|
|
96
|
-
|
|
97
|
-
// Cache synchronous factory throws as rejections too, so later sends cannot initialize again.
|
|
98
|
-
this.client = Promise.resolve().then(this.createClient)
|
|
99
|
-
}
|
|
100
|
-
const client = await this.client
|
|
96
|
+
const client = await this.getClient()
|
|
101
97
|
const payload: CreateEmailOptions = {
|
|
102
98
|
from: email.from ?? this.fromAddress,
|
|
103
99
|
to: [email.to].flat(),
|
|
@@ -125,4 +121,30 @@ export class ResendEmailService implements IEmailService {
|
|
|
125
121
|
}
|
|
126
122
|
return { providerId: result.data.id }
|
|
127
123
|
}
|
|
124
|
+
|
|
125
|
+
/** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */
|
|
126
|
+
async getReceivedEmail(emailId: string): Promise<ReceivedEmail> {
|
|
127
|
+
const client = await this.getClient()
|
|
128
|
+
const result = await client.emails.receiving.get(emailId)
|
|
129
|
+
if (result.error !== null) {
|
|
130
|
+
throw new EmailServiceError({
|
|
131
|
+
reason: reasonFromResend(result.error),
|
|
132
|
+
provider: 'resend',
|
|
133
|
+
providerCode: result.error.name,
|
|
134
|
+
statusCode: result.error.statusCode,
|
|
135
|
+
retryAfter: retryAfterSeconds(result.headers?.['retry-after']),
|
|
136
|
+
cause: result.error,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
return { headers: result.data.headers ?? {} }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async getClient(): Promise<Resend> {
|
|
143
|
+
if (this.client === undefined) {
|
|
144
|
+
const { Resend } = await import('resend')
|
|
145
|
+
// Recheck after the import so concurrent calls share one client.
|
|
146
|
+
this.client ??= new Resend(this.apiKey)
|
|
147
|
+
}
|
|
148
|
+
return this.client
|
|
149
|
+
}
|
|
128
150
|
}
|
|
@@ -9,27 +9,17 @@ const SignatureErrorSchema = z
|
|
|
9
9
|
.instanceof(Error)
|
|
10
10
|
.refine((error) => error.name === 'WebhookVerificationError')
|
|
11
11
|
|
|
12
|
-
/** The SDK's local signature verifier. It makes no API request. */
|
|
13
|
-
export type ResendWebhookClient = Pick<Resend['webhooks'], 'verify'>
|
|
14
|
-
|
|
15
12
|
/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */
|
|
16
13
|
export class ResendWebhook implements WebhookVerifier<EmailEvent> {
|
|
17
14
|
/** Load the SDK on the first verification. Only the webhook secret is required. */
|
|
18
15
|
static create(webhookSecret: string): ResendWebhook {
|
|
19
|
-
return new ResendWebhook(webhookSecret
|
|
20
|
-
const { Resend } = await import('resend')
|
|
21
|
-
// The SDK requires a nonempty key even though its local verifier never uses that key.
|
|
22
|
-
return new Resend('re_webhook_verification_only').webhooks
|
|
23
|
-
})
|
|
16
|
+
return new ResendWebhook(webhookSecret)
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
private client:
|
|
19
|
+
private client: Resend['webhooks'] | undefined
|
|
27
20
|
|
|
28
|
-
/**
|
|
29
|
-
constructor(
|
|
30
|
-
private readonly webhookSecret: string,
|
|
31
|
-
private readonly verifier: () => Promise<ResendWebhookClient>,
|
|
32
|
-
) {}
|
|
21
|
+
/** Store the webhook secret. Load the SDK on the first verification. */
|
|
22
|
+
constructor(private readonly webhookSecret: string) {}
|
|
33
23
|
|
|
34
24
|
/** Verify the raw body and Svix headers. Invalid requests throw typed errors; other failures propagate. */
|
|
35
25
|
async verify(body: string, headers: Headers): Promise<EmailEvent | null> {
|
|
@@ -37,8 +27,8 @@ export class ResendWebhook implements WebhookVerifier<EmailEvent> {
|
|
|
37
27
|
const timestamp = headers.get('svix-timestamp')
|
|
38
28
|
const signature = headers.get('svix-signature')
|
|
39
29
|
if (!id || !timestamp || !signature) throw new WebhookVerificationError('missing_headers')
|
|
40
|
-
const client = await
|
|
41
|
-
let payload: ReturnType<
|
|
30
|
+
const client = await this.getClient()
|
|
31
|
+
let payload: ReturnType<Resend['webhooks']['verify']>
|
|
42
32
|
try {
|
|
43
33
|
payload = client.verify({
|
|
44
34
|
payload: body,
|
|
@@ -57,4 +47,13 @@ export class ResendWebhook implements WebhookVerifier<EmailEvent> {
|
|
|
57
47
|
throw cause
|
|
58
48
|
}
|
|
59
49
|
}
|
|
50
|
+
|
|
51
|
+
private async getClient(): Promise<Resend['webhooks']> {
|
|
52
|
+
if (this.client === undefined) {
|
|
53
|
+
const { Resend } = await import('resend')
|
|
54
|
+
// The SDK requires a nonempty key even though its local verifier never uses that key.
|
|
55
|
+
this.client ??= new Resend('re_webhook_verification_only').webhooks
|
|
56
|
+
}
|
|
57
|
+
return this.client
|
|
58
|
+
}
|
|
60
59
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Stripe from 'stripe'
|
|
2
|
+
|
|
3
|
+
/** Default SDK retries; client configuration and individual requests can override this value. */
|
|
4
|
+
export const STRIPE_REQUEST_OPTIONS = Object.freeze({ maxNetworkRetries: 2 })
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Construct a native Stripe client with fetch transport and two SDK retries by default.
|
|
8
|
+
* Native configuration overrides these defaults. Inject the client into application services.
|
|
9
|
+
*/
|
|
10
|
+
export function createStripeClient(secretKey: string, options: Stripe.StripeConfig = {}): Stripe {
|
|
11
|
+
return new Stripe(secretKey, {
|
|
12
|
+
httpClient: Stripe.createFetchHttpClient(),
|
|
13
|
+
...STRIPE_REQUEST_OPTIONS,
|
|
14
|
+
...options,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type Stripe from 'stripe'
|
|
2
|
+
|
|
3
|
+
type EventObject<K extends Stripe.Event['type']> = Extract<
|
|
4
|
+
Stripe.Event,
|
|
5
|
+
{ type: K }
|
|
6
|
+
>['data']['object']
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert verified Stripe events with callbacks typed to each event's object.
|
|
10
|
+
* Unmapped events return undefined. Applications own validation and payment policy.
|
|
11
|
+
*/
|
|
12
|
+
export function stripeEventMap<T>(map: {
|
|
13
|
+
[K in Stripe.Event['type']]?: (object: EventObject<K>) => T
|
|
14
|
+
}): (event: Stripe.Event) => T | undefined {
|
|
15
|
+
const handlers: Partial<Record<Stripe.Event['type'], (object: never) => T>> = map
|
|
16
|
+
return (event) => {
|
|
17
|
+
// SAFETY: The map binds each event type to its object; indexed lookup loses that correlation.
|
|
18
|
+
return handlers[event.type]?.(event.data.object as never)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Identify one Stripe event delivery; different events for one payment retain different IDs. */
|
|
23
|
+
export function stripeEventId(event: Stripe.Event): string {
|
|
24
|
+
return `stripe:event:${event.id}`
|
|
25
|
+
}
|
|
@@ -26,7 +26,8 @@ function classifyStripeError(error: Stripe.errors.StripeError): FailureClassific
|
|
|
26
26
|
error instanceof Stripe.errors.StripeAuthenticationError ||
|
|
27
27
|
error instanceof Stripe.errors.StripePermissionError ||
|
|
28
28
|
error instanceof Stripe.errors.StripeInvalidRequestError ||
|
|
29
|
-
error instanceof Stripe.errors.StripeIdempotencyError
|
|
29
|
+
error instanceof Stripe.errors.StripeIdempotencyError ||
|
|
30
|
+
error instanceof Stripe.errors.StripeCardError
|
|
30
31
|
)
|
|
31
32
|
return 'terminal'
|
|
32
33
|
if (error.headers?.['stripe-should-retry'] === 'true') return 'transient'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Stripe from 'stripe'
|
|
1
|
+
import type Stripe from 'stripe'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { stripeRequest } from './request.ts'
|
|
4
4
|
|
|
5
5
|
/** Supply the same key and parameters when repeating one durable payment attempt. */
|
|
6
6
|
export type PaymentRequestOptions = Stripe.RequestOptions &
|
|
@@ -24,7 +24,7 @@ export type PaymentResult = Awaited<ReturnType<Stripe['paymentIntents']['create'
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Stripe payment operations, with native types and stable keys for mutations.
|
|
27
|
-
* Rejects with PaymentServiceError
|
|
27
|
+
* Rejects with PaymentServiceError for Stripe failures; non-Stripe failures propagate unchanged.
|
|
28
28
|
*/
|
|
29
29
|
export interface IPaymentService {
|
|
30
30
|
/** Create a customer; the application owns persistence and reuse of the customer link. */
|
|
@@ -36,7 +36,7 @@ export interface IPaymentService {
|
|
|
36
36
|
createCheckout(input: CheckoutInput, options: PaymentRequestOptions): Promise<CheckoutResult>
|
|
37
37
|
/** Create checkout in setup mode. The application records consent and verifies the saved method's owner. */
|
|
38
38
|
setupPaymentMethod(input: CheckoutInput, options: PaymentRequestOptions): Promise<CheckoutResult>
|
|
39
|
-
/** Confirm a saved method off-session.
|
|
39
|
+
/** Confirm a saved method off-session. PaymentServiceError retains card errors in cause. */
|
|
40
40
|
chargeSavedMethod(
|
|
41
41
|
input: SavedMethodCharge,
|
|
42
42
|
options: PaymentRequestOptions,
|
|
@@ -51,7 +51,7 @@ export interface IPaymentService {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Ported from Typist's StripeClient request boundary; the native SDK now owns retries.
|
|
54
|
-
*
|
|
54
|
+
* The injected client owns retry and timeout defaults. Native request options can override them.
|
|
55
55
|
*/
|
|
56
56
|
export class StripePaymentService implements IPaymentService {
|
|
57
57
|
/** Inject a native SDK instance; its transport, credentials, and API version remain caller-owned. */
|
|
@@ -62,18 +62,13 @@ export class StripePaymentService implements IPaymentService {
|
|
|
62
62
|
input: Stripe.CustomerCreateParams,
|
|
63
63
|
options: PaymentRequestOptions,
|
|
64
64
|
): Promise<CustomerResult> {
|
|
65
|
-
return
|
|
66
|
-
this.stripe.customers.create(input, { maxNetworkRetries: 2, ...options }),
|
|
67
|
-
)
|
|
65
|
+
return stripeRequest('customers.create', () => this.stripe.customers.create(input, options))
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
/** Force payment mode after caller parameters; return the native Checkout Session. */
|
|
71
69
|
createCheckout(input: CheckoutInput, options: PaymentRequestOptions): Promise<CheckoutResult> {
|
|
72
|
-
return
|
|
73
|
-
this.stripe.checkout.sessions.create(
|
|
74
|
-
{ ...input, mode: 'payment' },
|
|
75
|
-
{ maxNetworkRetries: 2, ...options },
|
|
76
|
-
),
|
|
70
|
+
return stripeRequest('checkout.sessions.create', () =>
|
|
71
|
+
this.stripe.checkout.sessions.create({ ...input, mode: 'payment' }, options),
|
|
77
72
|
)
|
|
78
73
|
}
|
|
79
74
|
|
|
@@ -82,24 +77,18 @@ export class StripePaymentService implements IPaymentService {
|
|
|
82
77
|
input: CheckoutInput,
|
|
83
78
|
options: PaymentRequestOptions,
|
|
84
79
|
): Promise<CheckoutResult> {
|
|
85
|
-
return
|
|
86
|
-
this.stripe.checkout.sessions.create(
|
|
87
|
-
{ ...input, mode: 'setup' },
|
|
88
|
-
{ maxNetworkRetries: 2, ...options },
|
|
89
|
-
),
|
|
80
|
+
return stripeRequest('checkout.sessions.create', () =>
|
|
81
|
+
this.stripe.checkout.sessions.create({ ...input, mode: 'setup' }, options),
|
|
90
82
|
)
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
/** Force confirmation and off-session use;
|
|
85
|
+
/** Force confirmation and off-session use; retain native card failures in PaymentServiceError.cause. */
|
|
94
86
|
chargeSavedMethod(
|
|
95
87
|
input: SavedMethodCharge,
|
|
96
88
|
options: PaymentRequestOptions,
|
|
97
89
|
): Promise<PaymentResult> {
|
|
98
|
-
return
|
|
99
|
-
this.stripe.paymentIntents.create(
|
|
100
|
-
{ ...input, confirm: true, off_session: true },
|
|
101
|
-
{ maxNetworkRetries: 2, ...options },
|
|
102
|
-
),
|
|
90
|
+
return stripeRequest('paymentIntents.create', () =>
|
|
91
|
+
this.stripe.paymentIntents.create({ ...input, confirm: true, off_session: true }, options),
|
|
103
92
|
)
|
|
104
93
|
}
|
|
105
94
|
|
|
@@ -109,21 +98,8 @@ export class StripePaymentService implements IPaymentService {
|
|
|
109
98
|
params?: Stripe.PaymentIntentRetrieveParams,
|
|
110
99
|
options?: Stripe.RequestOptions,
|
|
111
100
|
): Promise<PaymentResult> {
|
|
112
|
-
return
|
|
113
|
-
this.stripe.paymentIntents.retrieve(paymentId, params,
|
|
101
|
+
return stripeRequest('paymentIntents.retrieve', () =>
|
|
102
|
+
this.stripe.paymentIntents.retrieve(paymentId, params, options),
|
|
114
103
|
)
|
|
115
104
|
}
|
|
116
|
-
|
|
117
|
-
private async request<T>(operation: string, work: () => Promise<T>): Promise<T> {
|
|
118
|
-
try {
|
|
119
|
-
return await work()
|
|
120
|
-
} catch (cause) {
|
|
121
|
-
if (
|
|
122
|
-
!(cause instanceof Stripe.errors.StripeError) ||
|
|
123
|
-
cause instanceof Stripe.errors.StripeCardError
|
|
124
|
-
)
|
|
125
|
-
throw cause
|
|
126
|
-
throw new PaymentServiceError(operation, cause)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
105
|
}
|