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.
The entity this search resolves routes for, together with the
entityType below. The four
EntityDerivedCriteriaFields are derived from this row in SQL,
per candidate route — callers pass CallerSuppliedCriteria and
never those four fields.
The entity must already be known to exist: the subquery 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.
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 subquery reads the named table only, finds no row, and the search returns no candidates. See entityCriteriaSql.
Optionalnow?: InstantWall-clock instant the resolver evaluates against. Each candidate
route's matcher sees dayOfWeek / minuteOfDay 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().
OptionalorderBy?: "createdAt" | "priority"Optionalpage?: numberOptionalpageSize?: number
Why each of this product's routes did or did not survive candidacy.
search filters rejected routes out in SQL, so when it returns nothing there is no row left to describe and the caller can only say "no eligible route". This answers the question
searchstructurally cannot: it selects EVERY live route for the product without applying either matcher, then evaluates both in TypeScript via ruleMatchExplain — which is the same evaluatorruleMatchprojects its boolean from, and is property-tested against the plpgsqlb2b.rule_matchit mirrors.The criteria come back FROM SQL rather than being rebuilt here. Entity fields and the per-route clock fields (
dayOfWeek/timeOfDayin each route'sdata.cutoffTimezone) are derived per route by the same expressionssearchuses; re-deriving them in TypeScript would make time-windowed rules explain wrongly. SQL stays the source of truth for the payload; TypeScript only interprets it.Diagnostic only — nothing on a money path calls this, and it deliberately omits the activation, fee and limit CTEs. That also keeps the statement far below the 16,384-byte RDS Proxy pinning line (see
search's note at the vendor-matcher call site); it is a separate, smaller statement rather than a widening of the hot one.