@cfxlabsinc/b2b-services
    Preparing search index...

    Customer-scoped projection of a product_quote audit row. Mirrors the fields needed by both audit-trail readers (admin dashboards, customer receipts) and the quote() idempotency-replay path: every field needed to rebuild the original quote's QuoteOutput from the row alone is here.

    Internal numeric ids and FKs are dropped in favour of external IDs. fees is the per-receiver fee snapshot reconstructed from the tier- tagged columns (cfx_*, customer_*, provider_*). inputAmount is the discriminated union the caller originally passed (or null for fee-only quotes).

    type ProductQuote = {
        acceptedAt: Temporal.Instant | null;
        activationRuleVersionId: number | null;
        bankId: BankId | null;
        cashDepositRetailerId: string | null;
        createdAt: Temporal.Instant;
        customerId: string;
        data: PinnedMatcherCriteria;
        decimalPlaces: number;
        entity: { id: string; type: "IDENTITY" | "ORGANIZATION" };
        expiresAt: Temporal.Instant;
        feeRuleVersionId: number | null;
        fees: ProductFee[];
        id: string;
        idempotencyKey: string | null;
        inputAmount: { source: BigNumber } | { target: BigNumber } | null;
        inputBankId: BankId | null;
        limitEvaluation: LimitEvaluation | null;
        limitRuleVersionId: number | null;
        minSettlementDate: Temporal.PlainDate | null;
        outcome: "OK" | "LIMIT_EXCEEDED" | "INACTIVE" | "LEGACY";
        productName: ProductName;
        providerFeeVisible: boolean | null;
        quantity: number;
        quote: QuoteShape | null;
        regionCode: string | null;
        routeId: string;
        routeVersionId: number;
        sourceCurrency: string | null;
        speed: Speed | null;
        targetCurrency: string | null;
        vendorId: Vendor;
        vendorVersionId: number;
        violation: ProductLimitViolation | null;
    }
    Index
    acceptedAt: Temporal.Instant | null
    activationRuleVersionId: number | null
    bankId: BankId | null

    Resolved bankId — joined through product_route_version → vendor → bank. Reflects what the resolver actually picked. null when the route's vendor is non-bank (e.g. cfx).

    cashDepositRetailerId: string | null
    createdAt: Temporal.Instant
    customerId: string
    data: PinnedMatcherCriteria

    The pinned matcher criteria the resolver evaluated against.

    decimalPlaces: number
    entity: { id: string; type: "IDENTITY" | "ORGANIZATION" }
    expiresAt: Temporal.Instant
    feeRuleVersionId: number | null
    fees: ProductFee[]

    Reconstructed fee template — per-receiver line items.

    id: string
    idempotencyKey: string | null
    inputAmount: { source: BigNumber } | { target: BigNumber } | null
    inputBankId: BankId | null

    The caller's bankId hint, if any (narrows route selection). Audit row column; distinct from the resolved bankId above.

    limitEvaluation: LimitEvaluation | null

    What the unconditional limit evaluation concluded, independent of the caller's bypassLimits. null on rows where no evaluation ran (INACTIVE, LEGACY, pre-migration). Differs from outcome exactly when a bypassed quote would have been blocked — which is the only way to see that from the audit trail.

    limitRuleVersionId: number | null
    minSettlementDate: Temporal.PlainDate | null

    Earliest settle-by date. null on pre-migration rows (column was added 2026-05-11); service rebuild uses the current product config to recompute as a fallback.

    outcome: "OK" | "LIMIT_EXCEEDED" | "INACTIVE" | "LEGACY"
    productName: ProductName
    providerFeeVisible: boolean | null

    Snapshot of providerFeeVisible at quote time.

    quantity: number
    quote: QuoteShape | null

    Concrete amounts. null when the original input was amount-less.

    regionCode: string | null
    routeId: string
    routeVersionId: number

    Numeric FKs to the pinned version rows, exposed so callers that need deep version-row context (admin diff views, the quote() replay rebuild) can resolve them without re-querying the audit row.

    sourceCurrency: string | null
    speed: Speed | null
    targetCurrency: string | null
    vendorId: Vendor

    External id of the vendor the resolver picked (product_vendor.external_id). Never null — the vendor join is INNER.

    This is the dispatch key for consumers that must branch on which vendor won; bankId cannot serve that purpose, because it is null for every vendor that fronts banks rather than being one.

    vendorVersionId: number
    violation: ProductLimitViolation | null

    Limit-violation breakdown. Non-null iff outcome === 'LIMIT_EXCEEDED'.