first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# @suaveplan/payment-provider-square - Universal Square Payments API adapter for @suaveplan/payment. Ships SquareProvider (implements PaymentProvider with literal name "square"), SquareConfig (extends ApiKeyConfig with locationId, sandbox, apiVersion, randomId), and the Square-specific payment/refund mappers. Built on the @suaveplan/payment-providers foundation (httpRequest, composeAbortSignal, PAYMENT_PROVIDER_CODES, PaymentProviderError).
Universal Square Payments API adapter for `@suaveplan/payment`.
Ships the `SquareProvider` class (implements the structural
`PaymentProvider` contract with literal `name: "square"`), the
`SquareConfig` interface (extends `ApiKeyConfig` from
`@suaveplan/payment-providers` with the Square-specific `locationId`,
`sandbox`, `apiVersion`, and `randomId` fields), and the
Square-specific payment / refund mappers (`mapPayment`, `mapRefund`,
`mapPaymentStatus`, `mapRefundStatus`).
Built on the `@suaveplan/payment-providers` foundation — every
outbound HTTP call goes through `httpRequest`, which handles JSON
serialisation, the per-call timeout signal, external `AbortSignal`
composition, and typed `PaymentProviderError` raising.
<!-- begin:auto-docs -->
## Documentation tree
- [payment](./src/mapping/payment.md) — Translates between Square's `Payment` envelope (as returned by `POST /v2/payments`, `POST /v2/payments/{id}/complete`,…
- [refund](./src/mapping/refund.md) — Translates Square's `Refund` envelope (as returned by `POST /v2/refunds`) into `@suaveplan/payment`'s `Refund` shape. T…
- [square](./src/square/square.md) — `SquareProvider` is the Square Payments API adapter for `@suaveplan/payment`. It implements the structural `PaymentProv…
- [square](./src/square/square.md) — `SquareProvider` is the Square Payments API adapter for `@suaveplan/payment`. It implements the structural `PaymentProv…
- [types](./src/types/config.md) — Defines the configuration shape `SquareProvider` accepts and the three default constants the provider falls back to whe…
- [config](./src/types/config.md) — Defines the configuration shape `SquareProvider` accepts and the three default constants the provider falls back to whe…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/payment-provider-square @suaveplan/payment-providers @suaveplan/payment
```
## Quick start
```ts
import { ProviderRegistry } from "@suaveplan/payment-providers";
import { SquareProvider } from "@suaveplan/payment-provider-square";
import { PaymentEngine } from "@suaveplan/payment";
const registry = new ProviderRegistry();
registry.register(
new SquareProvider({
apiKey: process.env.SQUARE_ACCESS_TOKEN ?? "",
locationId: process.env.SQUARE_LOCATION_ID ?? "",
})
);
const engine = new PaymentEngine({ provider: registry.get("square") });
const intent = await engine.createIntent({
amount: { amount: 5000n, currency: "USD" },
});
```
## Features
- **`SquareProvider`** — full Square Payments API lifecycle
(`createIntent`, `confirmIntent`, `captureIntent`, `cancelIntent`,
`refund`, `getIntent`) with literal `name: "square"`.
- **`SquareConfig`** — extends `ApiKeyConfig`; adds required
`locationId` plus optional `sandbox`, `apiVersion`, `randomId`.
- **Sandbox toggle** — `sandbox: true` flips the default base URL to
`https://connect.squareupsandbox.com/v2`. Explicit `baseUrl`
always wins.
- **Pinned `Square-Version`** — defaults to `2024-01-18`; override via
`apiVersion` to opt into a newer API surface.
- **Injectable `randomId`** — fills missing `idempotency_key` fields
on `CreatePayment` + `RefundPayment` calls. Tests pass a
deterministic counter; production defaults to `crypto.randomUUID()`.
- **Universal** — no `node:`, `bun:`, `fs`, `path`, `crypto`
imports; runs on Node, Bun, browsers, and Web Workers.
- **`PaymentProviderError`-only** — every failure surfaces as a
typed `PaymentProviderError` via the foundation's `httpRequest`.
- **100% test coverage** (Istanbul) + sibling `.md` per
implementation file + 5 subpath exports.
## Modules
- [`src/square/square.md`](./src/square/square.md) — `SquareProvider`
class and lifecycle methods.
- [`src/types/config.md`](./src/types/config.md) — `SquareConfig`,
`SQUARE_DEFAULT_BASE_URL`, `SQUARE_SANDBOX_BASE_URL`,
`SQUARE_DEFAULT_API_VERSION`.
- [`src/mapping/payment.md`](./src/mapping/payment.md) — `mapPayment`,
`mapPaymentStatus`, `SquarePaymentDto`, `SquareMoney`, `SquareCard`.
- [`src/mapping/refund.md`](./src/mapping/refund.md) — `mapRefund`,
`mapRefundStatus`, `SquareRefundDto`.
## Related
- `@suaveplan/payment-providers` — `ProviderRegistry`,
`definePaymentProvider`, `httpRequest`, `composeAbortSignal`,
`PAYMENT_PROVIDER_CODES`, `PaymentProviderError`,
`ApiKeyConfig`/`OAuthConfig`/`MerchantKeyConfig`,
`FetchLike`/`RandomIdFn`/`NowFn`/`Base64EncodeFn`.
- `@suaveplan/payment` — `PaymentEngine`, `WebhookVerifier`,
`PaymentProvider` interface, the canonical `PaymentIntent` /
`Refund` / `PaymentStatus` / `RefundStatus` shapes.
## License
MIT

