@cfxlabsinc/b2b-services
    Preparing search index...
    Index
    db: B2bDatabase
    instance: (...args: []) => ProductRouteService = ...

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

    • Why each live route did or did not survive, per request: explainRoutes({ requests })[i] answers requests[i] alone, [] when its product has no routes. Walks every live route and evaluates its matcher in TypeScript via ruleMatchExplain — see docs/proposals/EXPLAINABLE_ROUTE_ENGINE.md §4.4 for why. Criteria come back FROM SQL rather than rebuilt here, so time-windowed rules still explain correctly. Diagnostic only: a smaller, separate, constant-text statement than search's — see test/ProductRouteService.statementSize.test.ts. Each route also names the cross-route activation rules that remove it, read by searchCrossRouteActivationRules alongside.

      Parameters

      • __namedParameters: {
            customerId: string;
            entityId: string;
            entityType: NonNullable<"IDENTITY" | "ORGANIZATION" | undefined>;
            now?: Instant;
            requests: readonly RouteSearchRequest[];
        }

      Returns Promise<RouteExplanation[][]>

    • For each requested product, return the rail-level set of supported source currencies and country codes.

      sourceCurrencies are extracted from product_route_version.matcher (the route's own gate). countryCodes are extracted from each route's ADMIN-tier APPROVE activation rule matchers — route matchers don't carry country constraints in practice. Both use b2b.matcher_extract_literals (recursive plpgsql walker that pulls is / is_one_of literals; none / is_not / ranges are unbounded) and the b2b.literal_union aggregate to fold per-route extractions into one capability summary per product.

      A field is undefined when at least one contributing matcher placed no enumerable constraint on it (ALWAYS, the field didn't appear, or any unsupported operator). Callers surface that as "no rail-level restriction"; an empty array would mean "the rail supports nothing".

      Customer-tier rules are intentionally excluded — this is a product- level capability summary, not a per-customer authorization check. Both read the live versions search reads, through the same views.

      Parameters

      • productIds: readonly string[]

      Returns Promise<
          Record<
              string,
              {
                  countryCodes: string[]
                  | undefined;
                  sourceCurrencies: string[] | undefined;
              },
          >,
      >

    • Single composite read-through for the rules engine. One CTE-shaped SQL traversal returns every route matching the lookup criteria, joined with the route's current version + vendor row, and the effective activation/ fee/limit rule-version per route.

      The route's own matcher (on product_route_version) gates which routes are candidates. Tier precedence (CUSTOMER > ADMIN-for-customer > ADMIN-global) is resolved inside the rule CTEs via DISTINCT ON ... ORDER BY. b2b.rule_match is the only custom SQL surface.

      search({ requests })[i] answers requests[i] alone, including [] for a request with no eligible route. Two requests naming the same product resolve independently, so their criteria can select different rules.

      The requests travel as one jsonb array bind parameter, unnested WITH ORDINALITY to recover the request index, so the statement TEXT is byte-identical for 1 request and for 25 — which is what keeps a wide batch clear of the RDS Proxy pinning line. See test/ProductRouteService.statementSize.test.ts.

      It also derives the DOMAIN_DERIVED_CRITERIA_FIELDS; a caller value wins and a foreign or unknown id derives nothing. See docs/PRODUCT_SERVICES.md § Domain-derived criteria.

      Cross-route rules, a vendor's own restriction among them, are not applied: callers apply searchCrossRouteActivationRules' denials, as ProductQuoteService.resolve() and the eligibility trace's deniedBy do.

      No pagination: a caller wants every request's whole result set, and a page across a batch names no meaningful slice.

      Parameters

      • __namedParameters: {
            customerId: string;
            entityId: string;
            entityType: NonNullable<"IDENTITY" | "ORGANIZATION" | undefined>;
            now?: Instant;
            requests: readonly RouteSearchRequest[];
        }
        • customerId: string
        • entityId: string

          The entity this search resolves routes for, together with the entityType below. The EntityDerivedCriteriaFields are derived from this row in SQL — callers pass CallerSuppliedCriteria and never those fields.

          The entity must already be known to exist: the view yields no row for an unknown id, and jsonb || NULL is NULL, which no matcher accepts. ProductQuoteService.resolve() satisfies that by fetching the entity first for its error paths and the audit-row pin.

          Quoting a resource rather than an amount — creating a new organization, say — has no entity for the quote's target yet. Pass the customer's own entity id there: the four fields then describe the identity the resource is being created under, which is what those matchers gate on in that flow anyway. There is deliberately no second parameter for the resource case.

          RFC §4.5 Deploy 1.

        • entityType: NonNullable<"IDENTITY" | "ORGANIZATION" | undefined>

          Which kind of entity entityId names, and therefore which table the other three EntityDerivedCriteriaFields are read from. Required alongside entityId: nothing derives an entity's kind from its id string, so the two always travel together. Every caller already holds the fetched row that proves entityId exists, which is where this comes from (entity.type).

          Mislabelling an entity is not silently corrected — the view lookup is keyed on the kind too, finds no row, and the search returns no candidates. See EntityDerivedCriteriaField.

        • Optionalnow?: Instant

          Wall-clock instant the resolver evaluates against. Each candidate route's matcher sees dayOfWeek / timeOfDay computed from this instant converted to that route's data.cutoffTimezone — so a "before 4:15 PM" cutoff fires at different UTC instants for routes in different timezones. Defaults to Temporal.Now.instant().

        • requests: readonly RouteSearchRequest[]

      Returns Promise<ResolvedRoute[][]>

    • The live routes a cross-route activation rule denies, per request: one entry per (route, matching rule). Callers intersect with their own candidates; see docs/PRODUCT_SERVICES.md § Cross-route activation rules.

      Parameters

      • args: {
            awaitingVerificationAsActive?: boolean;
            customerId: string;
            entityId: string;
            entityType: NonNullable<"IDENTITY" | "ORGANIZATION" | undefined>;
            now?: Instant;
            requests: readonly RouteSearchRequest[];
        }
        • OptionalawaitingVerificationAsActive?: boolean

          AWAITING_VERIFICATION_STATUSES read as APPROVED; onboarding sets it.

        • customerId: string
        • entityId: string
        • entityType: NonNullable<"IDENTITY" | "ORGANIZATION" | undefined>
        • Optionalnow?: Instant
        • requests: readonly RouteSearchRequest[]

      Returns Promise<CrossRouteDenial[][]>

    • Every live product (narrowed by the filters) resolved through the route engine for one subject: search's CTEs over requests sourced from the products themselves, with criteria shared by all. Request-independent, so no amount, instrument, account or usage; limits are not applied. ENTITY_NOT_FOUND when entityId is not the customer's.

      Two statements: the cross-route activation rules first, whose denials the route statement then applies by product and vendor, as resolve() does.

      Parameters

      Returns Promise<
          | { error: ServiceError; ok: false }
          | {
              ok: true;
              value: { hasNext: boolean; items: ProductAccess[]; total: number };
          },
      >