npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

flexpay-engine

v0.3.5

Published

FlexPay loan servicing engine — pricing, schedules, allocation, delinquency, lock-state

Readme

flexpay-engine

Loan-servicing engine for FlexPay. Owns money truth — pricing, schedules, payment allocation, delinquency, lock-state decisions, banking rails. Consumed by the rest of the FlexPay stack as an npm package today and as a Cloudflare Worker for banking integrations.

Mental model: Stripe-of-loans. This is the engine other services call. Workflow, UI, identity, and device integrations live elsewhere.

New here? Read docs/roadmap.md — the Upya cutover journey, where we are, and how the pieces connect.


⛔ Upgrading to 0.3.x — apply migrations 0004 and 0005 FIRST

Do not bump a consumer to 0.3.x before applying drizzle/engine/0004_next_state_change_at.sql and drizzle/engine/0005_lock_event_actor.sql to that consumer's ENGINE_DB.

From 0.3.0, recordEvent / recordPayment write engine_contracts.next_state_change_at and actor fields on engine_lock_state_events. Without these columns the contract update or event insert throws — and because engine writes are typically wired non-blocking, the failure is silent: a ledger gap, not an error the caller sees.

The migrations are additive and safe to apply ahead of the bump. 0004 also backfills next_state_change_at; 0005 backfills legacy event actors. Guard re-runs because ADD COLUMN itself is not idempotent.

After deploying the worker with actor-enriched archive version v2, apply drizzle/engine/0006_requeue_actor_archive.sql to clear archive markers and rewrite existing R2 objects with actor fields.

Earlier lock-state columns need 0001–0003 for the same reason.

0.3.0 is deprecated — use 0.3.1 or later. It was published from an unmerged branch and carries a float-drift bug in daysActivated that can add or drop a day of entitlement, which feeds the lock decision.

paid_through_date is provisional after upgrading

recordEvent maintains paid_through_date, but on a database where it is still NULL the lock-state bootstrap derives it from the payment date rather than the contract's real entitlement. Use snapshotSeedContract to seed it from an authoritative source before anything reads paid_through_date or next_state_change_at.


What this owns vs what it doesn't

The test for whether something belongs here: does it affect what a customer owes right now?

| Belongs in engine | Does NOT belong in engine | |---|---| | Ledger (contracts, payments, schedules) | Customer profiles / KYC | | Pricing, allocation, delinquency | Phone catalog / inventory | | Lock-state DECISION (locked/unlocked) | Lock-state EXECUTION (Trustonic, Nuovopay APIs) | | Payment rails (BNC, ChinChin) | Notification transport (SMS, email, push) | | Contract state events | Sales flow UI / portal forms | | Reconciliation against banking partners | Agent/store admin tools | | Append-only audit trail | Currency conversion / FX hedging |


System map

                  ┌──────────────────────────────────────┐
                  │  flexpay-engine (this repo)          │
                  │  ──── money brain ────               │
                  │                                      │
                  │  • Ledger + schedules                │
                  │  • Pricing, allocation, delinquency  │
                  │  • Lock-state DECISION               │
                  │  • Payment rails (BNC; ChinChin TBD) │
                  │  • R2 audit mirror                   │
                  │  • Emits state events                │
                  └────────────┬─────────────────────────┘
                               │ npm pkg + Service Binding RPC
            ┌──────────────────┼──────────────────┐
            ▼                  ▼                  ▼
 ┌────────────────────┐ ┌──────────────┐ ┌─────────────────┐
 │ flexpay-client-    │ │  Supabase    │ │ flexpay-backend │
 │ worker (limbs)     │ │  (catalog +  │ │ (mobile)        │
 │                    │ │   identity)  │ │                 │
 │ • Lock execution   │ │              │ │ • KYC (Didit)   │
 │   (Trustonic,      │ │ • Profiles   │ │ • Customer      │
 │    Nuovopay)       │ │ • SKUs       │ │   self-service  │
 │ • Sales flow       │ │ • Deals      │ │ • Mobile UX     │
 │ • Customer CRUD    │ │ • Inventory  │ │                 │
 │ • Signature        │ │ • Stores     │ │                 │
 │ • Agents / stores  │ │ • Agents     │ │                 │
 └────────────────────┘ └──────────────┘ └─────────────────┘
            ▲                                    ▲
            │ portal                             │ mobile app
            │                                    │
            └──── flexpay-portal (Next.js) ──────┘
                  flexpay-app (React Native)

Capability inventory

State as of 2026-05-22. Tracks what's owned, what's still on Upya, and what's in flight.

Payments — money in

