import type { CreatePaymentIntentInput, PaymentIntent, PaymentProvider, Refund, RefundInput, } from "@suaveplan/payment"; import { httpRequest, PAYMENT_PROVIDER_CODES, PaymentProviderError, type RandomIdFn, } from "@suaveplan/payment-providers"; import type { AdyenConfig } from "../types/adyen.js"; import { type AdyenModification, type AdyenPayment, mapPayment, mapRefundStatus, } from "./mapping.js"; const DEFAULT_BASE_URL = "https://checkout-test.adyen.com/v71"; const DEFAULT_TIMEOUT_MS = 30_000; const PROVIDER_NAME = "adyen"; function defaultRandomId(): string { const cryptoApi = (globalThis as { crypto?: { randomUUID?: () => string } }) .crypto; if (cryptoApi?.randomUUID !== undefined) { return cryptoApi.randomUUID(); } throw new PaymentProviderError( PAYMENT_PROVIDER_CODES.PARSE_ERROR, "AdyenConfig.randomId is required when globalThis.crypto.randomUUID is unavailable", { provider: PROVIDER_NAME } ); } /** * Adyen Checkout API v71 adapter implementing `PaymentProvider`. * Authenticates via `X-API-Key`, injects the configured * `merchantAccount` into every modification body, and generates * default `reference` values via an injectable `RandomIdFn` so the * package stays universal. */ export class AdyenProvider implements PaymentProvider { public readonly name = PROVIDER_NAME; private readonly apiKey: string; private readonly merchantAccount: string; private readonly baseUrl: string; private readonly timeoutMs: number; private readonly fetchImpl: AdyenConfig["fetch"]; private readonly signal: AbortSignal | undefined; private readonly randomId: RandomIdFn; public constructor(config: AdyenConfig) { this.apiKey = config.apiKey; this.merchantAccount = config.merchantAccount; this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL; this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS; this.fetchImpl = config.fetch; this.signal = config.signal; this.randomId = config.randomId ?? defaultRandomId; } public async createIntent( input: CreatePaymentIntentInput ): Promise { const captureMethod = input.captureMethod ?? "automatic"; const reference = input.idempotencyKey ?? this.randomId(); const body: Record = { amount: { value: Number(input.amount.amount), currency: input.amount.currency, }, merchantAccount: this.merchantAccount, reference, }; if (captureMethod === "manual") { body["captureDelayHours"] = 0; } if (input.description !== undefined) { body["shopperStatement"] = input.description; } if (input.returnUrl !== undefined) { body["returnUrl"] = input.returnUrl; } if (input.paymentMethodId !== undefined) { body["paymentMethod"] = { storedPaymentMethodId: input.paymentMethodId, }; } if (input.metadata !== undefined) { body["metadata"] = input.metadata; } const { data } = await this.request({ method: "POST", url: `${this.baseUrl}/payments`, headers: this.headers(input.idempotencyKey), body, }); return mapPayment(data, captureMethod, new Date().toISOString()); } public async confirmIntent(intentId: string): Promise { const { data } = await this.request({ method: "POST", url: `${this.baseUrl}/payments/${intentId}/details`, headers: this.headers(), body: { details: {} }, }); return mapPayment(data, "manual", new Date().toISOString()); } public async captureIntent(intentId: string): Promise { const { data } = await this.request({ method: "POST", url: `${this.baseUrl}/payments/${intentId}/captures`, headers: this.headers(), body: { merchantAccount: this.merchantAccount, amount: {} }, }); const nowIso = new Date().toISOString(); const currency = data.amount.currency; const amount = BigInt(data.amount.value); return { id: intentId, status: "captured", amount: { amount, currency }, capturedAmount: { amount, currency }, refundedAmount: { amount: 0n, currency }, captureMethod: "manual", refunds: [], createdAt: nowIso, updatedAt: nowIso, }; } public async cancelIntent(intentId: string): Promise { const { data } = await this.request({ method: "POST", url: `${this.baseUrl}/payments/${intentId}/cancels`, headers: this.headers(), body: { merchantAccount: this.merchantAccount }, }); const nowIso = new Date().toISOString(); const currency = data.amount.currency; return { id: intentId, status: "canceled", amount: { amount: 0n, currency }, capturedAmount: { amount: 0n, currency }, refundedAmount: { amount: 0n, currency }, captureMethod: "manual", refunds: [], createdAt: nowIso, updatedAt: nowIso, }; } public async refund(input: RefundInput): Promise { const body: Record = { merchantAccount: this.merchantAccount, }; if (input.amount !== undefined) { body["amount"] = { value: Number(input.amount.amount), currency: input.amount.currency, }; } const { data } = await this.request({ method: "POST", url: `${this.baseUrl}/payments/${input.paymentIntentId}/refunds`, headers: this.headers(input.idempotencyKey), body, }); return { id: data.pspReference, paymentIntentId: input.paymentIntentId, amount: { amount: BigInt(data.amount.value), currency: data.amount.currency, }, status: mapRefundStatus(data.status), createdAt: new Date().toISOString(), }; } public async getIntent(intentId: string): Promise { const { data } = await this.request({ method: "GET", url: `${this.baseUrl}/payments/${intentId}`, headers: this.headers(), }); return mapPayment(data, "automatic", new Date().toISOString()); } private headers(idempotencyKey?: string): Record { const h: Record = { "X-API-Key": this.apiKey }; if (idempotencyKey !== undefined) { h["Idempotency-Key"] = idempotencyKey; } return h; } private request(opts: { method: "GET" | "POST"; url: string; headers: Record; body?: unknown; }): Promise<{ status: number; data: T }> { return httpRequest({ method: opts.method, url: opts.url, headers: opts.headers, body: opts.body, fetch: this.fetchImpl, signal: this.signal, timeoutMs: this.timeoutMs, provider: PROVIDER_NAME, }); } }