View File

@@ -0,0 +1,126 @@
# Square payment mapping
## Purpose
Translates between Square's `Payment` envelope (as returned by
`POST /v2/payments`, `POST /v2/payments/{id}/complete`,
`POST /v2/payments/{id}/cancel`, and `GET /v2/payments/{id}`) and
`@suaveplan/payment`'s `PaymentIntent` shape. Square's status values
are upper-snake-case (`COMPLETED`, `APPROVED`, ...) while uses
the lower-case `PaymentStatus` union; the mapping pins this contract
in one place so the provider class stays focused on transport.
The mapper also captures the two implicit branches Square encodes via
optional fields rather than dedicated states: `approved_money`
present ↔ manual capture, `card_details` present ↔ card-shaped
`PaymentMethod`.
## Features
- `mapPaymentStatus(squareStatus)` — exhaustive five-status map with a
defensive `"failed"` fallback for forward-rolled API versions.
- `mapPayment(p)` — full envelope-to-`PaymentIntent` shape including
`capturedAmount` derivation (`p.total_money` on `COMPLETED`, `0n`
otherwise), `captureMethod` derivation (`approved_money` ↔
`"manual"`), and optional `PaymentMethod` derivation from
`card_details`.
- `SquareCard` / `SquareMoney` / `SquarePaymentDto` — typed views of
the Square API envelope that downstream code can rely on instead of
threading `unknown` through call sites.
- Card-brand normalisation — Square returns brands upper-case; the
mapper lower-cases and validates against the `CardBrand`
union, falling back to `"unknown"` for non-canonical brands
(Square gift cards, JCB sub-brands, etc.).
## Usage: Basic Example
```ts
import {
mapPayment,
mapPaymentStatus,
} from "@suaveplan/payment-provider-square";
mapPaymentStatus("COMPLETED"); // "captured"
const intent = mapPayment({
id: "sq_pay_123",
status: "COMPLETED",
amount_money: { amount: 5000, currency: "USD" },
total_money: { amount: 5000, currency: "USD" },
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
});
console.log(intent.status); // "captured"
console.log(intent.capturedAmount.amount); // 5000n
```
## Usage: Advanced Example
Map an authorized-but-not-captured payment with card metadata:
```ts
import { mapPayment } from "@suaveplan/payment-provider-square";
const intent = mapPayment({
id: "sq_pay_456",
status: "APPROVED",
amount_money: { amount: 9999, currency: "EUR" },
total_money: { amount: 9999, currency: "EUR" },
approved_money: { amount: 9999, currency: "EUR" },
customer_id: "cust_42",
note: "Pre-authorisation for hold",
card_details: { card: { card_brand: "MASTERCARD", last_4: "4242" } },
created_at: "2026-02-01T10:00:00Z",
updated_at: "2026-02-01T10:00:00Z",
});
console.log(intent.captureMethod); // "manual"
console.log(intent.status); // "authorized"
console.log(intent.capturedAmount.amount); // 0n
console.log(intent.paymentMethod);
// { id: "sq_card_sq_pay_456", type: "card", brand: "mastercard", last4: "4242" }
```
## API Reference
### `mapPaymentStatus(squareStatus: string): PaymentStatus`
Maps Square status → suaveplan status. Known mappings:
| Square | suaveplan |
|---|---|
| `APPROVED` | `"authorized"` |
| `COMPLETED` | `"captured"` |
| `CANCELED` | `"canceled"` |
| `FAILED` | `"failed"` |
| `PENDING` | `"pending"` |
| _other_ | `"failed"` |
### `mapPayment(p: SquarePaymentDto): PaymentIntent`
Shape the envelope. `capturedAmount.amount` is
`BigInt(p.total_money.amount)` when `p.status === "COMPLETED"`,
otherwise `0n`. `captureMethod` is `"manual"` when `p.approved_money`
is set. `paymentMethod` is set when `p.card_details` is present.
`refundedAmount.amount` is `0n` (refund totals are tracked by
`PaymentEngine`, not derived here). `refunds` is an empty array.
### Types
- `SquareMoney` — `{ amount: number; currency: string }`.
- `SquareCard` — `{ card_brand: string; last_4: string }`.
- `SquarePaymentDto` — the subset of Square's `Payment` envelope this
package consumes.
## Implementation Notes
- The status map is frozen at module init so accidental mutation
throws in strict mode.
- The card-brand normaliser is intentionally conservative: any brand
Square introduces that does not match the `CardBrand` union
falls back to `"unknown"` so downstream code never receives an
invalid discriminant.
- `paymentMethod.id` is synthesised as `sq_card_<payment-id>` because
Square does not expose a stable card-fingerprint on the inline
`card_details.card` object. Consumers needing a true stable card id
should hydrate it from Square's Cards API and overlay it.

