@cfxlabsinc/b2b-services
    Preparing search index...
    Index
    • Parameters

      • __namedParameters: { db: B2bDatabase; valkeyClient: ValkeyClient | null }
        • db: B2bDatabase
        • valkeyClient: ValkeyClient | null

          Busts the entity reads an accepted or failed quote's usage reaches; null where nothing caches them.

      Returns ProductQuoteService

    instance: (...args: []) => ProductQuoteService = ...

    The process default: built from each dependency's own instance().

    • Mark a product_quote row as accepted. Stamps accepted_at = now() and surfaces typed errors when the quote isn't acceptable. Once the stamp lands, the row counts toward rolling-window usage (the read path filters accepted_at IS NOT NULL AND outcome <> 'FAILED'; a failed quote no longer counts).

      Re-checks the rolling-window limits first: quote() measured usage when it issued the quote, and other quotes may have been accepted since. The check holds a transaction-scoped advisory lock on the quote's limit scope (its entity and product category), so two accepts in one scope cannot both read usage before either is stamped. It measures against the limit rule versions the quote pinned, product first, then category, as quote() did.

      A guarded UPDATE then flips only acceptable rows. On miss we re-SELECT to disambiguate the failure (NOT_FOUND vs ALREADY_ACCEPTED vs EXPIRED vs NOT_OK).

      Idempotency: re-calling on an already-accepted quote returns QUOTE_ALREADY_ACCEPTED. Callers that legitimately replay (e.g. Temporal activity retries) should map that code to success.

      Parameters

      • __namedParameters: {
            bypassLimits?: boolean;
            customerId: string;
            quoteId: string;
            tx?: PgAsyncTransaction<NodePgQueryResultHKT, EmptyRelations>;
        }
        • OptionalbypassLimits?: boolean

          Skips the limit re-check, for a quote issued with bypassLimits: the Green Dot AuthCommit and reconciliation re-quotes and a settled inbound ACH debit, where the money has already moved.

        • customerId: string
        • quoteId: string
        • Optionaltx?: PgAsyncTransaction<NodePgQueryResultHKT, EmptyRelations>

          Runs the claim in a caller's transaction, e.g. a rail's accept. The limit-scope lock is then held until the caller commits.

      Returns Promise<
          | { ok: true; value: { quoteId: string } }
          | {
              error:
                  | LimitExceededError
                  | ServiceError<
                      | "QUOTE_NOT_FOUND"
                      | "QUOTE_EXPIRED"
                      | "QUOTE_ALREADY_ACCEPTED"
                      | "QUOTE_NOT_OK",
                  >;
              ok: false;
          },
      >

    • Stateless preview — resolves the route + fee template + amount math without writing an audit row and without ever blocking on limits. Use this for UI previews and any caller that has no intent to trade.

      When amount is null returns the fee template + go/no-go (route status); no concrete amounts are computed.

      Limits are evaluated here too (Phase A), and the verdict comes back on QuoteOutput.limitEvaluation as a prediction: a preview can now tell the user "this exceeds your daily limit" instead of letting them discover it at quote() time. Phase B — the LIMIT_EXCEEDED error, the outcome stamp, the audit row — stays exclusively on the quote/accept path. estimate() still returns ok on a predicted violation; it is a preview, not a gate.

      bypassLimits is hard-set to true internally and is not part of this method's input. That flag now means only "do not enforce", which is exactly right here. It still keeps the usage snapshot out of matcher criteria, so usage-based matchers (usage30dUsd, etc.) evaluate as unset and route selection is byte-identical to what previews have always resolved.

      Cost: this adds the rolling-window usage read to every preview, plus one category limit rule search, which a category with no limit rules answers with nothing.

      Many products for one entity resolve in the same fixed number of round trips as one — see resolve.

      mode picks what a batch answers with. "EACH" (the default) returns one result per request, positionally aligned with requests. "ALL" is all-or-nothing: every output, or the single refusal REFUSAL_PRECEDENCE ranks sharpest. Reach for "ALL" only where a partial set is useless to the caller. A predicted LIMIT_EXCEEDED refuses neither — an estimate never blocks, so it rides along on an ok result as QuoteOutput.limitEvaluation.

      No request carries an idempotencyKey: an estimate writes no row, so there is nothing for a key to replay.

      Type Parameters

      Parameters

      • args: { customerId: string; entityId: string; mode: "ALL"; requests: R }

      Returns Promise<{ ok: true; value: T } | { error: BatchRefusal; ok: false }>

    • Stateless preview — resolves the route + fee template + amount math without writing an audit row and without ever blocking on limits. Use this for UI previews and any caller that has no intent to trade.

      When amount is null returns the fee template + go/no-go (route status); no concrete amounts are computed.

      Limits are evaluated here too (Phase A), and the verdict comes back on QuoteOutput.limitEvaluation as a prediction: a preview can now tell the user "this exceeds your daily limit" instead of letting them discover it at quote() time. Phase B — the LIMIT_EXCEEDED error, the outcome stamp, the audit row — stays exclusively on the quote/accept path. estimate() still returns ok on a predicted violation; it is a preview, not a gate.

      bypassLimits is hard-set to true internally and is not part of this method's input. That flag now means only "do not enforce", which is exactly right here. It still keeps the usage snapshot out of matcher criteria, so usage-based matchers (usage30dUsd, etc.) evaluate as unset and route selection is byte-identical to what previews have always resolved.

      Cost: this adds the rolling-window usage read to every preview, plus one category limit rule search, which a category with no limit rules answers with nothing.

      Many products for one entity resolve in the same fixed number of round trips as one — see resolve.

      mode picks what a batch answers with. "EACH" (the default) returns one result per request, positionally aligned with requests. "ALL" is all-or-nothing: every output, or the single refusal REFUSAL_PRECEDENCE ranks sharpest. Reach for "ALL" only where a partial set is useless to the caller. A predicted LIMIT_EXCEEDED refuses neither — an estimate never blocks, so it rides along on an ok result as QuoteOutput.limitEvaluation.

      No request carries an idempotencyKey: an estimate writes no row, so there is nothing for a key to replay.

      Type Parameters

      Parameters

      • args: { customerId: string; entityId: string; mode?: "EACH"; requests: R }

      Returns Promise<
          PerRequest<
              R,
              { error: EstimateError; ok: false }
              | { ok: true; value: QuoteOutput },
          >,
      >

    • Called when the transaction's on-chain debit fails: sets outcome to INSUFFICIENT_FUNDS (Solana rejected it for balance) or FAILED (any other failure), or INSUFFICIENT_BANK_ACCOUNT_FUNDS (a bank-account balance rule refused the payout at send time; internal only), so the attempt stops counting toward limits. Idempotent; a missing, unaccepted or already-failed quote is a no-op.

      Parameters

      • __namedParameters: {
            customerId: string;
            outcome:
                | "FAILED"
                | "INSUFFICIENT_FUNDS"
                | "INSUFFICIENT_BANK_ACCOUNT_FUNDS";
            quoteId: string;
        }

      Returns Promise<{ ok: true; value: undefined }>

    • Look up a single audit row by external id, scoped to the caller's customer. The audit table is append-only, so callers get a frozen snapshot of what the resolver decided at quote time — the on-disk row is the source of truth for replay, receipts, and downstream reconciliation.

      Returns null when the row doesn't exist or belongs to a different customer (treated as not-found per the service-pattern convention — no QUOTE_NOT_FOUND error code surfaced).

      Parameters

      • __namedParameters: { customerId: string; id: string }

      Returns Promise<ProductQuote | null>

    • Public read-only entry point for callers (admin dashboards, internal reports) that need rolling-window usage outside the quote() flow. Counts every product_quote row stamped with accepted_at and a non-FAILED outcome (a FAILED quote's debit failed), scoped to the entity.

      The published shape of fetchUsage, which owns the read.

      Parameters

      • args: {
            customerId: string;
            entityId: string;
            entityType: "IDENTITY" | "ORGANIZATION";
            productIds: readonly string[];
        }

      Returns Promise<
          Map<string, { aggregate: UsageSnapshot | null; product: UsageSnapshot }>,
      >

    • Transactional quote — same math as estimate(), plus per-product and aggregate-domain limit enforcement, plus a write to product_quote for end-to-end audit. Returns the inserted row's external id as quoteId, which downstream consumers (deposit/withdrawal/swap) persist on their own quote rows via db.helpers.productQuoteInternalId({ quoteId }) — a scalar subquery that resolves the externalId string to the int FK at insert time. Service interfaces stay string-only; ints are confined to the DB layer.

      Usage is fetched internally from product_quote (rows where acceptedAt IS NOT NULL count toward the rolling windows).

      Limits are split into two phases. Phase A evaluates them as a pure read, on every call — no caller can switch it off. Phase B applies what Phase A found: the LIMIT_EXCEEDED error, outcome, and the violation_* columns. bypassLimits: true suppresses Phase B only; the evaluated verdict is still recorded on the row's limitEvaluation, so a bypassed quote that would have been blocked is visible after the fact instead of leaving no trace.

      Auditing scope: a row is written iff a candidate route was picked — i.e. OK, LIMIT_EXCEEDED, or INACTIVE (winning activation rule's value is INACTIVE). Pre-pick failures (PRODUCT_INACTIVE, NO_ELIGIBLE_ROUTE, ENTITY_*, or a status label given to one) do not write.

      mode picks what a batch answers with. "EACH" (the default) denies per item: the requests are independent, so one refusal leaves the rest issued, and each element is exactly what a lone request would have got, down to the audit row. "ALL" is all-or-nothing — every quote issued, or one refusal standing for the batch and no OK row written, so no quote from a refused batch exists to be accepted. A refusal still writes the rows that record why; an INACTIVE or LIMIT_EXCEEDED row is that audit trail.

      Quotes in one batch do not spend each other's limits — only an accepted quote moves a rolling window, and nothing here accepts.

      Round trips are fixed in the request count on every path except a refused idempotency key, which costs a release and a reload for that input alone. Statement text is constant regardless, so RDS Proxy cannot pin on it.

      "ALL" takes no idempotencyKey on any request: replay is defined against a committed row, and across a batch that only some keys already hold there is no one right answer. assertNoIdempotencyKeys enforces that, because the overload's type cannot. Under "EACH", two requests for one product may still not share a key — see assertDistinctIdempotencyKeys, which throws. That is a deliberate carve-out from the sequential behaviour where a second call replays the first's row: a batch has no single INSERT analogue of that, and handing two requests one row id is worse than refusing. The key is caller-supplied, so the first API surface over this method owes a functional refusal code in place of the throw.

      Type Parameters

      Parameters

      • args: { customerId: string; entityId: string; mode: "ALL"; requests: R }

      Returns Promise<{ ok: true; value: T } | { error: BatchRefusal; ok: false }>

    • Transactional quote — same math as estimate(), plus per-product and aggregate-domain limit enforcement, plus a write to product_quote for end-to-end audit. Returns the inserted row's external id as quoteId, which downstream consumers (deposit/withdrawal/swap) persist on their own quote rows via db.helpers.productQuoteInternalId({ quoteId }) — a scalar subquery that resolves the externalId string to the int FK at insert time. Service interfaces stay string-only; ints are confined to the DB layer.

      Usage is fetched internally from product_quote (rows where acceptedAt IS NOT NULL count toward the rolling windows).

      Limits are split into two phases. Phase A evaluates them as a pure read, on every call — no caller can switch it off. Phase B applies what Phase A found: the LIMIT_EXCEEDED error, outcome, and the violation_* columns. bypassLimits: true suppresses Phase B only; the evaluated verdict is still recorded on the row's limitEvaluation, so a bypassed quote that would have been blocked is visible after the fact instead of leaving no trace.

      Auditing scope: a row is written iff a candidate route was picked — i.e. OK, LIMIT_EXCEEDED, or INACTIVE (winning activation rule's value is INACTIVE). Pre-pick failures (PRODUCT_INACTIVE, NO_ELIGIBLE_ROUTE, ENTITY_*, or a status label given to one) do not write.

      mode picks what a batch answers with. "EACH" (the default) denies per item: the requests are independent, so one refusal leaves the rest issued, and each element is exactly what a lone request would have got, down to the audit row. "ALL" is all-or-nothing — every quote issued, or one refusal standing for the batch and no OK row written, so no quote from a refused batch exists to be accepted. A refusal still writes the rows that record why; an INACTIVE or LIMIT_EXCEEDED row is that audit trail.

      Quotes in one batch do not spend each other's limits — only an accepted quote moves a rolling window, and nothing here accepts.

      Round trips are fixed in the request count on every path except a refused idempotency key, which costs a release and a reload for that input alone. Statement text is constant regardless, so RDS Proxy cannot pin on it.

      "ALL" takes no idempotencyKey on any request: replay is defined against a committed row, and across a batch that only some keys already hold there is no one right answer. assertNoIdempotencyKeys enforces that, because the overload's type cannot. Under "EACH", two requests for one product may still not share a key — see assertDistinctIdempotencyKeys, which throws. That is a deliberate carve-out from the sequential behaviour where a second call replays the first's row: a batch has no single INSERT analogue of that, and handing two requests one row id is worse than refusing. The key is caller-supplied, so the first API surface over this method owes a functional refusal code in place of the throw.

      Type Parameters

      Parameters

      • args: { customerId: string; entityId: string; mode?: "EACH"; requests: R }

      Returns Promise<
          PerRequest<
              R,
              | { ok: true; value: QuoteOutput & { quoteId: string } }
              | { error: QuoteError; ok: false },
          >,
      >

    • Re-resolve a pending product_quote against new inputs and patch the row in place — single audit row per logical transaction, mutated as the deposit lifecycle learns more about it. Designed for floating quotes (created with amount: null for a barcode-style placeholder) that need to be settled later with the real amount + recomputed fees

      • a fresh limit check.

      Only the genuinely updatable inputs are exposed on the signature — amount, regionCode, bankId, speed, bypassLimits. The static dimensions (productId, entity id, source/target currency, cashDepositRetailerId, decimalPlaces) are read back from the existing row and merged in. Callers can't change which customer / entity / product the quote belongs to, by construction.

      Re-runs the full resolver pipeline, then UPDATEs the row's fee snapshot, route version FKs, outcome, violation, and pinned criteria columns. bypassLimits: true skips usage prefetch + enforcement (e.g. AuthCommit settling the final amount after limits already passed at Auth); otherwise a violation lands as outcome = LIMIT_EXCEEDED + a snapshot and the method returns LIMIT_EXCEEDED.

      Once the row is accepted (accepted_at IS NOT NULL) it is frozen so rolling-window usage stays stable: the pre-check below and the UPDATE's accepted_at IS NULL predicate both answer QUOTE_ALREADY_ACCEPTED.

      Deliberately exempt from customer status, unlike quote() and estimate(): every request is systemInitiated, so the status rule's customer clause never refuses it. Entity status still applies. Two of update()'s three callers settle a quote already issued — settlement bookkeeping on money that has already moved. The third, greendotConsumptionEventActivities.ts's AUTH branch (:474), authorizes new cash at the register against an already-issued barcode, before any money has moved, so a disabled customer can keep depositing against it. The ruling to leave update() exempt stands anyway: gating AUTH without AUTH_COMMIT would strand half-committed deposits — cash already taken at the register with nothing to apply it to. Do not "fix" the inconsistency — see docs/proposals/CUSTOMER_DISABLE_DELETE.md § 5.3 for what that costs.

      Parameters

      • __namedParameters: {
            customerId: string;
            data: {
                amount?: { source: BigNumber } | { target: BigNumber } | null;
                bankId?: string;
                bypassLimits?: boolean;
                regionCode?: string;
                speed?: Speed;
            };
            id: string;
        }

      Returns Promise<
          | { ok: true; value: QuoteOutput & { quoteId: string } }
          | {
              error:
                  | PreRouteError
                  | QuoteInvariantError
                  | LimitExceededError
                  | ServiceError<"QUOTE_NOT_FOUND" | "QUOTE_ALREADY_ACCEPTED">;
              ok: false;
          },
      >