| Capability | Today | Target | Status | |---|---|---|---| | Pago Movil C2P (customer push w/ token) | ChinChin | engine via BNC SendC2P | ✅ sandbox-verified, ⏳ prod cert | | Pago Movil B2P (customer-initiated push) | ChinChin | engine via BNC webhook | ✅ webhook receiver shipped | | Débito Inmediato (recurring auto-debit) | not used | engine via BNC SIMF | ✅ sandbox-verified | | Cash at store | Upya | engine, recorded by portal | ❌ | | FX (USD ↔ VES) | ChinChin | TBD — not engine | ❌ scope decision pending |

Payments — money out

| Capability | Today | Target | Status | |---|---|---|---| | Refunds | manual | engine via BNC SendP2P | ⏳ rail done, refund event TBD | | Store / agent commissions | manual | engine | ❌ |

Contract lifecycle

| Capability | Today | Target | Status | |---|---|---|---| | Profile creation | Upya + Supabase | client-worker + Supabase | ❌ | | KYC (Didit) | backend | unchanged | ✅ | | Deal catalog | Upya + Supabase | Supabase only | ❌ — Upya sync to remove | | Deal application (deal → terms) | Upya editTerms | engine originate(dealOptionId) | ⏳ resolver TBD | | Unit assignment | Upya | client-worker + engine FK | ❌ | | Pricing calc | engine partial | engine | ✅ | | Schedule generation | engine | engine | ✅ | | Signature capture | client-worker + Supabase | unchanged | ✅ | | Downpayment | Upya hack | engine recordPayment(kind=downpayment) | ⏳ | | Activation | Upya | engine state derives from data | ❌ | | Status webhooks (lock/unlock) | Upya | engine emits events | ⏳ next: lock-state module | | Amendment / re-pricing | Upya editTerms | engine amend() | ❌ | | Closure (paid_off / repossessed / written_off) | manual + Upya | engine emits | ❌ |

Lock state

| Capability | Today | Target | Status | |---|---|---|---| | Decision (should device be locked?) | Upya nextStatusUpdate | engine | ⏳ next — context/specs/2026-05-22-feat-lock-state-decision.md | | Execution (Trustonic, Nuovopay) | client-worker | client-worker | ✅ | | Registration (first-time provisioning) | client-worker | client-worker | ✅ | | Reconciliation (device state vs intent) | implicit / Upya | client-worker cron reading engine | ❌ |

Identity / org

| Capability | Today | Target | Status | |---|---|---|---| | Customer profile CRUD | Supabase via Upya proxy | Supabase via client-worker | ❌ | | Agent directory | Upya | Supabase | ❌ | | Store directory | Supabase | unchanged | ✅ | | Auth (portal + app) | betterauth / supabase-auth | unchanged | ✅ |

Inventory

| Capability | Today | Target | Status | |---|---|---|---| | Phone SKU catalog | Supabase phone_skus | unchanged | ✅ (decouple Upya template dep) | | Store inventory | Supabase store_inventory | unchanged | ✅ | | Unit lifecycle state machine | Upya | client-worker + Supabase | ❌ |

Notifications

| Capability | Today | Target | Status | |---|---|---|---| | Payment received | partial | engine emits → notif worker | ⏳ | | Payment due reminder | scattered | engine emits | ❌ | | Lock-imminent warning | Upya | engine emits | ⏳ falls out of lock-state spec | | KYC reminder | backend | unchanged | ✅ | | Transport (SMS, email, push) | various | dedicated worker | (external) |

Reporting & reconciliation

| Capability | Today | Target | Status | |---|---|---|---| | Internal finance reporting | Upya CSV | engine read replica + report-scribe | ❌ | | Customer receipts | partial | engine event | ❌ | | SUDEBAN regulatory | unknown | engine + accounting overlay | ❌ — confirm requirements | | 3-way reconciliation (engine ↔ BNC ↔ counterparty) | none | engine | ⏳ pre-prod gate | | Daily close (tie-out) | manual | engine | ❌ |

Legend: ✅ shipped · ⏳ in flight · ❌ not started


What's in this repo today

src/
  calc/         pricing, schedule, allocation, delinquency, daysActivated
  operations/   originate, recordPayment, recordEvent, seedContract
  schema/       drizzle schema — npm-exported for consumers
worker/
  banking/
    bnc/        BNC ESolutions API client (P2P, C2P, Débito, auth, audit)
    event/      webhook → queue → idempotent processor + DLQ
    schema/     bank_* tables (separate from engine_* tables)
    services/   bake-check, archive-sweep, reconcile
    lib/        archive (R2), observability, redaction, ulid, encoding
  routes/       admin, health, BNC webhook
  scheduled.ts  cron handlers (key rotation, bake, archive sweep)
  index.ts      Hono app + queue consumer + scheduled dispatcher
drizzle/
  banking/      bank_* migrations
docs/
  bnc/          architecture, security, state-of-play, green-light criteria
  runbooks/     bnc-banking ops procedures, R2 restore drill
