From 6f09d0788f60da3ec76ad1eb5639142ee8c7ef02 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 21:15:38 +0000 Subject: [PATCH 1/4] Delete the sumupApi and squareApi alias wrapper exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The eleven one-line wrappers (four on sumup.ts, seven on square.ts) only renamed members of the stubbable api objects. Every caller now uses the shared mechanism itself — sumupApi.X(...) / squareApi.X(...) — which late-binds through test stubs exactly like the wrappers did. The two functions captured at module load (the SumUp checkout builder handed to makeCreateCheckoutSession, and each settings page's testFn) become lambdas over the api member so stubbing keeps working. Migrated callers: sumup-provider, square-provider, settings-sumup, settings-square, and the seven direct test importers. Stripe already had no wrapper layer. Closes the TODO entry recorded for this sweep, and marks M3 complete in the plan now that #2060 is merged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AqLNrpvmNdGSSqUtSafiMd --- PLAN.md | 14 ++++----- TODO.md | 20 ------------- src/features/admin/settings-square.ts | 6 ++-- src/features/admin/settings-sumup.ts | 6 ++-- src/shared/square-provider.ts | 18 ++++-------- src/shared/square.ts | 10 ------- src/shared/sumup-provider.ts | 17 ++++++----- src/shared/sumup.ts | 8 ------ test/shared/square/client.test.ts | 30 +++++++++---------- test/shared/square/refund-transport.test.ts | 12 ++++---- test/shared/square/rest-transport.test.ts | 32 ++++++++++----------- test/shared/square/retrieve-refund.test.ts | 4 +-- test/shared/sumup/connection.test.ts | 14 ++++----- test/shared/sumup/create-checkout.test.ts | 22 +++++++++----- test/shared/sumup/transactions.test.ts | 16 +++++------ test/test-utils/square/harness.ts | 6 ++-- 16 files changed, 99 insertions(+), 136 deletions(-) diff --git a/PLAN.md b/PLAN.md index 54e9154feb..c9976fe1df 100644 --- a/PLAN.md +++ b/PLAN.md @@ -25,13 +25,13 @@ ledger. ## Where we are -| Milestone | Status | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| M1 safety behavior (was PR 1) | Merged as #2020. Also landed the M2 pure modules: `src/shared/payment/money.ts`, `resource-id.ts`, `refund-state.ts`, and `validated-session.ts`. | -| M2 money/resource vocabulary (was PR 2) | Core modules merged inside #2020. Any provider parsing still off those schemas rides with M3 or M4. | -| M3 provider ownership (was PR 3) | In flight. Merged slices so far: #2048 (payment processing core), #2050 (bounded registration delivery). The observation boundary + SumUp callback wiring slice is in review. | -| M11 verifier slice (was PR 13) | Started early in #2056 — the verifier is read-only and parallelizable. | -| M4–M10 and M12–M13 | Not started. | +| Milestone | Status | +| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| M1 safety behavior (was PR 1) | Merged as #2020. Also landed the M2 pure modules: `src/shared/payment/money.ts`, `resource-id.ts`, `refund-state.ts`, and `validated-session.ts`. | +| M2 money/resource vocabulary (was PR 2) | Core modules merged inside #2020. Any provider parsing still off those schemas rides with M3 or M4. | +| M3 provider ownership (was PR 3) | Complete: #2048 (payment processing core), #2050 (bounded registration delivery), #2060 (observation boundary + SumUp callback wiring; F2 closed). | +| M11 verifier slice (was PR 13) | Started early in #2056 — the verifier is read-only and parallelizable. | +| M4–M10 and M12–M13 | Not started. | Budgets below count `src/` lines only. Observed totals run 4–15x the `src/` figure once tests, stories, and catalog copy are included (#2020: 714 src lines, diff --git a/TODO.md b/TODO.md index 824b87b43b..3012612f5e 100644 --- a/TODO.md +++ b/TODO.md @@ -2150,23 +2150,3 @@ promising shape is to give `mutation:audit-equivalents` a way to attempt a distinguishing input for each entry — or, failing that, an explicit re-audit stamp so an entry has to be re-confirmed after the file it lives in changes shape, instead of resting on a proof nobody has re-read since it was written. - -## Delete the sumupApi and squareApi alias wrapper exports (from PR #2060) - -`src/shared/sumup.ts` still exports `createCheckout`, `refundTransaction`, -`getTransactionStatus`, and `testSumupConnection` as one-line wrappers over the -same names on `sumupApi`, and `src/shared/square.ts` has the same pattern -(`getSquareClient`, `resetSquareClient`, `testSquareConnection`, -`createPaymentLink` and siblings around `square.ts:850`). AGENTS.md's "No alias -exports" rule says to expose the shared mechanism itself: callers should import -the api object and call `sumupApi.createCheckout(...)` directly, which -late-binds through test stubs exactly like the wrappers do. PR #2060 deleted the -one wrapper it had added (`readCheckoutById`) and migrated its callers to -`sumupApi.readCheckoutById(...)`; the pre-existing wrappers were left alone -because the sweep spans Square too and belongs in one dedicated pass. Starting -point: `grep -n "^export const" src/shared/sumup.ts src/shared/square.ts`, then -migrate `src/shared/sumup-provider.ts`, `src/shared/square-provider.ts`, -`src/features/admin/settings-sumup.ts`, and the direct test importers -(`test/shared/sumup/*.test.ts`). The only subtlety: a function handed away at -module load (like `makeCreateCheckoutSession("SumUp", createCheckout, ...)`) -must become a lambda over the api member so test stubbing keeps working. diff --git a/src/features/admin/settings-square.ts b/src/features/admin/settings-square.ts index a7d837caa5..26c3472919 100644 --- a/src/features/admin/settings-square.ts +++ b/src/features/admin/settings-square.ts @@ -12,7 +12,7 @@ import { } from "#routes/admin/settings-helpers.ts"; import { settings } from "#shared/db/settings.ts"; import { isDemoMode } from "#shared/demo/mode.ts"; -import { testSquareConnection } from "#shared/square.ts"; +import { squareApi } from "#shared/square.ts"; /* jscpd:ignore-end */ import { validateSquareAccessToken, @@ -42,7 +42,9 @@ export const squareRoutes = defineProviderCredentialsRoute({ secretField: "square_access_token", secretRequiredError: t("error.square_token_required"), successMessage: "Square credentials updated", - testFn: testSquareConnection, + // A lambda, not the member itself: the config is built once at module + // load, and resolving the member per call keeps test stubs live. + testFn: () => squareApi.testSquareConnection(), validate: ({ locationId }, secret) => { if (isDemoMode()) return t("error.square_demo_mode"); if (!locationId) return t("error.square_location_required"); diff --git a/src/features/admin/settings-sumup.ts b/src/features/admin/settings-sumup.ts index 64eae87859..72266a45bd 100644 --- a/src/features/admin/settings-sumup.ts +++ b/src/features/admin/settings-sumup.ts @@ -8,7 +8,7 @@ import { defineProviderCredentialsRoute } from "#routes/admin/settings-helpers.t import { settings } from "#shared/db/settings.ts"; import { isDemoMode } from "#shared/demo/mode.ts"; import { providerCurrencyBlock } from "#shared/payment-providers.ts"; -import { testSumupConnection } from "#shared/sumup.ts"; +import { sumupApi } from "#shared/sumup.ts"; /* jscpd:ignore-end */ @@ -30,7 +30,9 @@ export const sumupRoutes = defineProviderCredentialsRoute({ secretField: "sumup_api_key", secretRequiredError: "SumUp API Key is required", successMessage: "SumUp credentials updated", - testFn: testSumupConnection, + // A lambda, not the member itself: the config is built once at module + // load, and resolving the member per call keeps test stubs live. + testFn: () => sumupApi.testSumupConnection(), validate: ({ merchantCode }) => { if (isDemoMode()) return "Cannot configure SumUp in demo mode"; const currencyBlock = providerCurrencyBlock("sumup", settings.currency); diff --git a/src/shared/square-provider.ts b/src/shared/square-provider.ts index 0511ff95f5..c7e6545279 100644 --- a/src/shared/square-provider.ts +++ b/src/shared/square-provider.ts @@ -28,13 +28,7 @@ import type { WebhookSessionResult, WebhookSetupResult, } from "#shared/payments.ts"; -import { - createPaymentLink, - refundPayment, - retrieveOrder, - retrievePayment, - verifyWebhookSignature, -} from "#shared/square.ts"; +import { squareApi, verifyWebhookSignature } from "#shared/square.ts"; /** Square payment provider implementation */ export const squarePaymentProvider: PaymentProvider = { @@ -42,13 +36,13 @@ export const squarePaymentProvider: PaymentProvider = { createCheckoutSession(intent: CheckoutIntent, baseUrl: string) { return withCheckoutError(async () => { - const link = await createPaymentLink(intent, baseUrl); + const link = await squareApi.createPaymentLink(intent, baseUrl); return toCheckoutResult(link?.orderId, link?.url, "Square"); }); }, async isPaymentRefunded(paymentReference: string): Promise { - const payment = await retrievePayment(paymentReference); + const payment = await squareApi.retrievePayment(paymentReference); if (!payment) return false; // Fully refunded only: a partial refund leaves the customer still charged, // so it must not count as refunded (matches Stripe's charge.refunded and @@ -59,7 +53,7 @@ export const squarePaymentProvider: PaymentProvider = { }, refundPayment(paymentReference: string): Promise { - return refundPayment(paymentReference); + return squareApi.refundPayment(paymentReference); }, requiresWebhookSignature: true, @@ -112,7 +106,7 @@ export const squarePaymentProvider: PaymentProvider = { ): Promise { /* jscpd:ignore-end */ // sessionId is the Square order ID - const order = await retrieveOrder(sessionId); + const order = await squareApi.retrieveOrder(sessionId); if (!order?.id) { logDebug("Square", `Order ${sessionId} not found`); return null; @@ -130,7 +124,7 @@ export const squarePaymentProvider: PaymentProvider = { const paymentReference = paidPaymentId ?? order.tenders?.[0]?.paymentId ?? ""; const payment = paymentReference - ? await retrievePayment(paymentReference) + ? await squareApi.retrievePayment(paymentReference) : null; // The webhook already saw this payment complete, so a read-back that is // missing or still short of COMPLETED is Square lagging its own signed diff --git a/src/shared/square.ts b/src/shared/square.ts index 64522ba4d2..bbe323b522 100644 --- a/src/shared/square.ts +++ b/src/shared/square.ts @@ -846,16 +846,6 @@ export const squareApi: { }, }; -// Wrapper exports for production code (delegate to squareApi for test mocking) -export const getSquareClient = () => squareApi.getSquareClient(); -export const resetSquareClient = () => squareApi.resetSquareClient(); -export const testSquareConnection = () => squareApi.testSquareConnection(); -export const createPaymentLink = (i: CheckoutIntent, b: string) => - squareApi.createPaymentLink(i, b); -export const retrieveOrder = (id: string) => squareApi.retrieveOrder(id); -export const retrievePayment = (id: string) => squareApi.retrievePayment(id); -export const refundPayment = (id: string) => squareApi.refundPayment(id); - /** Result of testing the Square connection */ export type SquareConnectionTestResult = { ok: boolean; diff --git a/src/shared/sumup-provider.ts b/src/shared/sumup-provider.ts index 0830c11980..eaa4cb7aad 100644 --- a/src/shared/sumup-provider.ts +++ b/src/shared/sumup-provider.ts @@ -38,12 +38,7 @@ import type { WebhookSetupResult, WebhookVerifyResult, } from "#shared/payments.ts"; -import { - createCheckout, - getTransactionStatus, - refundTransaction, - sumupApi, -} from "#shared/sumup.ts"; +import { sumupApi } from "#shared/sumup.ts"; import type { SumupCheckout, SumupCheckoutStatus, @@ -94,7 +89,9 @@ const buildValidatedSession = ( /** SumUp's checkout-session builder (see {@link makeCreateCheckoutSession}). */ const createSumupCheckoutSession = makeCreateCheckoutSession( "SumUp", - createCheckout, + // A lambda, not the member itself: the checkout builder is captured once + // at module load, and resolving the member per call keeps test stubs live. + (intent, baseUrl) => sumupApi.createCheckout(intent, baseUrl), (result) => ({ id: result?.reference, url: result?.url }), ); @@ -104,11 +101,13 @@ export const sumupPaymentProvider: PaymentProvider = { createCheckoutSession: createSumupCheckoutSession, async isPaymentRefunded(paymentReference: string): Promise { - return (await getTransactionStatus(paymentReference)) === "REFUNDED"; + return ( + (await sumupApi.getTransactionStatus(paymentReference)) === "REFUNDED" + ); }, refundPayment(paymentReference: string): Promise { - return refundTransaction(paymentReference); + return sumupApi.refundTransaction(paymentReference); }, requiresWebhookSignature: false, diff --git a/src/shared/sumup.ts b/src/shared/sumup.ts index 3252281069..23d1b642df 100644 --- a/src/shared/sumup.ts +++ b/src/shared/sumup.ts @@ -266,11 +266,3 @@ export const sumupApi: { return result; }, }; - -// Wrapper exports for production code (delegate to sumupApi for test mocking) -export const createCheckout = (i: CheckoutIntent, b: string) => - sumupApi.createCheckout(i, b); -export const refundTransaction = (id: string) => sumupApi.refundTransaction(id); -export const getTransactionStatus = (id: string) => - sumupApi.getTransactionStatus(id); -export const testSumupConnection = () => sumupApi.testSumupConnection(); diff --git a/test/shared/square/client.test.ts b/test/shared/square/client.test.ts index aa3acf0451..2c788dbf16 100644 --- a/test/shared/square/client.test.ts +++ b/test/shared/square/client.test.ts @@ -1,11 +1,7 @@ import { expect } from "@std/expect"; import { describe, it as test } from "@std/testing/bdd"; import { settings } from "#shared/db/settings.ts"; -import { - getSquareClient, - resetSquareClient, - testSquareConnection, -} from "#shared/square.ts"; +import { squareApi } from "#shared/square.ts"; import { configureSquare, oneLocation, @@ -37,7 +33,7 @@ describeSquare(() => { }; test("returns null when access token not set", async () => { - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); expect(client).toBeNull(); expect(debugMessages(debugLog())).toEqual([ "[Square] No access token configured, cannot create client", @@ -46,7 +42,7 @@ describeSquare(() => { test("returns client when access token is set in database", async () => { await settings.update.square.accessToken("EAAAl_test_123"); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); expect(client).not.toBeNull(); expect(debugLog().calls.at(-1)?.args[0]).toBe( "[Square] Creating new Square client (production)", @@ -55,18 +51,18 @@ describeSquare(() => { test("returns cached client on second call with same token", async () => { await settings.update.square.accessToken("EAAAl_cache_test"); - const client1 = await getSquareClient(); + const client1 = await squareApi.getSquareClient(); expect(client1).not.toBeNull(); // Second call with same token returns the very same cached instance. - const client2 = await getSquareClient(); + const client2 = await squareApi.getSquareClient(); expect(client2).toBe(client1); }); test("returns client in sandbox mode when sandbox setting enabled", async () => { await settings.update.square.accessToken("EAAAl_sandbox_123"); await settings.update.square.sandbox(true); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); expect(client).not.toBeNull(); // Sandbox mode must route requests to the sandbox host. using _fetch = trackFetch(); @@ -79,14 +75,14 @@ describeSquare(() => { test("recreates client when sandbox setting changes", async () => { await settings.update.square.accessToken("EAAAl_sandbox_toggle"); await settings.update.square.sandbox(false); - const client1 = await getSquareClient(); + const client1 = await squareApi.getSquareClient(); expect(client1).not.toBeNull(); using _fetch = trackFetch(); expect(await hostFor(client1!)).toBe("connect.squareup.com"); // Toggling sandbox creates a new client configured for the sandbox host. await settings.update.square.sandbox(true); - const client2 = await getSquareClient(); + const client2 = await squareApi.getSquareClient(); expect(client2).not.toBe(client1); expect(await hostFor(client2!)).toBe("connect.squareupsandbox.com"); }); @@ -95,14 +91,14 @@ describeSquare(() => { describe("resetSquareClient", () => { test("resets client state after token removed from db", async () => { await settings.update.square.accessToken("EAAAl_test_123"); - const client1 = await getSquareClient(); + const client1 = await squareApi.getSquareClient(); expect(client1).not.toBeNull(); - resetSquareClient(); + squareApi.resetSquareClient(); resetDb(); await createTestDb(); - const client2 = await getSquareClient(); + const client2 = await squareApi.getSquareClient(); expect(client2).toBeNull(); }); }); @@ -157,12 +153,12 @@ describeSquare(() => { ) => { await configureSquare(config); await withSquareClient({ locationsList }, async () => { - assert(await testSquareConnection()); + assert(await squareApi.testSquareConnection()); }); }; test("returns error when no access token configured", async () => { - expect(await testSquareConnection()).toEqual({ + expect(await squareApi.testSquareConnection()).toEqual({ accessToken: { error: "No Square access token configured", valid: false, diff --git a/test/shared/square/refund-transport.test.ts b/test/shared/square/refund-transport.test.ts index 2b0ae719f4..bd790b52c1 100644 --- a/test/shared/square/refund-transport.test.ts +++ b/test/shared/square/refund-transport.test.ts @@ -2,7 +2,7 @@ import { expect } from "@std/expect"; import { afterEach, beforeEach, describe, it as test } from "@std/testing/bdd"; import { settings } from "#shared/db/settings.ts"; -import { getSquareClient, resetSquareClient } from "#shared/square.ts"; +import { squareApi } from "#shared/square.ts"; import { type FetchCall, installMockFetch, @@ -34,7 +34,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); await client!.refunds.refundPayment({ amountMoney: { amount: BigInt(3000), currency: "GBP" }, idempotencyKey: "idem-ref", @@ -61,7 +61,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); // The client returns raw JSON — squareApi.refundPayment validates it // with a Valibot schema at the boundary. const result = (await client!.refunds.refundPayment({ @@ -77,7 +77,7 @@ describeSquare(() => { test("returns the raw response even when no refund is present", async () => { mockFetch = installMockFetch(() => Promise.resolve(jsonResponse({}))); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); // The client returns the raw response — it does NOT normalize it. // The squareApi layer's Valibot parse would throw on this (refund is // required), but the transport client itself just passes it through. @@ -91,7 +91,7 @@ describeSquare(() => { }); test("uses production URL when sandbox is disabled", async () => { - resetSquareClient(); + squareApi.resetSquareClient(); await settings.update.square.sandbox(false); mockFetch = installMockFetch(() => Promise.resolve( @@ -99,7 +99,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); await client!.refunds.refundPayment({ amountMoney: { amount: BigInt(500), currency: "GBP" }, idempotencyKey: "idem-prod", diff --git a/test/shared/square/rest-transport.test.ts b/test/shared/square/rest-transport.test.ts index 5f6defc276..1c7d26250b 100644 --- a/test/shared/square/rest-transport.test.ts +++ b/test/shared/square/rest-transport.test.ts @@ -1,7 +1,7 @@ import { expect } from "@std/expect"; import { afterEach, beforeEach, describe, it as test } from "@std/testing/bdd"; import { settings } from "#shared/db/settings.ts"; -import { getSquareClient, resetSquareClient } from "#shared/square.ts"; +import { squareApi } from "#shared/square.ts"; import { type FetchCall, installMockFetch, @@ -37,7 +37,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.checkout.paymentLinks.create({ checkoutOptions: { redirectUrl: "https://example.com/success" }, idempotencyKey: "idem-rest", @@ -98,7 +98,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.checkout.paymentLinks.create({ checkoutOptions: { redirectUrl: "https://example.com" }, idempotencyKey: "idem-short", @@ -130,7 +130,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); await client!.checkout.paymentLinks.create({ checkoutOptions: { redirectUrl: "https://example.com" }, idempotencyKey: "idem-2", @@ -156,7 +156,7 @@ describeSquare(() => { test("returns undefined paymentLink when API returns no payment_link", async () => { mockFetch = installMockFetch(() => Promise.resolve(jsonResponse({}))); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.checkout.paymentLinks.create({ checkoutOptions: { redirectUrl: "https://example.com" }, idempotencyKey: "idem-3", @@ -185,7 +185,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.orders.get({ orderId: "ord_100" }); expect(mockFetch.calls[0]!.args[0]).toBe( @@ -209,7 +209,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.orders.get({ orderId: "ord_no_total" }); expect(result.order!.id).toBe("ord_no_total"); expect(result.order!.totalMoney).toBeUndefined(); @@ -218,7 +218,7 @@ describeSquare(() => { test("orders.get returns null order when API returns none", async () => { mockFetch = installMockFetch(() => Promise.resolve(jsonResponse({}))); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.orders.get({ orderId: "missing" }); expect(result.order).toBeNull(); }); @@ -238,7 +238,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.payments.get({ paymentId: "pay_1" }); expect(mockFetch.calls[0]!.args[0]).toBe( @@ -263,7 +263,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.payments.get({ paymentId: "pay_no_amount" }); expect(result.payment!.id).toBe("pay_no_amount"); expect(result.payment!.amountMoney).toBeUndefined(); @@ -283,7 +283,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.payments.get({ paymentId: "pay_2" }); expect(result.payment!.amountMoney!.amount).toBe(BigInt(2000)); expect(result.payment!.refundedMoney).toBeUndefined(); @@ -292,7 +292,7 @@ describeSquare(() => { test("payments.get returns null payment when API returns none", async () => { mockFetch = installMockFetch(() => Promise.resolve(jsonResponse({}))); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.payments.get({ paymentId: "missing" }); expect(result.payment).toBeNull(); }); @@ -306,7 +306,7 @@ describeSquare(() => { }), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); let err: Error | undefined; try { await client!.orders.get({ orderId: "bad" }); @@ -331,7 +331,7 @@ describeSquare(() => { ), ); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); const result = await client!.locations.list(); expect(mockFetch.calls[0]!.args[0]).toBe( @@ -344,11 +344,11 @@ describeSquare(() => { }); test("uses production URL when sandbox is disabled", async () => { - resetSquareClient(); + squareApi.resetSquareClient(); await settings.update.square.sandbox(false); mockFetch = installMockFetch(() => Promise.resolve(jsonResponse({}))); - const client = await getSquareClient(); + const client = await squareApi.getSquareClient(); await client!.orders.get({ orderId: "test" }); expect(mockFetch.calls[0]!.args[0]).toContain("connect.squareup.com"); diff --git a/test/shared/square/retrieve-refund.test.ts b/test/shared/square/retrieve-refund.test.ts index 19aacbf5b1..96426838d8 100644 --- a/test/shared/square/retrieve-refund.test.ts +++ b/test/shared/square/retrieve-refund.test.ts @@ -1,6 +1,6 @@ import { expect } from "@std/expect"; import { describe, it as test } from "@std/testing/bdd"; -import { retrievePayment, squareApi } from "#shared/square.ts"; +import { squareApi } from "#shared/square.ts"; import { withSquareClient } from "#test/test-utils/square/fixtures.ts"; import { describeSquare } from "#test/test-utils/square/harness.ts"; @@ -301,7 +301,7 @@ describeSquare(() => { }), }, async ({ paymentsGet }) => { - const result = await retrievePayment("pay_wrapper"); + const result = await squareApi.retrievePayment("pay_wrapper"); expect(result).not.toBeNull(); expect(result!.id).toBe("pay_wrapper"); expect(result!.status).toBe("COMPLETED"); diff --git a/test/shared/sumup/connection.test.ts b/test/shared/sumup/connection.test.ts index 54361e9da9..428622014f 100644 --- a/test/shared/sumup/connection.test.ts +++ b/test/shared/sumup/connection.test.ts @@ -1,7 +1,7 @@ import { expect } from "@std/expect"; import { describe, it as test } from "@std/testing/bdd"; import { settings } from "#shared/db/settings.ts"; -import { testSumupConnection } from "#shared/sumup.ts"; +import { sumupApi } from "#shared/sumup.ts"; import { makeSumupClient, setupSumupSuite, @@ -19,7 +19,7 @@ describe("sumup testSumupConnection", () => { merchantGet: () => Promise.reject(new Error(errorMessage)), }); await withSumupClient(client, async () => { - const result = await testSumupConnection(); + const result = await sumupApi.testSumupConnection(); expect(result.ok).toBe(false); expect(result.apiKey.valid).toBe(false); assertError(result.apiKey.error); @@ -28,7 +28,7 @@ describe("sumup testSumupConnection", () => { test("reports a missing API key", async () => { settings.setForTest({ sumup_api_key: "" }); - const result = await testSumupConnection(); + const result = await sumupApi.testSumupConnection(); expect(result.ok).toBe(false); expect(result.apiKey).toEqual({ error: "No SumUp API key configured", @@ -39,7 +39,7 @@ describe("sumup testSumupConnection", () => { test("reports a missing merchant code", async () => { settings.setForTest({ sumup_merchant_code: "" }); - const result = await testSumupConnection(); + const result = await sumupApi.testSumupConnection(); expect(result.ok).toBe(false); expect(result.apiKey).toEqual({ error: "Merchant code is required to verify the key", @@ -60,7 +60,7 @@ describe("sumup testSumupConnection", () => { test("reports success with key mode, merchant, and currency", () => withMerchantClient(async () => { - const result = await testSumupConnection(); + const result = await sumupApi.testSumupConnection(); expect(result.ok).toBe(true); expect(result.apiKey).toEqual({ mode: "test", valid: true }); expect(result.merchant).toEqual({ @@ -73,7 +73,7 @@ describe("sumup testSumupConnection", () => { test("fails overall when the site currency is unsupported", async () => { settings.setForTest({ currency: "AUD" }); await withMerchantClient(async () => { - const result = await testSumupConnection(); + const result = await sumupApi.testSumupConnection(); expect(result.ok).toBe(false); expect(result.apiKey.valid).toBe(true); expect(result.currency).toEqual({ code: "AUD", supported: false }); @@ -83,7 +83,7 @@ describe("sumup testSumupConnection", () => { test("reports the key mode as unknown for an unrecognized key prefix", async () => { settings.setForTest({ sumup_api_key: "plainkey" }); await withMerchantClient(async () => { - const result = await testSumupConnection(); + const result = await sumupApi.testSumupConnection(); expect(result.apiKey.mode).toBe("unknown"); }); }); diff --git a/test/shared/sumup/create-checkout.test.ts b/test/shared/sumup/create-checkout.test.ts index 6c4cebe3e3..9d5829bd5f 100644 --- a/test/shared/sumup/create-checkout.test.ts +++ b/test/shared/sumup/create-checkout.test.ts @@ -2,7 +2,7 @@ import { expect } from "@std/expect"; import { describe, it as test } from "@std/testing/bdd"; import { settings } from "#shared/db/settings.ts"; import { getSumupCheckout } from "#shared/db/sumup-checkouts.ts"; -import { createCheckout } from "#shared/sumup.ts"; +import { sumupApi } from "#shared/sumup.ts"; import { makeSumupClient, setupSumupSuite, @@ -28,7 +28,9 @@ describe("sumup createCheckout", () => { settings.setForTest({ sumup_merchant_code: "" }); const client = makeSumupClient({ create: () => Promise.resolve({}) }); await withSumupClient(client, async () => { - expect(await createCheckout(intent, "http://localhost")).toBeNull(); + expect( + await sumupApi.createCheckout(intent, "http://localhost"), + ).toBeNull(); }); }); @@ -46,7 +48,7 @@ describe("sumup createCheckout", () => { }, }); await withSumupClient(client, async () => { - const result = await createCheckout(intent, "http://localhost"); + const result = await sumupApi.createCheckout(intent, "http://localhost"); expect(result).not.toBeNull(); expect(result!.url).toBe("https://pay.sumup.com/x"); // 2 tickets * 1000 minor units = 2000 minor => 20 major units @@ -80,7 +82,7 @@ describe("sumup createCheckout", () => { }, }); await withSumupClient(client, async () => { - await createCheckout(intent, "http://localhost"); + await sumupApi.createCheckout(intent, "http://localhost"); expect(sentBody.amount).toBe(2000); expect(sentBody.currency).toBe("CLP"); }); @@ -92,7 +94,9 @@ describe("sumup createCheckout", () => { Promise.resolve({ hosted_checkout_url: "https://pay.sumup.com/z" }), }); await withSumupClient(client, async () => { - expect(await createCheckout(intent, "http://localhost")).toBeNull(); + expect( + await sumupApi.createCheckout(intent, "http://localhost"), + ).toBeNull(); }); expect( loggedDebug("Checkout response missing id or hosted_checkout_url"), @@ -104,13 +108,17 @@ describe("sumup createCheckout", () => { create: () => Promise.resolve({ id: "co_no_url" }), }); await withSumupClient(client, async () => { - expect(await createCheckout(intent, "http://localhost")).toBeNull(); + expect( + await sumupApi.createCheckout(intent, "http://localhost"), + ).toBeNull(); }); }); test("returns null when the client is unavailable", async () => { await withSumupClient(null, async () => { - expect(await createCheckout(intent, "http://localhost")).toBeNull(); + expect( + await sumupApi.createCheckout(intent, "http://localhost"), + ).toBeNull(); }); }); }); diff --git a/test/shared/sumup/transactions.test.ts b/test/shared/sumup/transactions.test.ts index fff581b4f9..e506a137aa 100644 --- a/test/shared/sumup/transactions.test.ts +++ b/test/shared/sumup/transactions.test.ts @@ -1,7 +1,7 @@ import { expect } from "@std/expect"; import { describe, it as test } from "@std/testing/bdd"; import { settings } from "#shared/db/settings.ts"; -import { getTransactionStatus, refundTransaction } from "#shared/sumup.ts"; +import { sumupApi } from "#shared/sumup.ts"; import { makeSumupClient, setupSumupSuite, @@ -15,7 +15,7 @@ describe("sumup transactions", () => { test("returns null when merchant code is absent", async () => { settings.setForTest({ sumup_merchant_code: "" }); await withSumupClient(makeSumupClient({}), async () => { - expect(await getTransactionStatus("txn")).toBeNull(); + expect(await sumupApi.getTransactionStatus("txn")).toBeNull(); }); }); @@ -24,7 +24,7 @@ describe("sumup transactions", () => { txnGet: () => Promise.resolve({ status: "SUCCESSFUL" }), }); await withSumupClient(client, async () => { - expect(await getTransactionStatus("txn")).toBe("SUCCESSFUL"); + expect(await sumupApi.getTransactionStatus("txn")).toBe("SUCCESSFUL"); }); }); @@ -33,14 +33,14 @@ describe("sumup transactions", () => { txnGet: () => Promise.resolve({ status: "" }), }); await withSumupClient(client, async () => { - expect(await getTransactionStatus("txn")).toBe(""); + expect(await sumupApi.getTransactionStatus("txn")).toBe(""); }); }); test("returns null when the status field is absent", async () => { const client = makeSumupClient({ txnGet: () => Promise.resolve({}) }); await withSumupClient(client, async () => { - expect(await getTransactionStatus("txn")).toBeNull(); + expect(await sumupApi.getTransactionStatus("txn")).toBeNull(); }); }); }); @@ -49,7 +49,7 @@ describe("sumup transactions", () => { test("returns false and reports the missing merchant code", async () => { settings.setForTest({ sumup_merchant_code: "" }); await withSumupClient(makeSumupClient({}), async () => { - expect(await refundTransaction("txn")).toBe(false); + expect(await sumupApi.refundTransaction("txn")).toBe(false); }); expect(errorSpy.contains("SumUp merchant code")).toBe(true); }); @@ -63,14 +63,14 @@ describe("sumup transactions", () => { }, }); await withSumupClient(client, async () => { - expect(await refundTransaction("txn_r")).toBe(true); + expect(await sumupApi.refundTransaction("txn_r")).toBe(true); expect(calls[0]).toEqual(["MC123", "txn_r"]); }); }); test("returns false when the client is unavailable", async () => { await withSumupClient(null, async () => { - expect(await refundTransaction("txn")).toBe(false); + expect(await sumupApi.refundTransaction("txn")).toBe(false); }); }); }); diff --git a/test/test-utils/square/harness.ts b/test/test-utils/square/harness.ts index 41b4d9d7c5..d9427c42f8 100644 --- a/test/test-utils/square/harness.ts +++ b/test/test-utils/square/harness.ts @@ -1,6 +1,6 @@ import { afterAll, afterEach, beforeEach, describe } from "@std/testing/bdd"; import { spy } from "@std/testing/mock"; -import { resetSquareClient, type SquareClient } from "#shared/square.ts"; +import { type SquareClient, squareApi } from "#shared/square.ts"; import { createTestDb, resetDb } from "#test-utils/db.ts"; import { reclaimLeakedFdsNow } from "#test-utils/reclaim-fds.ts"; @@ -48,12 +48,12 @@ export const createMockClient = ( export const describeSquare = (body: () => void): void => { describe("square", () => { beforeEach(async () => { - resetSquareClient(); + squareApi.resetSquareClient(); await createTestDb(); }); afterEach(() => { - resetSquareClient(); + squareApi.resetSquareClient(); resetDb(); }); From 90391606cca39d293bfadd486495d7ed65db42a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 21:27:24 +0000 Subject: [PATCH 2/4] Fix the two type-position leftovers, drop the dead delegation test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep's call-site migration missed two references in type position (typeof getSquareClient / typeof testSquareConnection in the client suite), which failed CI's typecheck; they now name the api members. The "retrievePayment wrapper export delegates" test asserted the very hop the sweep deleted, so it duplicated the direct coverage above it — removed, as the dead-code rule requires for a deleted export's test. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AqLNrpvmNdGSSqUtSafiMd --- test/shared/square/client.test.ts | 8 +++++-- test/shared/square/retrieve-refund.test.ts | 27 ---------------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/test/shared/square/client.test.ts b/test/shared/square/client.test.ts index 2c788dbf16..62969f2529 100644 --- a/test/shared/square/client.test.ts +++ b/test/shared/square/client.test.ts @@ -26,7 +26,9 @@ describeSquare(() => { /** Drive one request through the client and return the host it called. */ const hostFor = async ( - client: NonNullable>>, + client: NonNullable< + Awaited> + >, ): Promise => { await client.locations.list(); return new URL(calledUrl).host; @@ -104,7 +106,9 @@ describeSquare(() => { }); describe("testSquareConnection", () => { - type ConnectionResult = Awaited>; + type ConnectionResult = Awaited< + ReturnType + >; /** Run an assertion only when the test named a value for it. */ const when = (value: T | undefined, assert: (value: T) => void) => { diff --git a/test/shared/square/retrieve-refund.test.ts b/test/shared/square/retrieve-refund.test.ts index 96426838d8..74d5f90200 100644 --- a/test/shared/square/retrieve-refund.test.ts +++ b/test/shared/square/retrieve-refund.test.ts @@ -285,31 +285,4 @@ describeSquare(() => { ); }); }); - - describe("retrievePayment wrapper export", () => { - test("delegates to squareApi.retrievePayment", async () => { - await withSquareClient( - { - paymentsGet: () => - Promise.resolve({ - payment: { - amountMoney: { amount: BigInt(1000), currency: "GBP" }, - id: "pay_wrapper", - orderId: "order_wrapper", - status: "COMPLETED", - }, - }), - }, - async ({ paymentsGet }) => { - const result = await squareApi.retrievePayment("pay_wrapper"); - expect(result).not.toBeNull(); - expect(result!.id).toBe("pay_wrapper"); - expect(result!.status).toBe("COMPLETED"); - expect(paymentsGet.calls[0]!.args[0]).toEqual({ - paymentId: "pay_wrapper", - }); - }, - ); - }); - }); }); From 366a4992d3b6625eaa779681333af7f7cac46a04 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 21:32:12 +0000 Subject: [PATCH 3/4] Drop the stale resetSquareClient test-hook exemption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrapper sweep deleted the top-level resetSquareClient export, so the ALLOWED_TEST_HOOKS entry for it no longer matches anything — tests reach the member through squareApi, which the export scan already credits. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AqLNrpvmNdGSSqUtSafiMd --- test/integration/code-quality.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/code-quality.test.ts b/test/integration/code-quality.test.ts index 3fcbc50d09..5b44c83e17 100644 --- a/test/integration/code-quality.test.ts +++ b/test/integration/code-quality.test.ts @@ -207,8 +207,6 @@ const ALLOWED_TEST_HOOKS: string[] = [ "shared/limits.ts:PRUNE_CONTACTS_RETENTION_DAYS", "shared/limits.ts:ADDRESS_CACHE_DAYS", "shared/limits.ts:PRUNE_INTERVAL_HOURS", - // Reset cached Square client between tests - "shared/square.ts:resetSquareClient", // Test helper for creating signed Square webhook payloads "shared/square.ts:constructTestWebhookEvent", // Raw attendee fetch for testing encrypted data (production uses batched getListingWithAttendeesRaw) From 855c65f04e8961e62f334c5798f2fbe98e111735 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 21:45:52 +0000 Subject: [PATCH 4/4] Remove the equivalent-mutant entry a test now kills The square mutation run reports the testSquareConnection.locations ?? -> || entry as redundant: a test kills that mutant, so the registry entry no longer describes a real survivor. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AqLNrpvmNdGSSqUtSafiMd --- scripts/mutation/equivalent-mutants/features.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/mutation/equivalent-mutants/features.txt b/scripts/mutation/equivalent-mutants/features.txt index f738b60471..c5dafe395f 100644 --- a/scripts/mutation/equivalent-mutants/features.txt +++ b/scripts/mutation/equivalent-mutants/features.txt @@ -117,7 +117,6 @@ src/shared/runtime.ts::buildRuntimeInfo.os~05o6squ ?? → || # os is string|u src/shared/runtime.ts::buildRuntimeInfo.typescriptVersion~14e4wx7 ?? → || # typescriptVersion is string|undefined and falls back to "", so both operators agree src/shared/runtime.ts::buildRuntimeInfo.userAgent~17udaa2 ?? → || # userAgent is string|undefined and falls back to "", so both operators agree src/shared/square.ts::createSquareClient.checkout.paymentLinks.create.data.pre_populated_data~0evahy5 ?: → consequent only # the buyer_phone_number ternary spreads {} when the phone is absent; the consequent-only mutant always spreads { buyer_phone_number: undefined }, but JSON.stringify omits undefined values, so the serialized request body is identical in both cases -src/shared/square.ts::squareApi.testSquareConnection.locations~10z1q7q ?? → || # locations is an array or undefined; arrays are truthy and undefined takes [] under both operators src/ui/client/dom.ts::createButton.button%2eclassName~16pvzvs = → += # createElement returns a new button with className "", so both assignments produce the supplied class string # Paid-payment processing: private defaults are always supplied by their only