Files
suaveplan/packages/api-stack/api/api-plugin-payment/tests/setup/fake-provider.ts
2026-05-27 23:29:53 +01:00

169 lines
5.5 KiB
TypeScript

import type {
CreatePaymentIntentInput,
PaymentIntent,
PaymentProvider,
PaymentStatus,
Refund,
RefundInput,
} from "@suaveplan/payment";
interface MoneyLike {
readonly amount: bigint;
readonly currency: string;
}
function nowIso() {
return new Date().toISOString();
}
function makeMoney(amount: bigint, currency: string): MoneyLike {
return { amount, currency };
}
/**
* Test-only `PaymentProvider` implementing the real interface from
* `@suaveplan/payment`. Stores intents and refunds in two in-memory
* maps so the tests can assert against deterministic state. No
* external IO.
*/
export class FakePaymentProvider implements PaymentProvider {
public readonly name = "fake";
private readonly intents = new Map<string, PaymentIntent>();
private readonly idemMap = new Map<string, string>();
private seq = 0;
public failCreate = false;
public returnEmptyOnCreate = false;
public reset(): void {
this.intents.clear();
this.idemMap.clear();
this.seq = 0;
this.failCreate = false;
this.returnEmptyOnCreate = false;
}
public async createIntent(
input: CreatePaymentIntentInput
): Promise<PaymentIntent> {
if (this.failCreate) {
throw new Error("simulated provider failure");
}
if (this.returnEmptyOnCreate) {
return null as unknown as PaymentIntent;
}
if (input.idempotencyKey !== undefined) {
const existingId = this.idemMap.get(input.idempotencyKey);
if (existingId !== undefined) {
const found = this.intents.get(existingId);
if (found) return found;
}
}
this.seq += 1;
const id = `pi_${this.seq}`;
const intent: PaymentIntent = {
id,
status: input.captureMethod === "manual" ? "created" : "created",
amount: makeMoney(input.amount.amount, input.amount.currency),
capturedAmount: makeMoney(0n, input.amount.currency),
refundedAmount: makeMoney(0n, input.amount.currency),
captureMethod: input.captureMethod ?? "automatic",
refunds: [],
createdAt: nowIso(),
updatedAt: nowIso(),
...(input.description !== undefined
? { description: input.description }
: {}),
...(input.metadata !== undefined
? { metadata: input.metadata }
: {}),
...(input.idempotencyKey !== undefined
? { idempotencyKey: input.idempotencyKey }
: {}),
};
this.intents.set(id, intent);
if (input.idempotencyKey !== undefined) {
this.idemMap.set(input.idempotencyKey, id);
}
return intent;
}
public async confirmIntent(id: string): Promise<PaymentIntent> {
return this.updateStatus(id, "authorized");
}
public async captureIntent(
id: string,
amount?: MoneyLike
): Promise<PaymentIntent> {
const current = this.mustGet(id);
const captureAmt = amount?.amount ?? current.amount.amount;
const newStatus: PaymentStatus = "captured";
const updated: PaymentIntent = {
...current,
status: newStatus,
capturedAmount: makeMoney(captureAmt, current.amount.currency),
updatedAt: nowIso(),
};
this.intents.set(id, updated);
return updated;
}
public async cancelIntent(id: string): Promise<PaymentIntent> {
return this.updateStatus(id, "canceled");
}
public async refund(input: RefundInput): Promise<Refund> {
const current = this.mustGet(input.paymentIntentId);
const amt = input.amount?.amount ?? current.capturedAmount.amount;
const refundedSoFar = current.refundedAmount.amount + amt;
const refund: Refund = {
id: `re_${this.seq + 1}`,
paymentIntentId: input.paymentIntentId,
amount: makeMoney(amt, current.amount.currency),
status: "succeeded",
...(input.reason !== undefined ? { reason: input.reason } : {}),
createdAt: nowIso(),
...(input.metadata !== undefined
? { metadata: input.metadata }
: {}),
};
this.seq += 1;
const newStatus: PaymentStatus =
refundedSoFar >= current.capturedAmount.amount
? "refunded"
: "partially_refunded";
const updated: PaymentIntent = {
...current,
status: newStatus,
refundedAmount: makeMoney(refundedSoFar, current.amount.currency),
refunds: [...current.refunds, refund],
updatedAt: nowIso(),
};
this.intents.set(current.id, updated);
return refund;
}
public async getIntent(id: string): Promise<PaymentIntent> {
return this.mustGet(id);
}
private mustGet(id: string): PaymentIntent {
const found = this.intents.get(id);
if (found === undefined) {
throw new Error(`provider has no intent "${id}"`);
}
return found;
}
private updateStatus(id: string, status: PaymentStatus): PaymentIntent {
const current = this.mustGet(id);
const updated: PaymentIntent = {
...current,
status,
updatedAt: nowIso(),
};
this.intents.set(id, updated);
return updated;
}
}