context/
  specs/        active and shipped specs
  tasks/        per-task progress notes

What's next

Active specs:

  • context/specs/2026-05-22-feat-lock-state-decision.md — engine becomes the source of truth for lock state. Highest-priority Upya replacement.
  • docs/bnc/green-light-criteria.md — BNC production certification gates (14-day bake, 3-way reconciliation, prod creds).
  • docs/plans/2026-05-19-consumer-migration-spec.md — CHUNK 17: cut flexpay-backend and flexpay-client-worker off the npm package onto Service Binding RPC.

The Upya cutover, sequenced:

  1. Lock-state decision in engine (next)
  2. Move ChinChin into engine alongside BNC (CHUNK A3)
  3. Strip Upya state polling from client-worker
  4. Strip Upya CRM calls from sales flow
  5. Decommission Upya integration

Realistic runway: 6–10 weeks of focused work post-lock-state-decision.


Installation (npm consumers)

bun add flexpay-engine
import { recordPayment, toCents, seedContract } from 'flexpay-engine';
import { engineContracts, enginePayments } from 'flexpay-engine/schema';

Publishing a new version

  1. Bump version in package.json
  2. Commit: git commit -am "chore: bump to vX.Y.Z"
  3. Publish: npm publish --access public --otp=YOUR_CODE
  4. Update consumers:
    # In flexpay-client-worker
    bun add [email protected]
    # In flexpay-backend
    bun add [email protected]
  5. Commit lockfile changes in each consumer repo

Versioning

  • Patch (0.1.x): Bug fixes, no API changes
  • Minor (0.x.0): New features, backwards compatible
  • Major (x.0.0): Breaking changes

Development

bun install
bun test         # 135+ tests covering calc, banking, lib
bun run typecheck

Worker dev

# Local
bun run dev

# Dry-run deploy
bunx wrangler deploy --dry-run

# Deploy to dev
bunx wrangler deploy --env=""

# Tail logs
bunx wrangler tail flexpay-engine-dev

Migrations

# Banking tables (worker)
bunx wrangler d1 execute flexpay-engine-dev --remote --file=drizzle/banking/XXXX_*.sql

Engine ledger migrations are applied by the consumer that owns ENGINE_DB. For flexpay-client-worker, the authoritative path is that repository's drizzle-engine/ directory because its Wrangler config points there. Apply flexpay-client-worker/drizzle-engine/0003_* and 0004_* there; do not also apply this package's drizzle/engine/0007_* or 0008_* to the same database. Other consumers use this package's drizzle/engine/ migrations instead and must not run the worker copies against that database.

The archive requeue remains an Engine-package migration:

# Deploy the worker containing archive version v2, then:
bunx wrangler d1 execute <engine-db> --remote --file=drizzle/engine/0006_requeue_actor_archive.sql
# The requeue must run after v2 is deployed so the sweep rewrites R2 objects as v2.

Live URLs

| Endpoint | Auth | Purpose | |---|---|---| | https://flexpay-engine-dev.achilleas-dbe.workers.dev | n/a | Dev worker | | GET /health/bake-summary | none | Bake-check status (14-day clock) | | GET /admin/bnc/status | Bearer | Detailed health | | GET /admin/bnc/ping | Bearer | BNC sandbox connectivity | | POST /admin/bnc/bake/run | Bearer | Force a bake-check now | | POST /webhooks/bnc | BNC creds | Inbound BNC webhook |

R2 buckets: bank-audit-archive-{dev,prod} (append-only audit mirror).


Architecture decisions worth knowing

  • Single writer per table. src/ writes only engine_* tables. worker/banking/ writes only bank_* tables. CI guard enforces (scripts/check-engine-tables.sh).
  • Append-only ledger. No UPDATE/DELETE on engine_payments except via the audited forceReseedContract path. Same rule for engine_lock_state_events once shipped.
  • Cents only. No floats touching D1.
  • Idempotency via DB constraints, not error-string matching. UNIQUE indexes + onConflictDoNothing.
  • R2 mirror. Every banking row is mirrored to R2 within ~1s of D1 write; hourly sweep retries failures. D1 restore drill executed 2026-05-22.
  • Best-effort archive, never blocks. R2 write failures log + retry via sweep; never break the payment path.
  • Per-message queue ack/retry. Never throw from the queue handler — that retries the whole batch.

Key docs

| Doc | When to read | |---|---| | docs/bnc/README.md | Anything BNC-related | | docs/bnc/state-of-play.md | What works / doesn't / unknown | | docs/bnc/security.md | Threat model, crypto, secrets | | docs/bnc/green-light-criteria.md | When can we cut over to prod | | docs/runbooks/bnc-banking.md | Ops procedures, R2 drill | | context/specs/ | Active and shipped specs | | CLAUDE.md (parent dir) | Cross-repo conventions |