AkurAI Build
Menu

Public repository / 17ecb7d0341c5db2f4b548f08a16f8a1f3d26e4e

AkurAI-Platform / payment/service.test.ts

Latest change 54864d769f47e4b584afb75e6ecc5b19890c21b4 - Add modular subscriptions with myPOS checkout by Ólafur Búi Ólafsson

import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createPaymentService, type PaymentService } from "./service";
import { nextBillingDate, quoteCheckout, validateCatalog, type Catalog } from "./core";

const catalog: Catalog = validateCatalog({
  currency: "EUR",
  monthly_unit_cents: 900,
  annual_discount_percent: 15,
  products: [
    { id: "mail", name: "AkurAI Mail", category: "Communication" },
    { id: "passvault", name: "AkurAI PassVault", category: "Security" },
    { id: "crm", name: "AkurAI CRM", category: "Business", available: false },
  ],
});

let directory: string | null = null;
let service: PaymentService | null = null;

afterEach(() => {
  service?.close();
  service = null;
  if (directory) rmSync(directory, { recursive: true, force: true });
  directory = null;
});

function createService(now: () => Date = () => new Date("2026-07-17T12:00:00Z")): PaymentService {
  directory = mkdtempSync(join(tmpdir(), "akurai-payments-test-"));
  service = createPaymentService({
    catalog,
    dbPath: join(directory, "payments.db"),
    publicUrl: "http://payments.test",
    upstreamUrl: "http://127.0.0.1:1",
    env: { PAYMENTS_PROVIDER: "mock" },
    now,
  });
  return service;
}

async function checkout(app: PaymentService, interval: "monthly" | "annual" = "monthly", productIds = ["mail"]): Promise<{ orderId: string; manageToken: string; amountCents: number }> {
  const response = await app.fetch(new Request("http://payments.test/api/payments/checkout", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      productIds,
      interval,
      customer: { name: "Mock Customer", email: "mock@example.test" },
    }),
  }));
  expect(response.status).toBe(201);
  return response.json();
}

describe("subscription pricing", () => {
  test("locks monthly and discounted annual totals on the server", () => {
    const monthly = quoteCheckout(catalog, {
      productIds: ["mail", "passvault"],
      interval: "monthly",
      customer: { name: "Customer", email: "customer@example.test" },
    });
    const annual = quoteCheckout(catalog, {
      productIds: ["mail", "passvault"],
      interval: "annual",
      customer: { name: "Customer", email: "customer@example.test" },
    });
    expect(monthly.amountCents).toBe(1_800);
    expect(annual.amountCents).toBe(18_360);
    expect(() => quoteCheckout(catalog, {
      productIds: ["crm"],
      interval: "monthly",
      customer: { name: "Customer", email: "customer@example.test" },
    })).toThrow("unavailable");
  });

  test("preserves end-of-month billing dates", () => {
    expect(nextBillingDate(new Date("2026-01-31T12:00:00Z"), "monthly").toISOString()).toBe("2026-02-28T12:00:00.000Z");
    expect(nextBillingDate(new Date("2024-02-29T12:00:00Z"), "annual").toISOString()).toBe("2025-02-28T12:00:00.000Z");
  });
});

describe("mock checkout", () => {
  test("uses the hosted redirect flow, activates, exposes management, and cancels", async () => {
    const app = createService();
    const checkoutResult = await checkout(app, "annual", ["mail", "passvault"]);
    expect(checkoutResult.amountCents).toBe(18_360);

    const hosted = await app.fetch(new Request(`http://payments.test/api/payments/mock/checkout/${checkoutResult.orderId}`));
    expect(hosted.status).toBe(200);
    expect(await hosted.text()).toContain("Approve test payment");

    const approval = await app.fetch(new Request(`http://payments.test/api/payments/mock/orders/${checkoutResult.orderId}/approve`, { method: "POST" }));
    expect(approval.status).toBe(303);

    const order = await (await app.fetch(new Request(`http://payments.test/api/payments/orders/${checkoutResult.orderId}`))).json();
    expect(order.status).toBe("paid");
    expect(order.amountCents).toBe(18_360);

    const managed = await (await app.fetch(new Request(`http://payments.test/api/payments/subscriptions/${checkoutResult.manageToken}`))).json();
    expect(managed.status).toBe("active");
    expect(managed.email).toBe("m***@example.test");
    expect(managed.products.map((product: { id: string }) => product.id)).toEqual(["mail", "passvault"]);

    const canceled = await (await app.fetch(new Request(`http://payments.test/api/payments/subscriptions/${checkoutResult.manageToken}?action=cancel`, {
      method: "POST",
      headers: { accept: "application/json" },
    }))).json();
    expect(canceled.status).toBe("canceled");
  });

  test("records a declined mock payment without creating a subscription", async () => {
    const app = createService();
    const checkoutResult = await checkout(app);
    const decline = await app.fetch(new Request(`http://payments.test/api/payments/mock/orders/${checkoutResult.orderId}/decline`, { method: "POST" }));
    expect(decline.status).toBe(303);
    const order = await (await app.fetch(new Request(`http://payments.test/api/payments/orders/${checkoutResult.orderId}`))).json();
    expect(order.status).toBe("failed");
    const managed = await app.fetch(new Request(`http://payments.test/api/payments/subscriptions/${checkoutResult.manageToken}`));
    expect(managed.status).toBe(404);
  });

  test("renews a due subscription automatically at its locked price", async () => {
    let current = new Date("2026-01-31T12:00:00Z");
    const app = createService(() => new Date(current));
    const checkoutResult = await checkout(app);
    await app.fetch(new Request(`http://payments.test/api/payments/mock/orders/${checkoutResult.orderId}/approve`, { method: "POST" }));

    current = new Date("2026-03-01T12:00:00Z");
    await app.runRenewals();

    const managed = await (await app.fetch(new Request(`http://payments.test/api/payments/subscriptions/${checkoutResult.manageToken}`))).json();
    expect(managed.status).toBe("active");
    expect(managed.amountCents).toBe(900);
    expect(managed.nextChargeAt).toBe("2026-03-28T12:00:00.000Z");
  });

  test("rejects client attempts to select unavailable products", async () => {
    const app = createService();
    const response = await app.fetch(new Request("http://payments.test/api/payments/checkout", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        productIds: ["crm"],
        interval: "monthly",
        customer: { name: "Mock Customer", email: "mock@example.test" },
      }),
    }));
    expect(response.status).toBe(400);
    expect(await response.json()).toEqual({ error: "One or more selected apps are unavailable" });
  });
});