View File

@@ -0,0 +1,120 @@
# Square refund mapping
## Purpose
Translates Square's `Refund` envelope (as returned by
`POST /v2/refunds`) into `@suaveplan/payment`'s `Refund` shape. The
mapping is small but load-bearing: Square encodes refund lifecycle
states as upper-snake-case strings while the `RefundStatus`
union is lower-case, and Square's `reason` is a free-form string
where the `RefundReason` union is a closed discriminator.
The reason mapping is intentionally narrow — Square never reports
machine-readable reason codes, so any non-empty reason collapses to
`"requested_by_customer"` and an absent / empty reason collapses to
`"other"`. Consumers that need richer reason granularity must layer
their own mapping on top.
## Features
- `mapRefundStatus(squareStatus)` — exhaustive three-state map
(`COMPLETED`, `PENDING`, everything-else) with `"failed"` covering
Square's `REJECTED` and `FAILED` plus any forward-rolled values.
- `mapRefund(r)` — full envelope-to-`Refund` shape including bigint
amount coercion and reason defaulting.
- `SquareRefundDto` — typed view of the Square refund envelope.
## Usage: Basic Example
```ts
import {
mapRefund,
mapRefundStatus,
} from "@suaveplan/payment-provider-square";
mapRefundStatus("COMPLETED"); // "succeeded"
const refund = mapRefund({
id: "sq_ref_123",
payment_id: "sq_pay_123",
amount_money: { amount: 2000, currency: "USD" },
status: "COMPLETED",
created_at: "2026-01-02T00:00:00Z",
reason: "Customer changed mind",
});
console.log(refund.status); // "succeeded"
console.log(refund.reason); // "requested_by_customer"
console.log(refund.amount.amount); // 2000n
```
## Usage: Advanced Example
Show all three branches in turn — including the reason fallback and
the `REJECTED → "failed"` collapse:
```ts
import { mapRefund } from "@suaveplan/payment-provider-square";
const base = {
id: "sq_ref_a",
payment_id: "sq_pay_a",
amount_money: { amount: 500, currency: "GBP" },
created_at: "2026-03-01T00:00:00Z",
};
const pending = mapRefund({ ...base, status: "PENDING" });
console.log(pending.status); // "pending"
console.log(pending.reason); // "other" (no reason supplied)
const rejected = mapRefund({ ...base, status: "REJECTED", reason: "" });
console.log(rejected.status); // "failed"
console.log(rejected.reason); // "other" (empty string)
const succeeded = mapRefund({
...base,
status: "COMPLETED",
reason: "duplicate charge",
});
console.log(succeeded.status); // "succeeded"
console.log(succeeded.reason); // "requested_by_customer"
```
## API Reference
### `mapRefundStatus(squareRefundStatus: string): RefundStatus`
| Square | suaveplan |
|---|---|
| `COMPLETED` | `"succeeded"` |
| `PENDING` | `"pending"` |
| _other_ (`REJECTED`, `FAILED`, ...) | `"failed"` |
### `mapRefund(r: SquareRefundDto): Refund`
Produces a `Refund` whose `amount` mirrors `r.amount_money` (with
the amount widened to `bigint`), whose `status` comes from
`mapRefundStatus`, whose `reason` is `"requested_by_customer"` when
`r.reason` is a non-empty string and `"other"` otherwise, and whose
`createdAt` is `r.created_at` verbatim. `paymentIntentId` is set to
`r.payment_id`.
### Types
- `SquareRefundDto` — the subset of Square's `Refund` envelope this
package consumes.
## Implementation Notes
- The reason fallback to `"other"` is deliberate: Square's free-form
`reason` carries operational notes ("customer called", "fraud
flagged by ops"), not the discriminated reasons suaveplan tracks.
Treating any non-empty value as `"requested_by_customer"` matches
the upstream package's behaviour verbatim.
- `mapRefundStatus` is exported separately so callers consuming raw
Square webhooks (where the refund envelope arrives outside an HTTP
response) can reuse the same mapping logic without round-tripping
through `mapRefund`.
- `r.amount_money.amount` is coerced via `BigInt()`. Square uses
smallest-unit integers, so the coercion is exact for every
documented currency.

View File

@@ -0,0 +1,175 @@
# SquareProvider
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SquareProvider` is the Square Payments API adapter for
`@suaveplan/payment`. It implements the structural `PaymentProvider`
contract with `name: "square"` (narrowed at the type level) and
routes every outbound HTTP call through the
`@suaveplan/payment-providers` foundation's `httpRequest`, which
handles JSON serialisation, the per-call timeout signal, external
`AbortSignal` composition, and typed `PaymentProviderError` raising.
The provider preserves the upstream `square.ts` surface verbatim — the
six lifecycle methods (`createIntent`, `confirmIntent`, `captureIntent`,
`cancelIntent`, `refund`, `getIntent`), the canonical request bodies,
and the status mapping — while wiring the previously-hard-coded
`globalThis.crypto.randomUUID()` call onto the foundation's injectable
`RandomIdFn` seam. Tests pass a deterministic counter instead of
monkey-patching `globalThis.fetch`.
## Features
- Implements `PaymentProvider` with literal `name: "square"`.
- Sends `Authorization: Bearer <apiKey>` and `Square-Version` on every
request (`Square-Version` defaults to `2024-01-18`; override via
`SquareConfig.apiVersion`).
- Sandbox toggle (`sandbox: true`) flips the default base URL to
`https://connect.squareupsandbox.com/v2`; an explicit `baseUrl`
always wins.
- Idempotency keys filled from the injected `randomId` seam when the
caller omits one — deterministic in tests, `crypto.randomUUID()` by
default in production.
- Multi-location merchants supported via the required
`SquareConfig.locationId` field, sent as `location_id` on every
`CreatePayment` body.
- All errors raised as `PaymentProviderError` (foundation contract);
the package itself never throws `Error` instances directly.
- Universal — no `node:`, `bun:`, `fs`, `path`, `crypto` imports.
## Usage: Basic Example
```ts
import { SquareProvider } from "@suaveplan/payment-provider-square";
const square = new SquareProvider({
apiKey: process.env.SQUARE_ACCESS_TOKEN ?? "",
locationId: process.env.SQUARE_LOCATION_ID ?? "",
});
const intent = await square.createIntent({
amount: { amount: 5000n, currency: "USD" },
description: "Order #123",
});
const captured = await square.captureIntent(intent.id);
console.log(captured.status); // "captured"
```
## Usage: Advanced Example
Inject deterministic seams (FetchLike + RandomIdFn) for testing,
target the sandbox, pin the API version, and compose with an external
`AbortSignal`:
```ts
import type { FetchLike } from "@suaveplan/payment-providers";
import { SquareProvider } from "@suaveplan/payment-provider-square";
const controller = new AbortController();
const fakeFetch: FetchLike = async (url, init) => {
return new Response(
JSON.stringify({
payment: {
id: "sq_pay_1",
status: "COMPLETED",
amount_money: { amount: 5000, currency: "USD" },
total_money: { amount: 5000, currency: "USD" },
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
},
}),
{ status: 200 }
);
};
const square = new SquareProvider({
apiKey: "sandbox-token",
locationId: "L1",
sandbox: true,
apiVersion: "2024-09-19",
fetch: fakeFetch,
signal: controller.signal,
randomId: () => "deterministic-idempotency-key",
timeoutMs: 5_000,
});
const intent = await square.createIntent({
amount: { amount: 5000n, currency: "USD" },
});
```
## API Reference
### `class SquareProvider implements PaymentProvider`
#### `constructor(config: SquareConfig)`
Accepts a `SquareConfig` (see `../types/config.md`). Required:
`apiKey`, `locationId`. Optional: `baseUrl`, `sandbox`, `apiVersion`,
`randomId`, `fetch`, `signal`, `timeoutMs`, `webhookSecret`.
#### `readonly name: "square"`
Literal — narrows at the type level for discriminated unions.
#### `createIntent(input: CreatePaymentIntentInput): Promise<PaymentIntent>`
POSTs `<baseUrl>/payments` with `amount_money`, `idempotency_key`
(falls back to `randomId()`), `autocomplete` (`true` unless
`captureMethod === "manual"`), `location_id`, and the optional
`customer_id`, `note`, `source_id` fields. Returns the mapped
`PaymentIntent`.
#### `confirmIntent(intentId: string): Promise<PaymentIntent>`
No-op for Square (payments are auto-confirmed). Delegates to
`getIntent(intentId)`.
#### `captureIntent(intentId: string): Promise<PaymentIntent>`
POSTs `<baseUrl>/payments/<intentId>/complete` with no body.
#### `cancelIntent(intentId: string): Promise<PaymentIntent>`
POSTs `<baseUrl>/payments/<intentId>/cancel` with no body.
#### `refund(input: RefundInput): Promise<Refund>`
POSTs `<baseUrl>/refunds` with `payment_id`, `idempotency_key`
(falls back to `randomId()`), optional `amount_money`, and optional
`reason`. Returns the mapped `Refund`.
#### `getIntent(intentId: string): Promise<PaymentIntent>`
GETs `<baseUrl>/payments/<intentId>`.
### Throws
Every method throws `PaymentProviderError` on HTTP failure, timeout,
or JSON parse failure (raised by the foundation's `httpRequest`).
`err.provider` is always `"square"`; `err.httpStatus` is set when the
failure originated from a non-ok response.
## Implementation Notes
- The provider stores its `FetchLike` / `AbortSignal` / `timeoutMs` /
`randomId` once at construction and passes them through to every
`httpRequest` call so behaviour is deterministic across method
invocations.
- `location_id` is sent on `CreatePayment` only — Square does not
require it on `CompletePayment` / `CancelPayment` / `Refund` calls.
- The default `randomId` is a closure that calls `crypto.randomUUID()`
at call time. Runtimes without a global `crypto` MUST inject their
own `RandomIdFn`.
- The class does not duplicate webhook verification logic; Square
webhook signatures are verified by `@suaveplan/payment`'s
`WebhookVerifier`, which is provider-agnostic.
- All formatting flows through the foundation: caller-supplied
`Content-Type` / `Accept` are preserved by `httpRequest`'s shallow
header merge, but the Square-specific `Authorization` and
`Square-Version` headers always win on collision.

View File

@@ -0,0 +1,102 @@
# SquareConfig and default constants
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the configuration shape `SquareProvider` accepts and the three
default constants the provider falls back to when the caller omits the
corresponding optional field. The interface extends the shared
`ApiKeyConfig` from `@suaveplan/payment-providers` so every common
injectable seam — `fetch`, `timeoutMs`, `signal`, `baseUrl`,
`webhookSecret` — is inherited rather than redeclared.
## Features
- `SquareConfig extends ApiKeyConfig` with `locationId: string`
(required), `sandbox?: boolean`, `apiVersion?: string`, and
`randomId?: RandomIdFn`.
- Three exported constants pin the production base URL, the sandbox
base URL, and the default `Square-Version` header value so they
show up exactly once across the package and are auditable.
- Type-only — the file ships no runtime objects beyond the constants;
the interface is erased at build time.
## Usage: Basic Example
```ts
import type { SquareConfig } from "@suaveplan/payment-provider-square";
const config: SquareConfig = {
apiKey: "sq0atp-...",
locationId: "L1234567890",
};
```
## Usage: Advanced Example
Pin every optional field — useful for the sandbox + deterministic
testing combination:
```ts
import type { SquareConfig } from "@suaveplan/payment-provider-square";
import {
SQUARE_DEFAULT_API_VERSION,
SQUARE_SANDBOX_BASE_URL,
} from "@suaveplan/payment-provider-square";
const config: SquareConfig = {
apiKey: "sq0atp-sandbox-...",
locationId: "L_SANDBOX",
sandbox: true,
apiVersion: SQUARE_DEFAULT_API_VERSION,
randomId: () => "deterministic-id",
timeoutMs: 5_000,
baseUrl: SQUARE_SANDBOX_BASE_URL, // optional — sandbox flag picks this up
};
```
## API Reference
### `interface SquareConfig extends ApiKeyConfig`
| Field | Type | Required | Notes |
|---|---|---|---|
| `apiKey` | `string` | yes | Inherited from `ApiKeyConfig`. |
| `locationId` | `string` | yes | Square `location_id` sent on every `CreatePayment` body. |
| `sandbox` | `boolean` | no | When `true`, flips the default base URL to `SQUARE_SANDBOX_BASE_URL`. |
| `apiVersion` | `string` | no | Value of the `Square-Version` header. Defaults to `SQUARE_DEFAULT_API_VERSION`. |
| `randomId` | `RandomIdFn` | no | Generator for missing idempotency keys. Defaults to `crypto.randomUUID()`. |
| `baseUrl` | `string` | no | Inherited from `ApiKeyConfig`. Wins over `sandbox`. |
| `webhookSecret` | `string` | no | Inherited from `ApiKeyConfig`. Not used by the provider itself. |
| `fetch` | `FetchLike` | no | Inherited from `ProviderCommonOptions`. |
| `timeoutMs` | `number` | no | Inherited from `ProviderCommonOptions`. |
| `signal` | `AbortSignal` | no | Inherited from `ProviderCommonOptions`. |
### `const SQUARE_DEFAULT_BASE_URL`
`"https://connect.squareup.com/v2"` — Square production base URL.
### `const SQUARE_SANDBOX_BASE_URL`
`"https://connect.squareupsandbox.com/v2"` — Square sandbox base URL.
### `const SQUARE_DEFAULT_API_VERSION`
`"2024-01-18"` — default `Square-Version` header value.
## Implementation Notes
- `locationId` is intentionally required (the upstream
`payment-providers` Square adapter omitted it; consumers had to
wrap the provider to inject it). Multi-location merchants are the
common case, and Square's `CreatePayment` endpoint rejects requests
that target the wrong location implicitly.
- The `sandbox` flag is a convenience — callers who need exotic base
URLs (Square Connect for the EU, an internal proxy, etc.) should
set `baseUrl` directly.
- `randomId` is shaped as a synchronous `() => string` rather than a
promise-returning function. Square's idempotency keys are opaque;
any unique-enough string works.