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

    Module @cfxlabsinc/product-services

    product-services

    Product service layer for the CFX platform. Resolves whether an entity can use a payment-rail product, what it costs, and whether usage limits allow the transaction — driven by the v2 rules engine across activation / fee / limit rules with admin and customer tiers.

    Architecture and full quote flow: docs/PRODUCT_SERVICES.md.

    import { ProductQuoteService } from "@cfxlabsinc/product-services";

    const productQuoteService = new ProductQuoteService({ db, valkeyClient: null });

    const request = {
    productId: "withdraw.us_wire.v1",
    sourceCurrency: "MOVEUSD",
    targetCurrency: "USD",
    amount: { source: new BigNumber(1000) },
    } as const;

    // Preview — evaluates limits without enforcing them, writes no audit row.
    const [preview] = await productQuoteService.estimate({
    customerId,
    entityId: entity.externalId,
    requests: [request],
    });

    // Transactional — enforces limits, writes a product_quote audit row.
    const [result] = await productQuoteService.quote({
    customerId,
    entityId: entity.externalId,
    requests: [{ ...request, idempotencyKey }],
    });

    if (!result.ok) {
    // result.error.code:
    // "CUSTOMER_NOT_ACTIVE" | "ENTITY_NOT_FOUND" | "ENTITY_CANNOT_TRANSACT" |
    // "PAYMENT_INSTRUMENT_BLOCKED" | "PRODUCT_INACTIVE" | "NO_ELIGIBLE_ROUTE" |
    // "AMOUNT_BELOW_FEE_FLOOR" | "NEGATIVE_FEE" | "LIMIT_EXCEEDED"
    }

    const { entity, route, fees, quote, limitEvaluation, quoteId } = result.value;
    // quote.sourceAmount, quote.targetAmountAfterFees, quote.fees, quote.totalFees

    Both methods take one entity and a list of requests, one per product, and answer one Result per request (mode: "EACH", the default) or a single all-or-nothing result (mode: "ALL"). Usage is read inside the service from accepted product_quote rows on every call; estimate() reports a predicted violation on limitEvaluation and still returns ok. entityType, countryCode, and hasIdentityDocument are derived from the entity load — callers don't pass them.

    The package exports the route, vendor and product services, six leaf rule services (3 categories × admin/customer), the quote orchestrator and its admin reads, and an eligibility check.

    Service Role
    ProductRouteAdminService Admin CRUD over product_route and its product_route_version rows. create() / update() / delete() / search() / history().
    ProductRouteService Read-only search() returns, per request, every candidate route with the winning activation/fee/limit rule joined per route.
    ProductVendorAdminService Admin CRUD over product_vendor and its product_vendor_version rows.
    ProductAdminService Admin get() / search() / update() over product. productCategory is read-only.
    ProductService get() / search() over the products one entity can use, per the route engine, in one statement; ProductAdminService extends it and adds searchAccess() by entity or entity type.
    ProductActivationRuleAdminService Admin CRUD over product_activation_rule. No write guards.
    ProductActivationRuleService Customer CRUD. Write guard: value must be DENY (approving is admin-only) → CUSTOMER_CANNOT_ACTIVATE.
    ProductFeeRuleAdminService Admin CRUD over product_fee_rule. No sign guard.
    ProductFeeRuleService Customer CRUD. Write guard: rejects negative fixedFeeAmount / variableFeeBps → INVALID_FEE_RULE.
    ProductLimitRuleAdminService Admin CRUD over product_limit_rule. No tightening guard.
    ProductLimitRuleService Customer CRUD. Write guard: every column an admin rule sets must be set, at or inside it (max ≤, min ≥) → INVALID_LIMIT_RULE.
    ProductQuoteService Consolidated orchestrator. estimate() for preview; quote() for transactional + audit; accept() / update() on issued quotes.
    ProductQuoteAdminService Admin reads over product_quote: get() / search() / getRouteTiming().
    ProductEligibilityService check() answers whether an entity can use a product and why not, gate by gate. Writes nothing and reads no usage.
    import {
    calculateFeesForSourceAmount,
    calculateFeesForSourceAmountAfterFees,
    } from "@cfxlabsinc/product-services";
    • calculateFeesForSourceAmount — given a source amount and a ProductFee[] template, returns itemized fees and totalFees. Fixed fees deduct first, variable applies to the remainder.
    • calculateFeesForSourceAmountAfterFees — back-solves: given a desired post-fee target amount, returns the source amount needed to deliver that target. Solved source is rounded up so the recipient is never short.

    Always use BigNumber — never plain JS arithmetic for fee math.

    Code Meaning
    PAYMENT_INSTRUMENT_BLOCKED No route left, and the named payment instrument is BLOCKED
    CUSTOMER_NOT_ACTIVE No route left, and the customer is not ACTIVE (not for a systemInitiated request)
    ENTITY_NOT_FOUND Entity does not exist or customer mismatch
    ENTITY_CANNOT_TRANSACT No route left, and the entity is not APPROVED
    PRODUCT_INACTIVE No candidate route admits the request
    NO_ELIGIBLE_ROUTE Candidate routes exist, but none is ACTIVE or the bankId hint excludes them all
    AMOUNT_BELOW_FEE_FLOOR Post-fee target amount is ≤ 0
    NEGATIVE_FEE Total fees are negative
    LIMIT_EXCEEDED A per-transaction or rolling-window limit was breached (quote() / update())
    QUOTE_NOT_FOUND No quote with that id for this customer (accept() / update())
    QUOTE_ALREADY_ACCEPTED The quote is already accepted (accept() / update())
    QUOTE_NOT_OK The quote's outcome is not OK (accept())
    QUOTE_EXPIRED The quote's expires_at has passed (accept())
    CUSTOMER_CANNOT_ACTIVATE Customer wrote value: "APPROVE" on an activation rule
    INVALID_FEE_RULE Customer-tier fee rule had a negative component
    INVALID_LIMIT_RULE Customer-tier limit rule loosened or left unset a column the admin baseline sets
    DUPLICATE_MATCHER A (route, customer, type, matcher) row already exists
    ROUTE_NOT_FOUND External route ID does not exist

    LIMIT_EXCEEDED carries a ProductLimitViolation payload with { window, type, limit, usage } — usage is the source amount for a TRANSACTION bound, otherwise the window's usage before this quote.

    docs/PRODUCT_SERVICES.md — schema, tier-precedence semantics, the quote() flow step-by-step, and edge cases.

    EntityProductService
    ProductActivationRuleAdminService
    ProductActivationRuleService
    ProductAdminService
    ProductEligibilityService
    ProductFeeRuleAdminService
    ProductFeeRuleService
    ProductLimitRuleAdminService
    ProductLimitRuleService
    ProductQuoteAdminService
    ProductQuoteService
    ProductRouteAdminService
    ProductRouteService
    ProductService
    ProductVendorAdminService
    ActivationRuleScopeInput
    AdminProductActivationRule
    AdminProductFeeRule
    AdminProductLimitRule
    AdminProductQuote
    AdminProductRoute
    BlockingCondition
    CallerSuppliedCriteria
    CrossRouteDenial
    DomainDerivedCriteria
    EligibilityAmount
    EligibilityFacts
    EligibilityGate
    EligibilityRefusalCode
    EligibilityTrace
    EngineEntity
    EntityDerivedCriteriaField
    EntityProduct
    EntityProductCategory
    EntityProductFee
    EntityProductSearchArgs
    EntityProductWindow
    EstimateError
    EstimateRequest
    ExplainNode
    Fees
    GetUsageInput
    MatcherCriteria
    MatcherReferences
    MintRoute
    MintRouteError
    ProductAccess
    ProductAccessSubject
    ProductActivationRule
    ProductFee
    ProductFeeRule
    ProductLimit
    ProductLimitRule
    ProductLimitViolation
    ProductQuote
    ProductRoute
    ProductRow
    ProductVendor
    ProductVendorHistoryEntry
    ProductVendorTerms
    QuoteError
    QuoteInvariantViolation
    QuoteOutput
    QuoteRequest
    ResolvedRoute
    RouteEligibility
    RouteExplanation
    RouteResolutionFailure
    RouteSearchRequest
    RouteVersion
    UsageCriteria
    VirtualAccountMintProvider
    DOMAIN_DERIVED_CRITERIA_FIELDS
    MINT_PROVIDER_VENDOR
    UNEXPLAINED_EXCLUSION_HINT
    USAGE_CRITERIA_FIELDS
    addUsBankBusinessDays
    aggregateUsageSelectFields
    blockingLeaves
    buildEligibilityTrace
    buildFeeTemplate
    calculateFeesForSourceAmount
    calculateFeesForSourceAmountAfterFees
    domainDerivedCriteria
    earliestDate
    entityStatusCriteria
    getLegacyEntityProductUsageForApi
    hasProgramClause
    isCandidate
    isDenied
    isExcluded
    isUsBankHoliday
    ledgerAccountBalanceCriteria
    mintPlacementRequest
    mintRouteFrom
    pickMintRoute
    placementGrantClause
    programGrantClause
    resolveEntityProductRoutes
    routeFusionPlaces
    routeReasonLabel
    routeReasonRuleId
    ruleMatch
    ruleMatchExplain
    searchMintRoute
    usageCriteria
    usdAmountForLimits
    usFederalReserveHolidays
    validateQuoteInvariants
    withProgramClause