import { describe, it, expect } from 'vitest';
import { CheckoutSessionManager } from '../../src/ucp/checkout-session.js';
import type { LineItem, BuyerInfo, Address, PaymentInstrument } from '../../src/types.js';
// ─── Helpers ───
function makeBuyer(): BuyerInfo {
return { email: 'buyer@example.com', first_name: 'Alice', last_name: 'Smith' };
}
function makeAddress(): Address {
return {
line1: '123 Main St',
city: 'Portland',
province: 'OR',
postal_code: '97201',
country: 'US',
};
}
function makePaymentInstrument(): PaymentInstrument {
return {
handler_id: 'shopify_payments',
type: 'card',
display: { brand: 'visa', last_digits: '4242' },
};
}
function makeLineItem(overrides: Partial<LineItem> = {}): LineItem {
return {
id: 'li-1',
product_id: 'prod-1',
variant_id: 'var-1',
title: 'Widget',
quantity: 1,
unit_amount: 1000,
total_amount: 1000,
type: 'product',
...overrides,
};
}
// ─── create() ───
describe('CheckoutSessionManager.create', () => {
it('returns a session with status "incomplete"', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
expect(session.status).toBe('incomplete');
});
it('returns a session with an id', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
expect(session.id).toBeDefined();
expect(typeof session.id).toBe('string');
expect(session.id.length).toBeGreaterThan(0);
});
it('returns a session with warning messages for missing fields', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
expect(session.messages.length).toBeGreaterThanOrEqual(4);
const codes = session.messages.map((m) => m.code);
expect(codes).toContain('missing_line_items');
expect(codes).toContain('missing_buyer_email');
expect(codes).toContain('missing_shipping_address');
expect(codes).toContain('missing_payment');
});
it('creates a session with the specified currency', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create('JPY');
expect(session.currency).toBe('JPY');
expect(session.totals.currency).toBe('JPY');
});
it('defaults currency to USD', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
expect(session.currency).toBe('USD');
});
});
// ─── update() ───
describe('CheckoutSessionManager.update', () => {
it('updates buyer info', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const updated = mgr.update(session.id, { buyer: makeBuyer() });
expect(updated.buyer?.email).toBe('buyer@example.com');
// missing_buyer_email warning should be gone
expect(updated.messages.some((m) => m.code === 'missing_buyer_email')).toBe(false);
});
it('updates line items and recalculates totals', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const items = [makeLineItem({ total_amount: 5000, unit_amount: 5000, quantity: 1 })];
const updated = mgr.update(session.id, { line_items: items });
expect(updated.line_items).toHaveLength(1);
expect(updated.totals.subtotal).toBe(5000);
expect(updated.totals.tax).toBe(400); // 8%
expect(updated.totals.fee).toBe(25); // 0.5%
});
it('updates shipping address', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const updated = mgr.update(session.id, { shipping_address: makeAddress() });
expect(updated.shipping_address?.city).toBe('Portland');
expect(updated.messages.some((m) => m.code === 'missing_shipping_address')).toBe(false);
});
it('throws for nonexistent session', () => {
const mgr = new CheckoutSessionManager();
expect(() => mgr.update('nonexistent-id', { buyer: makeBuyer() })).toThrow(
'Checkout session not found',
);
});
});
// ─── State transitions ───
describe('CheckoutSessionManager state transitions', () => {
it('becomes ready_for_complete when all fields are present', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const updated = mgr.update(session.id, {
line_items: [makeLineItem()],
buyer: makeBuyer(),
shipping_address: makeAddress(),
payment_instruments: [makePaymentInstrument()],
});
expect(updated.status).toBe('ready_for_complete');
// All warning messages should be cleared
expect(updated.messages).toHaveLength(0);
});
it('stays incomplete when buyer is missing', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const updated = mgr.update(session.id, {
line_items: [makeLineItem()],
shipping_address: makeAddress(),
payment_instruments: [makePaymentInstrument()],
});
expect(updated.status).toBe('incomplete');
});
it('stays incomplete when payment is missing', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const updated = mgr.update(session.id, {
line_items: [makeLineItem()],
buyer: makeBuyer(),
shipping_address: makeAddress(),
});
expect(updated.status).toBe('incomplete');
});
});
// ─── Invalid transitions ───
describe('CheckoutSessionManager.transition', () => {
it('allows incomplete -> requires_escalation', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const result = mgr.transition(session.id, 'requires_escalation');
expect(result.status).toBe('requires_escalation');
});
it('allows incomplete -> ready_for_complete', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
const result = mgr.transition(session.id, 'ready_for_complete');
expect(result.status).toBe('ready_for_complete');
});
it('allows requires_escalation -> incomplete', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
mgr.transition(session.id, 'requires_escalation');
const result = mgr.transition(session.id, 'incomplete');
expect(result.status).toBe('incomplete');
});
it('allows requires_escalation -> ready_for_complete', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
mgr.transition(session.id, 'requires_escalation');
const result = mgr.transition(session.id, 'ready_for_complete');
expect(result.status).toBe('ready_for_complete');
});
it('rejects ready_for_complete -> incomplete', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
mgr.transition(session.id, 'ready_for_complete');
expect(() => mgr.transition(session.id, 'incomplete')).toThrow('Invalid transition');
});
it('rejects ready_for_complete -> requires_escalation', () => {
const mgr = new CheckoutSessionManager();
const session = mgr.create();
mgr.transition(session.id, 'ready_for_complete');
expect(() => mgr.transition(session.id, 'requires_escalation')).toThrow(
'Invalid transition',
);
});
it('throws for nonexistent session', () => {
const mgr = new CheckoutSessionManager();
expect(() => mgr.transition('nonexistent-id', 'ready_for_complete')).toThrow(
'Checkout session not found',
);
});
});