/**
* execute_checkout Tool — Comprehensive Test Suite
*
* Tests the full AP2 mandate chain checkout execution flow,
* covering all 9 steps: session retrieval, status check, chain verification,
* guardrail validation, mandate storage, checkout URL creation,
* fee collection, order building, and result assembly.
*/
import { executeCheckout } from '../../src/tools/execute-checkout.js';
import type {
ExecuteParams,
ExecuteCheckoutDeps,
} from '../../src/tools/execute-checkout.js';
import type {
CheckoutSession,
Mandate,
FeeRecord,
} from '../../src/types.js';
// ─── Mock Factories ───
function createMockSession(overrides: Partial<CheckoutSession> = {}): CheckoutSession {
return {
id: 'checkout-123',
status: 'ready_for_complete' as const,
currency: 'USD',
line_items: [
{
id: 'li-1',
product_id: 'p-1',
variant_id: 'v-1',
title: 'Widget',
quantity: 2,
unit_amount: 1000,
total_amount: 2000,
type: 'product' as const,
},
],
totals: {
subtotal: 2000,
tax: 160,
shipping: 0,
discount: 0,
fee: 10,
total: 2170,
currency: 'USD',
},
buyer: { email: 'buyer@test.com' },
shipping_address: {
line1: '123 Test St',
city: 'SF',
postal_code: '94105',
country: 'US',
},
payment_instruments: [
{ handler_id: 'ap2_mandate', type: 'mandate' as const },
],
messages: [],
continue_url: 'gid://shopify/Cart/abc123',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
...overrides,
};
}
function createMockIntentMandate(overrides: Partial<Mandate> = {}): Mandate {
return {
id: 'mandate-intent-1',
type: 'intent' as const,
status: 'active' as const,
issuer: 'buyer-agent',
subject: 'buyer@test.com',
payload: { max_amount: 10000, currency: 'USD' },
signature: 'jws-token',
issued_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 3600000).toISOString(),
...overrides,
};
}
function createMockCartMandate(overrides: Partial<Mandate> = {}): Mandate {
return {
id: 'mandate-cart-1',
type: 'cart' as const,
status: 'active' as const,
issuer: 'merchant-agent',
subject: 'buyer@test.com',
payload: {
checkout_id: 'checkout-123',
line_items: [
{
id: 'li-1',
product_id: 'p-1',
variant_id: 'v-1',
title: 'Widget',
quantity: 2,
unit_amount: 1000,
total_amount: 2000,
type: 'product' as const,
},
],
totals: {
subtotal: 2000,
tax: 160,
shipping: 0,
discount: 0,
fee: 10,
total: 2170,
currency: 'USD',
},
merchant_signature: 'merchant-sig',
},
signature: 'jws-token-cart',
issued_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 3600000).toISOString(),
...overrides,
};
}
function createMockPaymentMandate(overrides: Partial<Mandate> = {}): Mandate {
return {
id: 'mandate-payment-1',
type: 'payment' as const,
status: 'active' as const,
issuer: 'buyer-agent',
subject: 'buyer@test.com',
payload: {
checkout_id: 'checkout-123',
amount: 2170,
currency: 'USD',
payment_handler_id: 'ap2_mandate',
cart_mandate_id: 'mandate-cart-1',
},
signature: 'jws-token-payment',
issued_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 3600000).toISOString(),
...overrides,
};
}
function createMockFeeRecord(overrides: Partial<FeeRecord> = {}): FeeRecord {
return {
transaction_id: 'txn-001',
checkout_id: 'checkout-123',
order_id: 'order-001',
gross_amount: 2170,
fee_rate: 0.005,
fee_amount: 11,
currency: 'USD',
wallet_address: '0xfee',
status: 'collected',
created_at: new Date().toISOString(),
...overrides,
};
}
function createMockParams(overrides: Partial<ExecuteParams> = {}): ExecuteParams {
return {
checkout_id: 'checkout-123',
intent_mandate: 'jws-intent-token',
cart_mandate: 'jws-cart-token',
payment_mandate: 'jws-payment-token',
...overrides,
};
}
function createMockDeps(overrides: Partial<ExecuteCheckoutDeps> = {}): ExecuteCheckoutDeps {
const intentMandate = createMockIntentMandate();
const cartMandate = createMockCartMandate();
const paymentMandate = createMockPaymentMandate();
const pendingFee = createMockFeeRecord({ status: 'pending' });
const collectedFee = createMockFeeRecord({ status: 'collected' });
return {
sessionManager: {
get: vi.fn().mockReturnValue(createMockSession()),
create: vi.fn(),
update: vi.fn(),
getStatus: vi.fn(),
transition: vi.fn(),
} as any,
verifier: {
verifyMandateChain: vi.fn().mockResolvedValue({ valid: true, errors: [] }),
verifyIntent: vi.fn().mockResolvedValue({ valid: true, mandate: intentMandate }),
verifyCart: vi.fn().mockResolvedValue({ valid: true, mandate: cartMandate }),
verifyPayment: vi.fn().mockResolvedValue({ valid: true, mandate: paymentMandate }),
} as any,
mandateStore: {
store: vi.fn().mockResolvedValue(undefined),
get: vi.fn(),
getByCheckout: vi.fn(),
revoke: vi.fn(),
} as any,
guardrail: {
validateMandateAmount: vi.fn().mockReturnValue(true),
validatePrice: vi.fn(),
validateInventory: vi.fn(),
validateCheckoutTransition: vi.fn(),
runAllChecks: vi.fn(),
} as any,
feeCollector: {
calculateFee: vi.fn().mockReturnValue(pendingFee),
collect: vi.fn().mockResolvedValue(collectedFee),
getLedger: vi.fn(),
getTotalCollected: vi.fn(),
} as any,
storefrontAPI: {
createCheckoutUrl: vi.fn().mockResolvedValue('https://shop.myshopify.com/checkout/abc'),
searchProducts: vi.fn(),
getProduct: vi.fn(),
createCart: vi.fn(),
addToCart: vi.fn(),
getCart: vi.fn(),
} as any,
...overrides,
};
}
// ─── Test Suite ───
describe('executeCheckout', () => {
let params: ExecuteParams;
let deps: ExecuteCheckoutDeps;
beforeEach(() => {
params = createMockParams();
deps = createMockDeps();
});
// ──────────────────────────────────────────────
// 1. Happy path — all steps succeed
// ──────────────────────────────────────────────
describe('happy path', () => {
it('returns success with order, checkout URL from StorefrontAPI, and fee', async () => {
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.order).toBeDefined();
expect(result.order!.checkout_id).toBe('checkout-123');
expect(result.order!.status).toBe('confirmed');
expect(result.order!.line_items).toHaveLength(1);
expect(result.order!.line_items[0]!.title).toBe('Widget');
expect(result.order!.totals.total).toBe(2170);
expect(result.order!.totals.currency).toBe('USD');
expect(result.order!.fulfillment).toEqual({ status: 'unfulfilled' });
expect(result.order!.id).toMatch(/^order_/);
expect(result.order!.created_at).toBeDefined();
expect(result.order!.updated_at).toBeDefined();
expect(result.checkout_url).toBe('https://shop.myshopify.com/checkout/abc');
expect(result.fee).toBeDefined();
expect(result.fee!.status).toBe('collected');
// No errors on clean happy path
expect(result.errors).toBeUndefined();
});
it('calls all dependency methods in the correct order', async () => {
await executeCheckout(params, deps);
// Step 1: session retrieval
expect(deps.sessionManager.get).toHaveBeenCalledWith('checkout-123');
// Step 3: mandate chain verification
expect(deps.verifier.verifyMandateChain).toHaveBeenCalledWith(
'jws-intent-token',
'jws-cart-token',
'jws-payment-token',
);
// Step 4: guardrail - intent re-verify
expect(deps.verifier.verifyIntent).toHaveBeenCalledWith('jws-intent-token');
// Step 4: guardrail - amount check
expect(deps.guardrail.validateMandateAmount).toHaveBeenCalledWith(
expect.objectContaining({ id: 'mandate-intent-1', type: 'intent' }),
2170,
);
// Step 5: mandate storage - verifyIntent/Cart/Payment called for decode
expect(deps.verifier.verifyCart).toHaveBeenCalledWith('jws-cart-token');
expect(deps.verifier.verifyPayment).toHaveBeenCalledWith('jws-payment-token');
expect(deps.mandateStore.store).toHaveBeenCalledTimes(3);
// Step 6: checkout URL
expect(deps.storefrontAPI!.createCheckoutUrl).toHaveBeenCalledWith(
'gid://shopify/Cart/abc123',
);
// Step 7: fee
expect(deps.feeCollector.calculateFee).toHaveBeenCalledWith(2170, 'USD');
expect(deps.feeCollector.collect).toHaveBeenCalled();
});
it('returns success with simulated checkout URL when storefrontAPI is null', async () => {
deps = createMockDeps({ storefrontAPI: null });
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.checkout_url).toBe(
'https://checkout.shopify.com/sessions/checkout-123',
);
expect(result.order).toBeDefined();
expect(result.fee).toBeDefined();
});
});
// ──────────────────────────────────────────────
// 2. Session not found
// ──────────────────────────────────────────────
describe('session not found', () => {
it('returns failure with descriptive error when session does not exist', async () => {
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(undefined);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toEqual([
'Checkout session not found: checkout-123',
]);
expect(result.order).toBeUndefined();
expect(result.checkout_url).toBeUndefined();
expect(result.fee).toBeUndefined();
});
it('does not call any downstream dependencies', async () => {
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(undefined);
await executeCheckout(params, deps);
expect(deps.verifier.verifyMandateChain).not.toHaveBeenCalled();
expect(deps.verifier.verifyIntent).not.toHaveBeenCalled();
expect(deps.guardrail.validateMandateAmount).not.toHaveBeenCalled();
expect(deps.mandateStore.store).not.toHaveBeenCalled();
expect(deps.feeCollector.calculateFee).not.toHaveBeenCalled();
});
});
// ──────────────────────────────────────────────
// 3. Session wrong status
// ──────────────────────────────────────────────
describe('session wrong status', () => {
it('returns failure when session status is "incomplete"', async () => {
const incompleteSession = createMockSession({ status: 'incomplete' });
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(incompleteSession);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0]).toContain('"incomplete"');
expect(result.errors![0]).toContain('expected "ready_for_complete"');
});
it('returns failure when session status is "requires_escalation"', async () => {
const escalationSession = createMockSession({ status: 'requires_escalation' });
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(escalationSession);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0]).toContain('"requires_escalation"');
expect(result.errors![0]).toContain('expected "ready_for_complete"');
});
it('does not proceed to mandate chain verification', async () => {
const incompleteSession = createMockSession({ status: 'incomplete' });
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(incompleteSession);
await executeCheckout(params, deps);
expect(deps.verifier.verifyMandateChain).not.toHaveBeenCalled();
});
});
// ──────────────────────────────────────────────
// 4. Mandate chain invalid
// ──────────────────────────────────────────────
describe('mandate chain invalid', () => {
it('returns failure with chain errors when verifyMandateChain returns invalid', async () => {
(deps.verifier.verifyMandateChain as ReturnType<typeof vi.fn>).mockResolvedValue({
valid: false,
errors: [
'Cart total (5000) exceeds intent max_amount (3000)',
'Payment currency (EUR) does not match cart currency (USD)',
],
});
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toEqual([
'Cart total (5000) exceeds intent max_amount (3000)',
'Payment currency (EUR) does not match cart currency (USD)',
]);
});
it('does not proceed to guardrail or storage when chain is invalid', async () => {
(deps.verifier.verifyMandateChain as ReturnType<typeof vi.fn>).mockResolvedValue({
valid: false,
errors: ['Chain invalid'],
});
await executeCheckout(params, deps);
expect(deps.verifier.verifyIntent).not.toHaveBeenCalled();
expect(deps.guardrail.validateMandateAmount).not.toHaveBeenCalled();
expect(deps.mandateStore.store).not.toHaveBeenCalled();
expect(deps.feeCollector.calculateFee).not.toHaveBeenCalled();
});
});
// ──────────────────────────────────────────────
// 5. Mandate chain verification throws
// ──────────────────────────────────────────────
describe('mandate chain verification throws', () => {
it('returns failure with wrapped error message when verifyMandateChain throws an Error', async () => {
(deps.verifier.verifyMandateChain as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('JWK fetch timeout'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toEqual([
'Mandate chain verification failed: JWK fetch timeout',
]);
});
it('handles non-Error thrown values by converting to string', async () => {
(deps.verifier.verifyMandateChain as ReturnType<typeof vi.fn>).mockRejectedValue(
'raw string error',
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toEqual([
'Mandate chain verification failed: raw string error',
]);
});
});
// ──────────────────────────────────────────────
// 6. Guardrail fails — intent re-verify fails
// ──────────────────────────────────────────────
describe('guardrail fails — intent re-verification', () => {
it('returns failure when verifyIntent returns invalid after chain passed', async () => {
(deps.verifier.verifyIntent as ReturnType<typeof vi.fn>).mockResolvedValue({
valid: false,
mandate: null,
error: 'Mandate expired',
});
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0]).toContain('Intent mandate re-verification failed');
expect(result.errors![0]).toContain('Mandate expired');
});
it('returns failure when verifyIntent returns valid but no mandate object', async () => {
(deps.verifier.verifyIntent as ReturnType<typeof vi.fn>).mockResolvedValue({
valid: false,
mandate: null,
});
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0]).toContain('Intent mandate re-verification failed');
expect(result.errors![0]).toContain('unknown');
});
it('returns failure when verifyIntent throws an exception', async () => {
// First call to verifyIntent (guardrail step) throws
(deps.verifier.verifyIntent as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Key import failed'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0]).toContain('Intent mandate re-verification threw');
expect(result.errors![0]).toContain('Key import failed');
});
it('does not call validateMandateAmount when intentMandate is null', async () => {
(deps.verifier.verifyIntent as ReturnType<typeof vi.fn>).mockResolvedValue({
valid: false,
mandate: null,
error: 'invalid signature',
});
await executeCheckout(params, deps);
expect(deps.guardrail.validateMandateAmount).not.toHaveBeenCalled();
});
});
// ──────────────────────────────────────────────
// 7. Guardrail fails — amount exceeds max
// ──────────────────────────────────────────────
describe('guardrail fails — amount exceeds max', () => {
it('returns failure with guardrail error when validateMandateAmount returns false', async () => {
(deps.guardrail.validateMandateAmount as ReturnType<typeof vi.fn>).mockReturnValue(false);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0]).toContain('Guardrail');
expect(result.errors![0]).toContain('checkout total (2170)');
expect(result.errors![0]).toContain('max_amount (10000)');
});
it('does not proceed to mandate storage or fee collection', async () => {
(deps.guardrail.validateMandateAmount as ReturnType<typeof vi.fn>).mockReturnValue(false);
await executeCheckout(params, deps);
expect(deps.mandateStore.store).not.toHaveBeenCalled();
expect(deps.feeCollector.calculateFee).not.toHaveBeenCalled();
});
});
// ──────────────────────────────────────────────
// 8. Mandate storage fails (non-fatal)
// ──────────────────────────────────────────────
describe('mandate storage fails', () => {
it('collects error but continues checkout when mandateStore.store throws', async () => {
(deps.mandateStore.store as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('DynamoDB write failed'),
);
const result = await executeCheckout(params, deps);
// Checkout should still succeed because mandate storage is non-fatal
expect(result.success).toBe(true);
expect(result.order).toBeDefined();
expect(result.fee).toBeDefined();
expect(result.errors).toBeDefined();
expect(result.errors).toContain('Failed to store mandates: DynamoDB write failed');
});
it('collects error when verifier decode throws during storage step', async () => {
// verifyIntent first call (guardrail) succeeds, second call (storage) fails
const intentMandate = createMockIntentMandate();
(deps.verifier.verifyIntent as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ valid: true, mandate: intentMandate }) // guardrail step
.mockRejectedValueOnce(new Error('Decode failed on second call')); // storage step
const result = await executeCheckout(params, deps);
// Should still succeed with an error collected
expect(result.success).toBe(true);
expect(result.errors).toBeDefined();
expect(result.errors!.some((e: string) => e.includes('Failed to store mandates'))).toBe(true);
});
});
// ──────────────────────────────────────────────
// 9. StorefrontAPI checkout URL fails (non-fatal)
// ──────────────────────────────────────────────
describe('StorefrontAPI checkout URL fails', () => {
it('collects error and continues when createCheckoutUrl throws', async () => {
(deps.storefrontAPI!.createCheckoutUrl as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Storefront API 503'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.order).toBeDefined();
expect(result.fee).toBeDefined();
expect(result.errors).toBeDefined();
expect(result.errors).toContain('Failed to create checkout URL: Storefront API 503');
// checkout_url should be undefined since creation failed and no fallback for present API
expect(result.checkout_url).toBeUndefined();
});
});
// ──────────────────────────────────────────────
// 10. No StorefrontAPI (null)
// ──────────────────────────────────────────────
describe('no StorefrontAPI (null)', () => {
it('uses simulated checkout URL when storefrontAPI is null', async () => {
deps = createMockDeps({ storefrontAPI: null });
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.checkout_url).toBe(
'https://checkout.shopify.com/sessions/checkout-123',
);
});
it('uses correct checkout_id in simulated URL', async () => {
deps = createMockDeps({ storefrontAPI: null });
params = createMockParams({ checkout_id: 'my-special-checkout' });
// Need the session to match the new checkout_id
const session = createMockSession({ id: 'my-special-checkout' });
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(session);
const result = await executeCheckout(params, deps);
expect(result.checkout_url).toBe(
'https://checkout.shopify.com/sessions/my-special-checkout',
);
});
});
// ──────────────────────────────────────────────
// 11. StorefrontAPI present but no continue_url
// ──────────────────────────────────────────────
describe('StorefrontAPI present but no continue_url', () => {
it('uses simulated URL when session has no continue_url', async () => {
const sessionNoContinueUrl = createMockSession({ continue_url: undefined });
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(sessionNoContinueUrl);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.checkout_url).toBe(
'https://checkout.shopify.com/sessions/checkout-123',
);
// StorefrontAPI should NOT have been called
expect(deps.storefrontAPI!.createCheckoutUrl).not.toHaveBeenCalled();
});
it('uses simulated URL when continue_url is empty string', async () => {
const sessionEmptyUrl = createMockSession({ continue_url: '' });
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(sessionEmptyUrl);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
// Empty string is falsy, so simulated URL is used
expect(result.checkout_url).toBe(
'https://checkout.shopify.com/sessions/checkout-123',
);
expect(deps.storefrontAPI!.createCheckoutUrl).not.toHaveBeenCalled();
});
});
// ──────────────────────────────────────────────
// 12. Fee collection fails
// ──────────────────────────────────────────────
describe('fee collection fails', () => {
it('returns failure when calculateFee throws', async () => {
(deps.feeCollector.calculateFee as ReturnType<typeof vi.fn>).mockImplementation(() => {
throw new Error('Invalid gross amount');
});
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors!.some((e: string) =>
e.includes('Fee collection or order creation failed: Invalid gross amount'),
)).toBe(true);
expect(result.order).toBeUndefined();
// checkout_url should still be present since step 6 succeeded
expect(result.checkout_url).toBeDefined();
});
it('returns failure when collect throws', async () => {
(deps.feeCollector.collect as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Payment gateway timeout'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors!.some((e: string) =>
e.includes('Fee collection or order creation failed: Payment gateway timeout'),
)).toBe(true);
expect(result.order).toBeUndefined();
});
it('includes checkout_url in result even when fee collection fails', async () => {
(deps.feeCollector.collect as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Payment failed'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.checkout_url).toBe('https://shop.myshopify.com/checkout/abc');
});
});
// ──────────────────────────────────────────────
// 13. Success with non-fatal errors
// ──────────────────────────────────────────────
describe('success with non-fatal errors', () => {
it('returns success with errors when mandateStore.store fails but everything else succeeds', async () => {
(deps.mandateStore.store as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('DynamoDB throttled'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.order).toBeDefined();
expect(result.order!.status).toBe('confirmed');
expect(result.fee).toBeDefined();
expect(result.fee!.status).toBe('collected');
expect(result.checkout_url).toBe('https://shop.myshopify.com/checkout/abc');
expect(result.errors).toBeDefined();
expect(result.errors).toHaveLength(1);
expect(result.errors![0]).toBe('Failed to store mandates: DynamoDB throttled');
});
it('returns success with errors when checkout URL creation fails but everything else succeeds', async () => {
(deps.storefrontAPI!.createCheckoutUrl as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('GraphQL error'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.order).toBeDefined();
expect(result.fee).toBeDefined();
expect(result.errors).toBeDefined();
expect(result.errors).toContain('Failed to create checkout URL: GraphQL error');
});
it('returns success with multiple non-fatal errors accumulated', async () => {
// Both mandate storage and checkout URL creation fail
(deps.mandateStore.store as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('DynamoDB write capacity exceeded'),
);
(deps.storefrontAPI!.createCheckoutUrl as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Storefront API rate limited'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(result.order).toBeDefined();
expect(result.fee).toBeDefined();
expect(result.errors).toBeDefined();
expect(result.errors!.length).toBe(2);
expect(result.errors!.some((e: string) => e.includes('Failed to store mandates'))).toBe(true);
expect(result.errors!.some((e: string) => e.includes('Failed to create checkout URL'))).toBe(true);
});
});
// ──────────────────────────────────────────────
// Additional edge cases
// ──────────────────────────────────────────────
describe('edge cases', () => {
it('assigns correct checkout_id and unique order_id to fee record', async () => {
const pendingFee = createMockFeeRecord({ status: 'pending', checkout_id: '', order_id: '' });
(deps.feeCollector.calculateFee as ReturnType<typeof vi.fn>).mockReturnValue(pendingFee);
await executeCheckout(params, deps);
// The function mutates pendingFee before passing to collect
expect(pendingFee.checkout_id).toBe('checkout-123');
expect(pendingFee.order_id).toMatch(/^order_/);
expect(deps.feeCollector.collect).toHaveBeenCalledWith(pendingFee);
});
it('order id in fee record matches the order id in the returned order', async () => {
let capturedFeeRecord: FeeRecord | undefined;
const pendingFee = createMockFeeRecord({ status: 'pending', checkout_id: '', order_id: '' });
(deps.feeCollector.calculateFee as ReturnType<typeof vi.fn>).mockReturnValue(pendingFee);
(deps.feeCollector.collect as ReturnType<typeof vi.fn>).mockImplementation(
async (fee: FeeRecord) => {
capturedFeeRecord = fee;
return { ...fee, status: 'collected' as const };
},
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(true);
expect(capturedFeeRecord).toBeDefined();
expect(result.order!.id).toBe(capturedFeeRecord!.order_id);
});
it('stores mandates for all three types when verifiers return valid mandates', async () => {
await executeCheckout(params, deps);
const storeCalls = (deps.mandateStore.store as ReturnType<typeof vi.fn>).mock.calls;
expect(storeCalls).toHaveLength(3);
const storedTypes = storeCalls.map(
(call: [Mandate]) => call[0].type,
);
expect(storedTypes).toContain('intent');
expect(storedTypes).toContain('cart');
expect(storedTypes).toContain('payment');
});
it('handles fee collection failure combined with mandate storage failure', async () => {
(deps.mandateStore.store as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Store failed'),
);
(deps.feeCollector.collect as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error('Collect failed'),
);
const result = await executeCheckout(params, deps);
expect(result.success).toBe(false);
expect(result.errors).toBeDefined();
expect(result.errors!.some((e: string) => e.includes('Failed to store mandates'))).toBe(true);
expect(result.errors!.some((e: string) => e.includes('Fee collection or order creation failed'))).toBe(true);
expect(result.order).toBeUndefined();
});
it('passes the correct currency from session totals to calculateFee', async () => {
const jpySession = createMockSession({
totals: {
subtotal: 200000,
tax: 20000,
shipping: 0,
discount: 0,
fee: 1000,
total: 221000,
currency: 'JPY',
},
});
(deps.sessionManager.get as ReturnType<typeof vi.fn>).mockReturnValue(jpySession);
await executeCheckout(params, deps);
expect(deps.feeCollector.calculateFee).toHaveBeenCalledWith(221000, 'JPY');
});
});
});