Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
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;
}
}

View File

@@ -0,0 +1,37 @@
// Side-effect imports — surface the Hono ContextVariableMap augmentation
// (`user: Principal | null`) so tests can `c.set("user", ...)` with the
// expected typing.
import "@suaveplan/api-plugin-auth";
/**
* Test setup for @suaveplan/api-plugin-payment.
*
* Installs a real in-memory OpenTelemetry MeterProvider BEFORE any source
* module imports so the meter resolved on first call binds to it.
*/
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const reader = new PeriodicExportingMetricReader({
exporter: testInMemoryMetricExporter,
exportIntervalMillis: 60_000,
});
export const testMeterProvider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(testMeterProvider);
export async function flushTestMetrics() {
await testMeterProvider.forceFlush();
const collected = testInMemoryMetricExporter.getMetrics();
testInMemoryMetricExporter.reset();
return collected;
}