/**
* execute_checkout Tool — AP2 Mandate Chain Checkout Execution
*
* The most critical tool in the Shopify Agentic MCP Gateway.
* Completes autonomous purchases by verifying the full AP2 mandate chain
* (intent -> cart -> payment), running guardrail checks, collecting fees,
* and building a confirmed order from the checkout session.
*/
import { randomUUID } from 'node:crypto';
import type {
Order,
FeeRecord,
Mandate,
IntentPayload,
} from '../types.js';
import type { ISessionManager, IMandateStore } from '../interfaces.js';
import type { MandateVerifier } from '../ap2/verifier.js';
import type { Guardrail } from '../middleware/guardrail.js';
import type { FeeCollector } from '../middleware/fee-collector.js';
import type { StorefrontAPI } from '../shopify/storefront.js';
import { logger } from '../utils/logger.js';
// ─── Parameter & Result Types ───
export interface ExecuteParams {
checkout_id: string;
intent_mandate: string;
cart_mandate: string;
payment_mandate: string;
}
export interface ExecuteResult {
success: boolean;
order?: Order;
checkout_url?: string;
fee?: FeeRecord;
errors?: string[];
}
// ─── Dependencies ───
export interface ExecuteCheckoutDeps {
sessionManager: ISessionManager;
verifier: MandateVerifier;
mandateStore: IMandateStore;
guardrail: Guardrail;
feeCollector: FeeCollector;
storefrontAPI: StorefrontAPI | null;
}
// ─── Main Function ───
/**
* Execute a full checkout using the AP2 mandate chain.
*
* Steps:
* 1. Retrieve the checkout session by ID.
* 2. Verify the session is in "ready_for_complete" status.
* 3. Verify the full mandate chain (intent -> cart -> payment).
* 4. Run guardrail validation (intent max_amount covers checkout total).
* 5. Store all 3 mandates in the MandateStore.
* 6. Create a checkout URL via the Storefront API (if available).
* 7. Calculate and collect the platform fee.
* 8. Build the confirmed Order object.
* 9. Return success with order, checkout_url, and fee record.
*
* Each step's failure is handled gracefully — errors are collected and
* returned without throwing, so the caller gets a structured result.
*/
export async function executeCheckout(
params: ExecuteParams,
deps: ExecuteCheckoutDeps,
): Promise<ExecuteResult> {
const errors: string[] = [];
logger.info('execute_checkout: starting', { checkout_id: params.checkout_id });
// ── Step 1: Retrieve the checkout session ──
const session = await deps.sessionManager.get(params.checkout_id);
if (!session) {
logger.error('execute_checkout: session not found', { checkout_id: params.checkout_id });
return {
success: false,
errors: [`Checkout session not found: ${params.checkout_id}`],
};
}
// ── Step 2: Verify session is ready_for_complete ──
if (session.status !== 'ready_for_complete') {
logger.error('execute_checkout: session not ready', {
checkout_id: params.checkout_id,
status: session.status,
});
return {
success: false,
errors: [
`Checkout session is in "${session.status}" status, expected "ready_for_complete". ` +
`Resolve all requirements before executing checkout.`,
],
};
}
// ── Step 3: Verify the full mandate chain ──
let chainResult: { valid: boolean; errors: string[] };
try {
chainResult = await deps.verifier.verifyMandateChain(
params.intent_mandate,
params.cart_mandate,
params.payment_mandate,
);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error('execute_checkout: mandate chain verification threw', { error: message });
return {
success: false,
errors: [`Mandate chain verification failed: ${message}`],
};
}
if (!chainResult.valid) {
logger.warn('execute_checkout: mandate chain invalid', { errors: chainResult.errors });
return {
success: false,
errors: chainResult.errors,
};
}
// ── Step 4: Guardrail — validate intent mandate covers checkout total ──
// We need to decode the intent mandate to run the guardrail check.
// The verifier already validated the chain; now re-verify intent to get the payload.
let intentMandate: Mandate | null = null;
try {
const intentResult = await deps.verifier.verifyIntent(params.intent_mandate);
if (!intentResult.valid || !intentResult.mandate) {
// Should not happen since chain passed, but handle defensively
errors.push(`Intent mandate re-verification failed: ${intentResult.error ?? 'unknown'}`);
} else {
intentMandate = intentResult.mandate;
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
errors.push(`Intent mandate re-verification threw: ${message}`);
}
if (intentMandate) {
const mandateValid = deps.guardrail.validateMandateAmount(
intentMandate,
session.totals.total,
);
if (!mandateValid) {
const intentPayload = intentMandate.payload as IntentPayload;
errors.push(
`Guardrail: checkout total (${session.totals.total}) exceeds intent mandate ` +
`max_amount (${intentPayload.max_amount}) or mandate is not active/expired`,
);
}
}
// If guardrail errors accumulated, bail out
if (errors.length > 0) {
logger.warn('execute_checkout: guardrail validation failed', { errors });
return {
success: false,
errors,
};
}
// ── Step 5: Store all 3 mandates ──
try {
// Decode each mandate for storage
const [intentRes, cartRes, paymentRes] = await Promise.all([
deps.verifier.verifyIntent(params.intent_mandate),
deps.verifier.verifyCart(params.cart_mandate),
deps.verifier.verifyPayment(params.payment_mandate),
]);
const mandatesToStore: Mandate[] = [];
if (intentRes.mandate) mandatesToStore.push(intentRes.mandate);
if (cartRes.mandate) mandatesToStore.push(cartRes.mandate);
if (paymentRes.mandate) mandatesToStore.push(paymentRes.mandate);
await Promise.all(mandatesToStore.map((m) => deps.mandateStore.store(m)));
logger.info('execute_checkout: mandates stored', {
count: mandatesToStore.length,
ids: mandatesToStore.map((m) => m.id),
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
errors.push(`Failed to store mandates: ${message}`);
logger.error('execute_checkout: mandate storage failed', { error: message });
}
// ── Step 6: Create checkout URL via Storefront API ──
let checkoutUrl: string | undefined;
if (deps.storefrontAPI && session.continue_url) {
try {
// Use the cart ID stored as continue_url on the session
checkoutUrl = await deps.storefrontAPI.createCheckoutUrl(session.continue_url);
logger.info('execute_checkout: checkout URL created', { checkout_url: checkoutUrl });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
errors.push(`Failed to create checkout URL: ${message}`);
logger.warn('execute_checkout: checkout URL creation failed', { error: message });
}
} else {
// Generate a simulated checkout URL when Storefront API is not available
checkoutUrl = `https://checkout.shopify.com/sessions/${params.checkout_id}`;
logger.info('execute_checkout: simulated checkout URL generated', { checkout_url: checkoutUrl });
}
// ── Step 7: Calculate and collect platform fee ──
let feeRecord: FeeRecord | undefined;
try {
const pendingFee = deps.feeCollector.calculateFee(
session.totals.total,
session.totals.currency,
);
// Attach checkout and order context before collecting
const orderId = `order_${randomUUID()}`;
pendingFee.checkout_id = params.checkout_id;
pendingFee.order_id = orderId;
feeRecord = await deps.feeCollector.collect(pendingFee);
logger.info('execute_checkout: fee collected', {
fee_amount: feeRecord.fee_amount,
order_id: orderId,
});
// ── Step 8: Build the confirmed Order ──
const now = new Date().toISOString();
const order: Order = {
id: orderId,
checkout_id: params.checkout_id,
status: 'confirmed',
line_items: session.line_items,
totals: session.totals,
fulfillment: {
status: 'unfulfilled',
},
created_at: now,
updated_at: now,
};
logger.info('execute_checkout: order confirmed', {
order_id: order.id,
total: order.totals.total,
currency: order.totals.currency,
});
// ── Step 9: Return success ──
// Include any non-fatal errors that occurred during optional steps
const result: ExecuteResult = {
success: true,
order,
checkout_url: checkoutUrl,
fee: feeRecord,
};
if (errors.length > 0) {
result.errors = errors;
}
return result;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
errors.push(`Fee collection or order creation failed: ${message}`);
logger.error('execute_checkout: fee/order step failed', { error: message });
return {
success: false,
checkout_url: checkoutUrl,
errors,
};
}
}