/**
 * Cart checkout workflow.
 *
 * Single DB transaction:
 *   1. SELECT … FOR UPDATE on each deal in the cart.
 *   2. Validate: active, not expired, stock ≥ qty, qty ≤ maxPerUser, price unchanged.
 *   3. Decrement deal.quantitySold per line.
 *   4. Insert purchases row per line.
 *   5. Clear cart_items for user.
 *   6. Insert outbox rows per purchase (within the same tx).
 *   COMMIT.
 *   7. Post-tx Phase 1: placeHold per purchase (manual-capture PI).
 *   8. Post-tx Phase 2: captureHold per purchase.
 *   9. Enqueue fulfillment work after every capture is durable.
 *
 * All DB access goes through the query layer (query-layer exclusivity rule).
 */

import { eq, sql } from 'drizzle-orm';
import type { TxDrizzleClient } from '@/server/db/client.js';
import { paymentMethods } from '@/server/db/schema.js';
import { getCartItemsForCheckout, clampQty } from '@/server/db/queries/cart.js';
import { checkAndMarkSoldOut } from '@/server/workflows/purchase.js';
import { enqueueOutbox } from '@/server/queues/outbox-producer.js';
import { decide } from '@/server/domain/cart-checkout/machine.js';
import { applyEffects } from '@/server/domain/cart-checkout/apply-effects.js';
import type { ApplyEffectsResult } from '@/server/domain/cart-checkout/apply-effects.js';
import type { ValidatedCartLine, CartLineState } from '@/server/domain/cart-checkout/events.js';
import type { CartCheckoutEffect } from '@/server/domain/cart-checkout/effects.js';
import type { PaymentProvider } from '@/server/payments/provider.js';
import type { MultidealEnv } from '@/server/env.js';
import { loadVendorPaymentContext } from '@/server/db/queries/purchases.js';
import { failOrderAndRestoreStock } from '@/server/workflows/fail-order.js';
import * as userQueries from '@/server/db/queries/users.js';
import { captureCaught } from '@/server/observability/capture.server.js';
import {
  reservePromoOrchestration,
  releasePromoReservation,
  buildPromoClaim,
  computePromoAwareSplit,
  isPromoSplitValid,
  STRIPE_MIN_CHARGE_AGOROT,
} from '@/server/promo/service.js';
import { resolveCartForUser } from '@/server/promo/cart-resolver.js';
import { promoReservations, paymentExpectedContracts } from '@/server/db/schema.js';
import { listUserMemberships } from '@/server/db/queries/club.js';
import { getQtyTiersForSkus } from '@/server/db/queries/sku-qty-tiers.js';
import { formatAgorotPlain } from '@/lib/money.js';
import { getPlatformFeePct } from '@/server/payments/platform-fee.js';
import {
  persistCartCaptureIntent,
  recordCartCapture,
  createCartCompensation,
  createCartFulfillmentWork,
} from '@/server/workflows/cart-checkout-compensation.js';
// redeemCreditForOrder is dynamically imported at call site — kept out of cold-start graph.

// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------

export type StaleItem = {
  dealId: string;
  reason: 'expired' | 'inactive' | 'sold_out' | 'qty_clamped';
  availableQty?: number;
};

export class CartCheckoutStaleError extends Error {
  constructor(
    public readonly removed: StaleItem[],
    public readonly updated: StaleItem[],
  ) {
    super('Cart contains stale items');
    this.name = 'CartCheckoutStaleError';
  }
}

export class CartCheckoutPaymentError extends Error {
  constructor(
    public readonly reason: string,
    message: string,
  ) {
    super(message);
    this.name = 'CartCheckoutPaymentError';
  }
}

export class CartEmptyError extends Error {
  constructor() {
    super('Cart is empty');
    this.name = 'CartEmptyError';
  }
}

