@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.

    Bootstrap with createProductServices:

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

    const { productQuoteService } = createProductServices({ db, entityService });

    // Stateless preview — no limit enforcement, no audit row.
    const preview = await productQuoteService.estimate({
    productName: "withdraw.us_wire.v1",
    entityId: entity.externalId,
    customerId,
    sourceCurrency: "MOVEUSD",
    targetCurrency: "USD",
    amount: { source: new BigNumber(1000) },
    });

    // Transactional — enforces limits, writes a product_quote audit row.
    const result = await productQuoteService.quote({
    productName: "withdraw.us_wire.v1",
    entityId: entity.externalId,
    customerId,
    sourceCurrency: "MOVEUSD",
    targetCurrency: "USD",
    amount: { source: new BigNumber(1000) },
    getUsage: (input) => withdrawalUsageService.get({ entityId, input }),
    getAggregateUsage: (input) => withdrawalUsageService.getAggregate({ entityId, input }),
    });

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

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

    getUsage / getAggregateUsage are TS-required on quote() to surface the limit-enforcement decision in code review. Pass null to opt out explicitly. entityType, countryCode, and hasIdentityDocument are derived inside quote() from the entity load — callers don't pass them.

    The package exports two route services + six leaf rule services (3 categories × admin/customer) + two block services + the quote orchestrator.

    Service Role
    ProductRouteAdminService Admin CRUD over product_route. create() / update() / delete(). No search() — that's a leaf-rule concern.
    ProductRouteService Read-only search() returns every route for the lookup tuple with the winning activation/fee/limit rule joined per route.
    ProductActivationRuleAdminService Admin CRUD over product_activation_rule. No write guards.
    ProductActivationRuleService Customer CRUD. Write guard: value must be INACTIVE (re-enabling is admin-only) → CUSTOMER_CANNOT_ACTIVATE.
    ProductFeeRuleAdminService Admin CRUD over product_fee_rule. No sign guard.
    ProductFeeRuleService Customer CRUD. Write guard: rejects negative fixedFeeAmount / variableFeeBpsINVALID_FEE_RULE. Customer can only add markup.
    ProductLimitRuleAdminService Admin CRUD over product_limit_rule. No tightening guard.
    ProductLimitRuleService Customer CRUD. Write guard: per-column tightening (max ≤ admin baseline, min ≥ admin baseline) → INVALID_LIMIT_RULE.
    ProductBlockAdminService Admin block CRUD on entity_product_block (type='ADMIN').
    ProductBlockService Customer block CRUD (type='CUSTOMER'). Exposes isBlocked() — invoked by ProductQuoteService before route resolution.
    ProductQuoteService Consolidated orchestrator. estimate() for preview; quote() for transactional + audit.

    Legacy non-routed-product fallbacks (ProductConfigService.isActive(), ProductFeeConfigService.getFees(), ProductLimitConfigService.getLimits()) are retained for identity.v1 / organization.v1 and exported for completeness.

    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
    ENTITY_NOT_FOUND Entity does not exist or customer mismatch
    ENTITY_CANNOT_TRANSACT Entity lacks TRANSACT capability
    PRODUCT_BLOCKED Entity has an active admin or customer block
    PRODUCT_INACTIVE Non-routed product not active (legacy fallback)
    NO_ELIGIBLE_ROUTE Routes exist but none is ACTIVE for the lookup criteria
    LIMIT_EXCEEDED A per-transaction or rolling-window limit was breached
    CUSTOMER_CANNOT_ACTIVATE Customer wrote value: "ACTIVE" on an activation rule
    INVALID_FEE_RULE Customer-tier fee rule had a negative component
    INVALID_LIMIT_RULE Customer-tier limit rule loosened past the admin baseline
    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 snapshot value from getUsage for the offending window.

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

    Classes

    ProductActivationRuleAdminService
    ProductActivationRuleService
    ProductAdminService
    ProductBlockAdminService
    ProductBlockService
    ProductFeeRuleAdminService
    ProductFeeRuleService
    ProductLimitRuleAdminService
    ProductLimitRuleService
    ProductQuoteAdminService
    ProductQuoteService
    ProductRouteAdminService
    ProductRouteService
    ProductVendorAdminService

    Type Aliases

    AdminProductActivationRule
    AdminProductFeeRule
    AdminProductLimitRule
    AdminProductQuote
    AdminProductRoute
    Fees
    GetUsageInput
    MatcherCriteria
    Product
    ProductActivationRule
    ProductBlock
    ProductFee
    ProductFeeRule
    ProductLimit
    ProductLimitRule
    ProductLimitViolation
    ProductQuote
    ProductRoute
    ProductVendor
    ProductVendorHistoryEntry
    ProductVendorTerms
    QuoteInvariantViolation
    ResolvedRoute
    RouteVersion

    Variables

    MANDATORY_VENDOR_MATCHER

    Functions

    addUsBankBusinessDays
    aggregateUsageSelectFields
    applicableMatcherFields
    calculateFeesForSourceAmount
    calculateFeesForSourceAmountAfterFees
    createProductBlock
    earliestDate
    getLegacyEntityProductUsageForApi
    isUsBankHoliday
    updateProductBlock
    usFederalReserveHolidays
    validateQuoteInvariants
    windowsEarliestDate