export class CartCheckoutSelfDealError extends Error {
  constructor() {
    super('Vendor owners cannot purchase their own deals');
    this.name = 'CartCheckoutSelfDealError';
  }
}

export class CartCheckoutPromoError extends Error {
  constructor(public readonly reason: string) {
    super(`Promo rejected: ${reason}`);
    this.name = 'CartCheckoutPromoError';
  }
}

// ---------------------------------------------------------------------------
// Deps
// ---------------------------------------------------------------------------

export interface CartCheckoutDeps {
  db: TxDrizzleClient;
  storage: {
    bucket: R2Bucket;
    publicBase: string;
    uploadQr(purchaseId: string, token: string): Promise<string>;
  };
  qrSecret: string;
  payments: PaymentProvider;
  env: MultidealEnv;
}

export interface CartCheckoutInput {
  userId: string;
  paymentMethodId?: string;
  idempotencyKey: string;
  promoCode?: string;
  /** When true, skip auto-applying referral credit for this order. */
  holdCredit?: boolean;
}

export interface CartCheckoutResult {
  orderLineIds: string[];
}

// ---------------------------------------------------------------------------
// Workflow
// ---------------------------------------------------------------------------

export async function cartCheckout(
  deps: CartCheckoutDeps,
  input: CartCheckoutInput,
): Promise<CartCheckoutResult> {
  const { db } = deps;
  const checkoutKey = `${input.userId}:${input.idempotencyKey}`;

  // ─── Pre-tx: validate payment method BEFORE any fulfillment side-effect ───
  // A missing/foreign/declined method must fail here, while the cart is still
  // intact — after applyEffects the cart is cleared and vouchers are issued,
  // and no path restores them (SP-033).
  if (!input.paymentMethodId) {
    throw new CartCheckoutPaymentError(
      'NO_PAYMENT_METHOD',
      'Saved payment method required for cart checkout',
    );
  }

  const [pmRow] = await db
    .select({ token: paymentMethods.token })
    .from(paymentMethods)
    .where(
      sql`${paymentMethods.id} = ${input.paymentMethodId} AND ${paymentMethods.userId} = ${input.userId}`,
    )
    .limit(1);
  if (!pmRow) {
    throw new CartCheckoutPaymentError('PAYMENT_METHOD_NOT_FOUND', 'Payment method not found');
  }

  const cardCheck = await deps.payments.checkCard({
    paymentMethodId: pmRow.token,
  });
  if (!cardCheck.ok) {
    throw new CartCheckoutPaymentError(cardCheck.code, cardCheck.message);
  }

  // ─── Transaction: lock, validate, decide, applyEffects ────────────────────
  const outboxIds: string[] = [];
  const orderLineIds: string[] = [];
  const stockDecrementedDealIds: string[] = [];
  // Hoisted outside the tx closure so placeHold phase can iterate applied.orders.
  let applied: ApplyEffectsResult = { orders: [], outboxIds: [] };
  // Maps orderId → explicit application_fee_amount (agorot) to use in placeHold.
  // Populated inside the tx from patched effects; consumed post-tx in placeHold call.
  const orderIdToFeeAgorot = new Map<string, number>();
  const promoFeeOrderIds = new Set<string>();
  // Maps orderId → credit (agorot) applied to that order — reduces placeHold total.
  const creditAppliedByOrderId = new Map<string, number>();
  // Promo quota was reserved inside the tx — released if payment never lands.
  let reservedPromoCodeId: string | null = null;
  const promoReservationByOrderId = new Map<string, string>();

  await db.transaction(async (tx) => {
    // Lock deal rows and fetch cart items
    const cartLines = await getCartItemsForCheckout(tx, input.userId);

    if (cartLines.length === 0) {
      throw new CartEmptyError();
    }

    if (cartLines.some((line) => line.vendorOwnerUserId === input.userId)) {
      throw new CartCheckoutSelfDealError();
    }

    const tierMap = await getQtyTiersForSkus(
      tx,
      cartLines.map((l) => l.dealSkuId).filter((id): id is string => id !== null),
    );

    const now = new Date();

    // Pre-decide validation per line — classify each into a lineState.
    const validated: ValidatedCartLine[] = cartLines.map((line) => {
      const stock = Math.max(0, line.stockRemaining ?? 0);
      const isExpired =
        (line.windowEnd != null && line.windowEnd < now) || line.dealState === 'EXPIRED';
      const clampedQty = clampQty(line.qty, line.maxPerUser, stock);
      const expiresAt = line.windowEnd ?? new Date(now.getTime() + 180 * 24 * 60 * 60 * 1000);

      let lineState: CartLineState;
      if (isExpired) lineState = 'EXPIRED';
      else if (line.dealState !== 'ACTIVE') lineState = 'INACTIVE';
      else if (stock <= 0 || clampedQty <= 0) lineState = 'SOLD_OUT';
      else if (clampedQty < line.qty) lineState = 'QTY_CLAMPED';
      else lineState = 'OK';

      if (!line.dealSkuId) lineState = 'SOLD_OUT';

      return {
        dealId: line.dealId,
        dealSkuId: line.dealSkuId,
        vendorId: line.vendorId,
        dealTitle: line.dealTitle,
        qty: line.qty,
        clampedQty,
        availableStock: stock,
        dealState: line.dealState,
        isExpired,
        discountedPrice: line.discountedPrice,
        commissionRate: line.commissionRate,
        qtyTiers: line.dealSkuId ? (tierMap.get(line.dealSkuId) ?? []) : [],
        windowEnd: line.windowEnd ?? null,
        expiresAt,
        lineState,
      };
    });

    const decideResult = decide(
      { state: 'BATCH_VALIDATING' },
      {
        kind: 'lines_validated',
        userId: input.userId,
        paymentMethodId: input.paymentMethodId,
        idempotencyKey: input.idempotencyKey,
        lines: validated,
        at: now,
      },
    );

    if (!decideResult.ok) {
      if (decideResult.error === 'EMPTY_CART') {
        throw new CartEmptyError();
      }
      // STALE_ITEMS
      throw new CartCheckoutStaleError(
        decideResult.stale?.removed ?? [],
        decideResult.stale?.updated ?? [],
      );
    }

    // ── Promo orchestration (atomic with cart FOR UPDATE locks) ──────────────
    // Runs inside the transaction so redemption count reads are consistent.
    type PromoBreakdownOk = Extract<
      Awaited<ReturnType<typeof reservePromoOrchestration>>,
      { ok: true }
    >;
    let promoBreakdown: PromoBreakdownOk | null = null;
    const PLATFORM_FEE_PCT = getPlatformFeePct(deps.env);
    const perDealDiscount = new Map<string, number>();
    if (input.promoCode) {
      const resolvedCart = await resolveCartForUser(tx, input.userId);
      const memberships = await listUserMemberships(tx, input.userId);
      const isClubMember = memberships.length > 0;

      const promoResult = await reservePromoOrchestration(
        tx,
        input.promoCode,
        { id: input.userId, isClubMember },
        resolvedCart,
        PLATFORM_FEE_PCT,
      );

      if (!promoResult.ok) {
        throw new CartCheckoutPromoError(promoResult.reason);
      }

      promoBreakdown = promoResult;
      reservedPromoCodeId = promoResult.code.id;

      // Build dealId → discountAgorot map from per-line breakdown.
      // Uses dealId key (1:1 per user in cart) to avoid double-counting multi-line vendors.
      const lineIdToDealId = new Map<string, string>();
      for (const rl of resolvedCart.lines) {
        lineIdToDealId.set(rl.lineId, rl.dealId);
      }
      for (const pl of promoBreakdown.breakdown.perLine) {
        const dealId = lineIdToDealId.get(pl.lineId);
        if (dealId) {
          perDealDiscount.set(dealId, (perDealDiscount.get(dealId) ?? 0) + pl.amountAgorot);
        }
      }
    }

    // ── Apply promo-aware pricing to reserve-line effects ────────────────────

    // Mutate reserve-line effects to inject promo-aware amounts and promoClaim.
    const patchedEffects: CartCheckoutEffect[] = decideResult.effects.map((effect) => {
      if (effect.kind !== 'reserve-line') return effect;

      const lineDiscountAgorot = perDealDiscount.get(effect.dealId) ?? 0;
      if (lineDiscountAgorot === 0 && !promoBreakdown) return effect;

      // Recalculate amounts in agorot for precision, then convert to numeric(10,2) string.
      const lineSubtotalAgorot = Math.round(parseFloat(effect.amountPaid) * 100);
      const funder = promoBreakdown?.code.funder ?? 'platform';
      const { amountPaidAgorot, commissionAgorot, vendorAgorot } = computePromoAwareSplit(
        lineSubtotalAgorot,
        lineDiscountAgorot,
        funder,
        PLATFORM_FEE_PCT,
      );

      if (
        lineDiscountAgorot > 0 &&
        !isPromoSplitValid(amountPaidAgorot, commissionAgorot, vendorAgorot)
      ) {
        throw new CartCheckoutPromoError(
          funder === 'platform' ? 'PLATFORM_FUND_INSUFFICIENT' : 'VENDOR_FUND_INSUFFICIENT',
        );
      }

      if (lineDiscountAgorot === 0) {
        return effect;
      }

      const promoClaim =
        lineDiscountAgorot > 0 && promoBreakdown
          ? buildPromoClaim(promoBreakdown.code, lineDiscountAgorot, input.userId)
          : undefined;

      return {
        ...effect,
        amountPaid: formatAgorotPlain(amountPaidAgorot),
        commissionAmount: formatAgorotPlain(commissionAgorot),
        vendorAmount: formatAgorotPlain(vendorAgorot),
        platformFeeAmountAgorot: commissionAgorot,
        promoClaim,
      };
    });

    applied = await applyEffects(
      {
        tx: tx,
        userId: input.userId,
        paymentMethodId: input.paymentMethodId,
        cartSessionId: input.idempotencyKey,
      },
      patchedEffects,
    );

    if (promoBreakdown) {
      let quotaOwnerAssigned = false;
      for (const created of applied.orders) {
        const discount = promoBreakdown.breakdown.perLine
          .filter((line) => line.vendorId === created.vendorId)
          .reduce((sum, line) => sum + line.amountAgorot, 0);
        if (discount <= 0) continue;
        const [reservation] = await tx
          .insert(promoReservations)
          .values({
            promoCodeId: promoBreakdown.code.id,
            userId: input.userId,
            purchaseId: created.firstOrderLineId,
            orderLineId: created.firstOrderLineId,
            vendorId: created.vendorId,
            checkoutScope: checkoutKey,
            currency: 'ILS',
            snapshot: {
              code: promoBreakdown.code,
              breakdown: promoBreakdown.breakdown,
              currency: 'ILS',
            },
            quotaOwner: !quotaOwnerAssigned,
            discountAgorot: discount,
            funder: promoBreakdown.code.funder,
          })
          .returning({ id: promoReservations.id });
        quotaOwnerAssigned = true;
        if (reservation) promoReservationByOrderId.set(created.orderId, reservation.id);
      }
    }

    // Build dealId → commissionAgorot from all reserve-line effects (all items, not just promo).
    // Used post-tx to pass correct application_fee_amount to placeHold.
    const dealIdToFeeAgorot = new Map<string, number>();
    for (const effect of patchedEffects) {
      if (effect.kind === 'reserve-line') {
        const feeAgorot = Math.round(parseFloat(effect.commissionAmount) * 100);
        dealIdToFeeAgorot.set(effect.dealId, feeAgorot);
      }
    }

    for (const orderCreated of applied.orders) {
      for (const lid of orderCreated.orderLineIds) {
        orderLineIds.push(lid);
      }
      for (const did of orderCreated.dealIds) stockDecrementedDealIds.push(did);

      // Sum per-deal fee overrides across all lines in this vendor order.
      let vendorGroupFeeAgorot = 0;
      for (const did of orderCreated.dealIds) {
        vendorGroupFeeAgorot += dealIdToFeeAgorot.get(did) ?? 0;
      }
      if (vendorGroupFeeAgorot > 0) {
        orderIdToFeeAgorot.set(orderCreated.orderId, vendorGroupFeeAgorot);
      }

      // Mark order as promo-affected when any of its deals had a discount.
      if (promoBreakdown && orderCreated.dealIds.some((d) => (perDealDiscount.get(d) ?? 0) > 0)) {
        promoFeeOrderIds.add(orderCreated.orderId);
      }
    }
    for (const id of applied.outboxIds) {
      outboxIds.push(id);
    }

    // ── Referral wallet-credit redemption (spec §8) ──────────────────────────
    // Treat applied credit EXACTLY like a platform-funded promo discount:
    // subtract from the amount charged, reduce the platform take by the same
    // amount, and leave the vendor payout UNCHANGED. Credit can never reduce
    // the vendor's cut. Guests (no userId / no wallet) are skipped.
    //
    // Multi-line carts produce N purchases each with its own PaymentIntent, but
    // the wallet ledger records ONE redemption keyed to a single purchaseId.
    // We anchor the whole applied amount on the first purchase and cap it at
    // that purchase's platform commission so the platform take cannot go
    // negative (vendor payout = subtotal − commission stays intact). Any wallet
    // balance beyond the anchor's commission is intentionally left unspent for
    // this order — matching the platform-funded-discount invariant (you cannot
    // refund more than the platform's own take).
    //
    // Defensive: a referral failure must NOT break checkout — wrap + swallow.
    // Skipped entirely when the user explicitly opted out via holdCredit flag.
    if (input.userId && applied.orders.length > 0 && !input.holdCredit) {
      try {
        // Anchor the credit on the first purchase of the first vendor order.
        // Matches the pre-T2 behaviour where the first reservedPurchase was the anchor.
        const firstOrder = applied.orders[0]!;
        // Anchor: order total in agorot (vendor payout stays unchanged; credit
        // reduces platform take only).
        const anchorAmountAgorot = Number(firstOrder.total);
        // Anchor's platform commission: patched fee override if present, else
        // the default 10% of the order total.
        const anchorCommissionAgorot =
          dealIdToFeeAgorot.get(firstOrder.firstDealId) ??
          Math.floor((anchorAmountAgorot * PLATFORM_FEE_PCT) / 100);
        // Cap redemption at the anchor's commission so platform take ≥ 0.
        const redeemCapAgorot = Math.max(
          0,
          Math.min(anchorCommissionAgorot, anchorAmountAgorot - STRIPE_MIN_CHARGE_AGOROT),
        );

        if (redeemCapAgorot > 0) {
          const { redeemCreditForOrder } = await import('@/server/referrals/service.js');
          const creditApplied = await redeemCreditForOrder(tx, {
            userId: input.userId,
            orderTotalAgorot: redeemCapAgorot,
            purchaseId: firstOrder.firstOrderLineId,
          });

          if (creditApplied > 0) {
            const newCommissionAgorot = anchorCommissionAgorot - creditApplied;
            // Update order-level fee override consumed post-tx by placeHold.
            orderIdToFeeAgorot.set(firstOrder.orderId, newCommissionAgorot);
            // Track credit so placeHold subtracts it from the order total.
            creditAppliedByOrderId.set(firstOrder.orderId, creditApplied);
          }
        }
      } catch (err) {
        captureCaught(err, {
          scope: 'cart-checkout.redeemCredit',
          severity: 'warning',
          extra: { userId: input.userId },
        });
      }
    }
  });

  for (const dealId of [...new Set(stockDecrementedDealIds)]) {
    try {
      await checkAndMarkSoldOut(db, dealId);
    } catch (e) {
      captureCaught(e, {
        scope: 'workflows.cart-checkout.soldOutCheck',
        severity: 'warning',
        extra: { dealId },
      });
    }
  }

  const failAllOrders = async () => {
    let cleanupError: unknown;
    for (const orderCreated of applied.orders) {
      try {
        // Cart checkout took SKU stock inside the create tx — give it back.
        await failOrderAndRestoreStock(db, orderCreated.orderId, 'payment_failed', {
          restoreStock: true,
        });
      } catch (failErr) {
        cleanupError ??= failErr;
        captureCaught(failErr, {
          scope: 'cart-checkout.failOrder',
          severity: 'error',
          extra: { orderId: orderCreated.orderId },
        });
      }
    }
    // Give back the promo quota reserved in the checkout tx.
    if (reservedPromoCodeId) {
      try {
        for (const [orderId, reservationId] of promoReservationByOrderId) {
          const created = applied.orders.find((order) => order.orderId === orderId);
          if (created)
            await releasePromoReservation(db, {
              reservationId,
              purchaseId: created.firstOrderLineId,
              userId: input.userId,
              vendorId: created.vendorId,
            });
        }
      } catch (promoErr) {
        cleanupError ??= promoErr;
        captureCaught(promoErr, {
          scope: 'cart-checkout.releasePromoReservation',
          severity: 'error',
          extra: { promoCodeId: reservedPromoCodeId },
        });
      }
    }
    if (cleanupError) throw cleanupError;
  };

  // ─── Post-tx Phase 1: hold all ──────────────────────────────────────────

  // Load Stripe customer ID so off-session charges can attach the mandate.
  const stripeCustomerId =
    (await userQueries.getUserStripeCustomerId(deps.db, input.userId)) ?? undefined;

  const holds: Array<{
    orderId: string;
    firstOrderLineId: string;
    providerHoldId: string;
    totalAgorot: number;
  }> = [];

  // Phase 1: one PI per vendor order (reservationId = orderId).
  for (const orderCreated of applied.orders) {
    // Load vendor / deal context from the first deal in the group.
    const ctx = await loadVendorPaymentContext(
      db,
      orderCreated.vendorId,
      orderCreated.firstDealId,
      Number(orderCreated.total),
      input.userId,
    );

    if (ctx.vendor.id !== orderCreated.vendorId || ctx.deal.id !== orderCreated.firstDealId) {
      await failAllOrders();
      throw new CartCheckoutPaymentError(
        'PAYMENT_CONTRACT_MISMATCH',
        `Vendor payment context mismatch for order ${orderCreated.orderId}`,
      );
    }

    if (!ctx.vendor.chargesEnabled || !ctx.vendor.stripeAccountId) {
      for (const h of holds) {
        try {
          await deps.payments.releaseHold({
            reservationId: h.orderId,
            providerHoldId: h.providerHoldId,
          });
        } catch (releaseErr) {
          captureCaught(releaseErr, {
            scope: 'cart-checkout.releaseHold',
            severity: 'warning',
          });
        }
      }
      await failAllOrders();
      throw new CartCheckoutPaymentError(
        'VENDOR_NOT_ONBOARDED',
        `Vendor not onboarded for order ${orderCreated.orderId}`,
      );
    }

    const feeOverride = orderIdToFeeAgorot.get(orderCreated.orderId);
    if (promoFeeOrderIds.has(orderCreated.orderId) && feeOverride === undefined) {
      captureCaught(
        new Error(`promo_fee_override_missing order=${orderCreated.orderId} deal=${ctx.deal.id}`),
        { scope: 'cart-checkout.placeHold.promoFee', severity: 'error' },
      );
    }

    // Order total minus any referral credit applied to this order.
    const creditApplied = creditAppliedByOrderId.get(orderCreated.orderId) ?? 0;
    const orderTotalAgorot = Number(orderCreated.total) - creditApplied;
    const applicationFeeAgorot =
      feeOverride ??
      Math.floor((orderTotalAgorot * (Number(getPlatformFeePct(deps.env)) || 10)) / 100);
    const expectedContract = {
      purchaseId: orderCreated.firstOrderLineId,
      orderId: orderCreated.orderId,
      amountAgorot: orderTotalAgorot,
      currency: 'ils',
      customerId: stripeCustomerId ?? null,
      destinationAccountId: ctx.vendor.stripeAccountId,
      applicationFeeAgorot,
      effectKey: `payment-contract:${orderCreated.firstOrderLineId}`,
    };
    const [existingContract] = await db
      .insert(paymentExpectedContracts)
      .values(expectedContract)
      .onConflictDoNothing({ target: paymentExpectedContracts.purchaseId })
      .returning({ id: paymentExpectedContracts.id });
    if (!existingContract) {
      const [boundContract] = await db
        .select({
          purchaseId: paymentExpectedContracts.purchaseId,
          orderId: paymentExpectedContracts.orderId,
          amountAgorot: paymentExpectedContracts.amountAgorot,
          currency: paymentExpectedContracts.currency,
          customerId: paymentExpectedContracts.customerId,
          destinationAccountId: paymentExpectedContracts.destinationAccountId,
          applicationFeeAgorot: paymentExpectedContracts.applicationFeeAgorot,
        })
        .from(paymentExpectedContracts)
        .where(eq(paymentExpectedContracts.purchaseId, expectedContract.purchaseId))
        .limit(1);
      if (
        !boundContract ||
        Object.entries(expectedContract).some(
          ([key, value]) =>
            key !== 'effectKey' && boundContract[key as keyof typeof boundContract] !== value,
        )
      ) {
        throw new CartCheckoutPaymentError(
          'PAYMENT_CONTRACT_MISMATCH',
          `Payment contract mismatch for ${expectedContract.purchaseId}`,
        );
      }
    }

    const holdResult = await deps.payments.placeHold({
      reservationId: orderCreated.orderId,
      // First line id = the purchase id finalizePurchase resolves the order
      // from (PI metadata.purchaseId) — without it the webhook cannot finalize
      // cart orders (chargeRef is only set BY finalize → circular lookup).
      purchaseId: orderCreated.firstOrderLineId,
      checkoutKind: 'cart',
      providerCardToken: cardCheck.providerCardToken,
      customerId: stripeCustomerId,
      totalAgorot: orderTotalAgorot,
      vendor: {
        vendorId: ctx.vendor.id,
        providerAccountId: ctx.vendor.stripeAccountId,
        dealTitle: ctx.deal.title,
      },
      // Pass explicit fee override when promo patched the commission amount.
      // Avoids recomputing 10% of discounted total (which would be wrong for
      // vendor-funded promos where platform keeps fee on original price).
      applicationFeeAgorot,
      promoReservationId: promoReservationByOrderId.get(orderCreated.orderId),
      // SKU identity — lets Stripe provider build a variant label for the PI
      // description and metadata (e.g. "Deal Title — L · Red").
      dealId: ctx.deal.id,
      dealSkuId: ctx.deal.dealSkuId,
    });

    if (!holdResult.ok) {
      for (const h of holds) {
        try {
          await deps.payments.releaseHold({
            reservationId: h.orderId,
            providerHoldId: h.providerHoldId,
          });
        } catch (releaseErr) {
          captureCaught(releaseErr, {
            scope: 'cart-checkout.releaseHold',
            severity: 'warning',
          });
        }
      }
      await failAllOrders();
      throw new CartCheckoutPaymentError(holdResult.code, holdResult.message);
    }

    holds.push({
      orderId: orderCreated.orderId,
      firstOrderLineId: orderCreated.firstOrderLineId,
      providerHoldId: holdResult.providerHoldId,
      totalAgorot: orderTotalAgorot,
    });
  }

  // Persist every capture intent before any provider capture can occur.
  try {
    for (const hold of holds) {
      await persistCartCaptureIntent(db, {
        checkoutKey,
        orderId: hold.orderId,
        orderLineId: hold.firstOrderLineId,
        providerHoldId: hold.providerHoldId,
        amountAgorot: hold.totalAgorot,
        checkoutOrderIds: applied.orders.map((order) => order.orderId),
      });
    }
  } catch (persistErr) {
    for (const hold of holds) {
      try {
        await deps.payments.releaseHold({
          reservationId: hold.orderId,
          providerHoldId: hold.providerHoldId,
        });
      } catch (releaseErr) {
        captureCaught(releaseErr, { scope: 'cart-checkout.releaseHold', severity: 'warning' });
      }
    }
    await failAllOrders();
    throw persistErr;
  }

  // ─── Post-tx Phase 2: capture all (all holds succeeded) ────────────────
  for (const hold of holds) {
    let captureResult;
    try {
      captureResult = await deps.payments.captureHold({
        reservationId: hold.orderId,
        purchaseId: hold.firstOrderLineId,
        providerHoldId: hold.providerHoldId,
        totalAgorot: hold.totalAgorot,
      });
    } catch (error) {
      captureCaught(error, {
        scope: 'cart-checkout.capture',
        severity: 'error',
      });
      const compensation = await createCartCompensation(db as TxDrizzleClient, {
        checkoutKey,
        orderIds: applied.orders.map((order) => order.orderId),
      });
      for (const outboxId of compensation.outboxIds) await enqueueOutbox(outboxId);
      throw new CartCheckoutPaymentError(
        'COMPENSATION_PENDING',
        'Payment compensation pending retry',
      );
    }

    if (!captureResult.ok) {
      captureCaught(new Error(`Cart capture failed: ${hold.orderId}`), {
        scope: 'cart-checkout.capture',
        severity: 'error',
        extra: { orderId: hold.orderId, code: captureResult.code },
      });
      const compensation = await createCartCompensation(db as TxDrizzleClient, {
        checkoutKey,
        orderIds: applied.orders.map((order) => order.orderId),
      });
      for (const outboxId of compensation.outboxIds) await enqueueOutbox(outboxId);
      if (compensation.state === 'compensation_pending')
        throw new CartCheckoutPaymentError(
          'COMPENSATION_PENDING',
          'Payment compensation pending retry',
        );
      await failAllOrders();
      throw new CartCheckoutPaymentError(captureResult.code, captureResult.message);
    }
    try {
      await recordCartCapture(db, {
        checkoutKey,
        orderId: hold.orderId,
        providerPaymentId: captureResult.providerPaymentId,
      });
    } catch (error) {
      captureCaught(error, {
        scope: 'cart-checkout.recordCartCapture',
        severity: 'error',
      });
      const compensation = await createCartCompensation(db as TxDrizzleClient, {
        checkoutKey,
        orderIds: applied.orders.map((order) => order.orderId),
      });
      for (const outboxId of compensation.outboxIds) await enqueueOutbox(outboxId);
      throw new CartCheckoutPaymentError(
        'COMPENSATION_PENDING',
        'Payment compensation pending retry',
      );
    }
  }

  const fulfillmentOutboxIds = await createCartFulfillmentWork(db, {
    checkoutKey,
    orderIds: applied.orders.map((order) => order.orderId),
  });
  for (const outboxId of fulfillmentOutboxIds) await enqueueOutbox(outboxId);

  for (const outboxId of outboxIds) await enqueueOutbox(outboxId);

  return { orderLineIds };
}
