flexpay-engine 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,21 +1,224 @@
1
1
  # flexpay-engine
2
2
 
3
- FlexPay loan servicing engine — pricing, schedules, allocation, delinquency.
3
+ Loan-servicing engine for FlexPay. Owns **money truth** — pricing, schedules,
4
+ payment allocation, delinquency, lock-state decisions, banking rails. Consumed
5
+ by the rest of the FlexPay stack as an npm package today and as a Cloudflare
6
+ Worker for banking integrations.
4
7
 
5
- ## Installation
8
+ > **Mental model**: Stripe-of-loans. This is the engine other services call.
9
+ > Workflow, UI, identity, and device integrations live elsewhere.
10
+
11
+ > **New here?** Read [`docs/roadmap.md`](docs/roadmap.md) — the Upya cutover
12
+ > journey, where we are, and how the pieces connect.
13
+
14
+ ---
15
+
16
+ ## What this owns vs what it doesn't
17
+
18
+ The test for whether something belongs here:
19
+ **does it affect what a customer owes right now?**
20
+
21
+ | Belongs in engine | Does NOT belong in engine |
22
+ |---|---|
23
+ | Ledger (contracts, payments, schedules) | Customer profiles / KYC |
24
+ | Pricing, allocation, delinquency | Phone catalog / inventory |
25
+ | Lock-state DECISION (locked/unlocked) | Lock-state EXECUTION (Trustonic, Nuovopay APIs) |
26
+ | Payment rails (BNC, ChinChin) | Notification transport (SMS, email, push) |
27
+ | Contract state events | Sales flow UI / portal forms |
28
+ | Reconciliation against banking partners | Agent/store admin tools |
29
+ | Append-only audit trail | Currency conversion / FX hedging |
30
+
31
+ ---
32
+
33
+ ## System map
34
+
35
+ ```
36
+ ┌──────────────────────────────────────┐
37
+ │ flexpay-engine (this repo) │
38
+ │ ──── money brain ──── │
39
+ │ │
40
+ │ • Ledger + schedules │
41
+ │ • Pricing, allocation, delinquency │
42
+ │ • Lock-state DECISION │
43
+ │ • Payment rails (BNC; ChinChin TBD) │
44
+ │ • R2 audit mirror │
45
+ │ • Emits state events │
46
+ └────────────┬─────────────────────────┘
47
+ │ npm pkg + Service Binding RPC
48
+ ┌──────────────────┼──────────────────┐
49
+ ▼ ▼ ▼
50
+ ┌────────────────────┐ ┌──────────────┐ ┌─────────────────┐
51
+ │ flexpay-client- │ │ Supabase │ │ flexpay-backend │
52
+ │ worker (limbs) │ │ (catalog + │ │ (mobile) │
53
+ │ │ │ identity) │ │ │
54
+ │ • Lock execution │ │ │ │ • KYC (Didit) │
55
+ │ (Trustonic, │ │ • Profiles │ │ • Customer │
56
+ │ Nuovopay) │ │ • SKUs │ │ self-service │
57
+ │ • Sales flow │ │ • Deals │ │ • Mobile UX │
58
+ │ • Customer CRUD │ │ • Inventory │ │ │
59
+ │ • Signature │ │ • Stores │ │ │
60
+ │ • Agents / stores │ │ • Agents │ │ │
61
+ └────────────────────┘ └──────────────┘ └─────────────────┘
62
+ ▲ ▲
63
+ │ portal │ mobile app
64
+ │ │
65
+ └──── flexpay-portal (Next.js) ──────┘
66
+ flexpay-app (React Native)
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Capability inventory
72
+
73
+ State as of 2026-05-22. Tracks what's owned, what's still on Upya, and what's
74
+ in flight.
75
+
76
+ ### Payments — money in
77
+
78
+ | Capability | Today | Target | Status |
79
+ |---|---|---|---|
80
+ | Pago Movil C2P (customer push w/ token) | ChinChin | engine via BNC SendC2P | ✅ sandbox-verified, ⏳ prod cert |
81
+ | Pago Movil B2P (customer-initiated push) | ChinChin | engine via BNC webhook | ✅ webhook receiver shipped |
82
+ | Débito Inmediato (recurring auto-debit) | not used | engine via BNC SIMF | ✅ sandbox-verified |
83
+ | Cash at store | Upya | engine, recorded by portal | ❌ |
84
+ | FX (USD ↔ VES) | ChinChin | TBD — not engine | ❌ scope decision pending |
85
+
86
+ ### Payments — money out
87
+
88
+ | Capability | Today | Target | Status |
89
+ |---|---|---|---|
90
+ | Refunds | manual | engine via BNC SendP2P | ⏳ rail done, refund event TBD |
91
+ | Store / agent commissions | manual | engine | ❌ |
92
+
93
+ ### Contract lifecycle
94
+
95
+ | Capability | Today | Target | Status |
96
+ |---|---|---|---|
97
+ | Profile creation | Upya + Supabase | client-worker + Supabase | ❌ |
98
+ | KYC (Didit) | backend | unchanged | ✅ |
99
+ | Deal catalog | Upya + Supabase | Supabase only | ❌ — Upya sync to remove |
100
+ | Deal application (deal → terms) | Upya `editTerms` | engine `originate(dealOptionId)` | ⏳ resolver TBD |
101
+ | Unit assignment | Upya | client-worker + engine FK | ❌ |
102
+ | Pricing calc | engine partial | engine | ✅ |
103
+ | Schedule generation | engine | engine | ✅ |
104
+ | Signature capture | client-worker + Supabase | unchanged | ✅ |
105
+ | Downpayment | Upya hack | engine `recordPayment(kind=downpayment)` | ⏳ |
106
+ | Activation | Upya | engine state derives from data | ❌ |
107
+ | Status webhooks (lock/unlock) | Upya | engine emits events | ⏳ next: lock-state module |
108
+ | Amendment / re-pricing | Upya `editTerms` | engine `amend()` | ❌ |
109
+ | Closure (paid_off / repossessed / written_off) | manual + Upya | engine emits | ❌ |
110
+
111
+ ### Lock state
112
+
113
+ | Capability | Today | Target | Status |
114
+ |---|---|---|---|
115
+ | Decision (should device be locked?) | Upya `nextStatusUpdate` | **engine** | ⏳ next — `context/specs/2026-05-22-feat-lock-state-decision.md` |
116
+ | Execution (Trustonic, Nuovopay) | client-worker | client-worker | ✅ |
117
+ | Registration (first-time provisioning) | client-worker | client-worker | ✅ |
118
+ | Reconciliation (device state vs intent) | implicit / Upya | client-worker cron reading engine | ❌ |
119
+
120
+ ### Identity / org
121
+
122
+ | Capability | Today | Target | Status |
123
+ |---|---|---|---|
124
+ | Customer profile CRUD | Supabase via Upya proxy | Supabase via client-worker | ❌ |
125
+ | Agent directory | Upya | Supabase | ❌ |
126
+ | Store directory | Supabase | unchanged | ✅ |
127
+ | Auth (portal + app) | betterauth / supabase-auth | unchanged | ✅ |
128
+
129
+ ### Inventory
130
+
131
+ | Capability | Today | Target | Status |
132
+ |---|---|---|---|
133
+ | Phone SKU catalog | Supabase phone_skus | unchanged | ✅ (decouple Upya template dep) |
134
+ | Store inventory | Supabase store_inventory | unchanged | ✅ |
135
+ | Unit lifecycle state machine | Upya | client-worker + Supabase | ❌ |
136
+
137
+ ### Notifications
138
+
139
+ | Capability | Today | Target | Status |
140
+ |---|---|---|---|
141
+ | Payment received | partial | engine emits → notif worker | ⏳ |
142
+ | Payment due reminder | scattered | engine emits | ❌ |
143
+ | Lock-imminent warning | Upya | engine emits | ⏳ falls out of lock-state spec |
144
+ | KYC reminder | backend | unchanged | ✅ |
145
+ | Transport (SMS, email, push) | various | dedicated worker | (external) |
146
+
147
+ ### Reporting & reconciliation
148
+
149
+ | Capability | Today | Target | Status |
150
+ |---|---|---|---|
151
+ | Internal finance reporting | Upya CSV | engine read replica + report-scribe | ❌ |
152
+ | Customer receipts | partial | engine event | ❌ |
153
+ | SUDEBAN regulatory | unknown | engine + accounting overlay | ❌ — confirm requirements |
154
+ | 3-way reconciliation (engine ↔ BNC ↔ counterparty) | none | engine | ⏳ pre-prod gate |
155
+ | Daily close (tie-out) | manual | engine | ❌ |
156
+
157
+ **Legend**: ✅ shipped · ⏳ in flight · ❌ not started
158
+
159
+ ---
160
+
161
+ ## What's in this repo today
162
+
163
+ ```
164
+ src/
165
+ calc/ pricing, schedule, allocation, delinquency, daysActivated
166
+ operations/ originate, recordPayment, recordEvent, seedContract
167
+ schema/ drizzle schema — npm-exported for consumers
168
+ worker/
169
+ banking/
170
+ bnc/ BNC ESolutions API client (P2P, C2P, Débito, auth, audit)
171
+ event/ webhook → queue → idempotent processor + DLQ
172
+ schema/ bank_* tables (separate from engine_* tables)
173
+ services/ bake-check, archive-sweep, reconcile
174
+ lib/ archive (R2), observability, redaction, ulid, encoding
175
+ routes/ admin, health, BNC webhook
176
+ scheduled.ts cron handlers (key rotation, bake, archive sweep)
177
+ index.ts Hono app + queue consumer + scheduled dispatcher
178
+ drizzle/
179
+ banking/ bank_* migrations
180
+ docs/
181
+ bnc/ architecture, security, state-of-play, green-light criteria
182
+ runbooks/ bnc-banking ops procedures, R2 restore drill
183
+ context/
184
+ specs/ active and shipped specs
185
+ tasks/ per-task progress notes
186
+ ```
187
+
188
+ ---
189
+
190
+ ## What's next
191
+
192
+ Active specs:
193
+
194
+ - **`context/specs/2026-05-22-feat-lock-state-decision.md`** — engine becomes the source of truth for lock state. Highest-priority Upya replacement.
195
+ - **`docs/bnc/green-light-criteria.md`** — BNC production certification gates (14-day bake, 3-way reconciliation, prod creds).
196
+ - **`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.
197
+
198
+ The Upya cutover, sequenced:
199
+
200
+ 1. Lock-state decision in engine (next)
201
+ 2. Move ChinChin into engine alongside BNC (CHUNK A3)
202
+ 3. Strip Upya state polling from client-worker
203
+ 4. Strip Upya CRM calls from sales flow
204
+ 5. Decommission Upya integration
205
+
206
+ Realistic runway: 6–10 weeks of focused work post-lock-state-decision.
207
+
208
+ ---
209
+
210
+ ## Installation (npm consumers)
6
211
 
7
212
  ```bash
8
213
  bun add flexpay-engine
9
214
  ```
10
215
 
11
- ## Usage
12
-
13
216
  ```typescript
14
217
  import { recordPayment, toCents, seedContract } from 'flexpay-engine';
15
218
  import { engineContracts, enginePayments } from 'flexpay-engine/schema';
16
219
  ```
17
220
 
18
- ## Publishing to npm
221
+ ### Publishing a new version
19
222
 
20
223
  1. Bump version in `package.json`
21
224
  2. Commit: `git commit -am "chore: bump to vX.Y.Z"`
@@ -24,29 +227,90 @@ import { engineContracts, enginePayments } from 'flexpay-engine/schema';
24
227
  ```bash
25
228
  # In flexpay-client-worker
26
229
  bun add flexpay-engine@X.Y.Z
27
-
28
230
  # In flexpay-backend
29
231
  bun add flexpay-engine@X.Y.Z
30
232
  ```
31
233
  5. Commit lockfile changes in each consumer repo
32
234
 
33
- ## Versioning
235
+ ### Versioning
34
236
 
35
237
  - **Patch** (0.1.x): Bug fixes, no API changes
36
238
  - **Minor** (0.x.0): New features, backwards compatible
37
239
  - **Major** (x.0.0): Breaking changes
38
240
 
39
- ## Consumers
40
-
41
- - `flexpay-client-worker` — portal payments (PORTAL source)
42
- - `flexpay-backend` — ChinChin payments (CHINCHIN source)
43
-
44
- Both write to shared D1 database `ENGINE_DB`.
241
+ ---
45
242
 
46
243
  ## Development
47
244
 
48
245
  ```bash
49
246
  bun install
50
- bun test
247
+ bun test # 135+ tests covering calc, banking, lib
51
248
  bun run typecheck
52
249
  ```
250
+
251
+ ### Worker dev
252
+
253
+ ```bash
254
+ # Local
255
+ bun run dev
256
+
257
+ # Dry-run deploy
258
+ bunx wrangler deploy --dry-run
259
+
260
+ # Deploy to dev
261
+ bunx wrangler deploy --env=""
262
+
263
+ # Tail logs
264
+ bunx wrangler tail flexpay-engine-dev
265
+ ```
266
+
267
+ ### Migrations
268
+
269
+ ```bash
270
+ # Banking tables (worker)
271
+ bunx wrangler d1 execute flexpay-engine-dev --remote --file=drizzle/banking/XXXX_*.sql
272
+
273
+ # Engine tables (npm package — applied by consumers)
274
+ # Live in src/schema/ as drizzle definitions; migrations TBD per consumer.
275
+ ```
276
+
277
+ ---
278
+
279
+ ## Live URLs
280
+
281
+ | Endpoint | Auth | Purpose |
282
+ |---|---|---|
283
+ | `https://flexpay-engine-dev.achilleas-dbe.workers.dev` | n/a | Dev worker |
284
+ | `GET /health/bake-summary` | none | Bake-check status (14-day clock) |
285
+ | `GET /admin/bnc/status` | Bearer | Detailed health |
286
+ | `GET /admin/bnc/ping` | Bearer | BNC sandbox connectivity |
287
+ | `POST /admin/bnc/bake/run` | Bearer | Force a bake-check now |
288
+ | `POST /webhooks/bnc` | BNC creds | Inbound BNC webhook |
289
+
290
+ R2 buckets: `bank-audit-archive-{dev,prod}` (append-only audit mirror).
291
+
292
+ ---
293
+
294
+ ## Architecture decisions worth knowing
295
+
296
+ - **Single writer per table.** `src/` writes only `engine_*` tables. `worker/banking/` writes only `bank_*` tables. CI guard enforces (`scripts/check-engine-tables.sh`).
297
+ - **Append-only ledger.** No UPDATE/DELETE on `engine_payments` except via the audited `forceReseedContract` path. Same rule for `engine_lock_state_events` once shipped.
298
+ - **Cents only.** No floats touching D1.
299
+ - **Idempotency via DB constraints**, not error-string matching. UNIQUE indexes + `onConflictDoNothing`.
300
+ - **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.
301
+ - **Best-effort archive, never blocks.** R2 write failures log + retry via sweep; never break the payment path.
302
+ - **Per-message queue ack/retry.** Never throw from the queue handler — that retries the whole batch.
303
+
304
+ ---
305
+
306
+ ## Key docs
307
+
308
+ | Doc | When to read |
309
+ |---|---|
310
+ | `docs/bnc/README.md` | Anything BNC-related |
311
+ | `docs/bnc/state-of-play.md` | What works / doesn't / unknown |
312
+ | `docs/bnc/security.md` | Threat model, crypto, secrets |
313
+ | `docs/bnc/green-light-criteria.md` | When can we cut over to prod |
314
+ | `docs/runbooks/bnc-banking.md` | Ops procedures, R2 drill |
315
+ | `context/specs/` | Active and shipped specs |
316
+ | `CLAUDE.md` (parent dir) | Cross-repo conventions |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flexpay-engine",
3
- "version": "0.2.0",
4
- "description": "FlexPay loan servicing engine pricing, schedules, allocation, delinquency",
3
+ "version": "0.3.0",
4
+ "description": "FlexPay loan servicing engine \u2014 pricing, schedules, allocation, delinquency, lock-state",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -23,15 +23,32 @@
23
23
  "scripts": {
24
24
  "test": "bun test",
25
25
  "test:watch": "bun test --watch",
26
- "typecheck": "bunx tsc --noEmit"
26
+ "typecheck": "bunx tsc --noEmit",
27
+ "typecheck:worker": "bunx tsc --noEmit -p tsconfig.worker.json",
28
+ "dev": "wrangler dev",
29
+ "deploy:dev": "wrangler deploy",
30
+ "deploy:prod": "wrangler deploy --env prod",
31
+ "db:migrate:dev": "wrangler d1 migrations apply flexpay-engine-dev --remote",
32
+ "db:migrate:prod": "wrangler d1 migrations apply flexpay-engine-prod --remote --env prod",
33
+ "guard:deps": "bash scripts/check-deps.sh",
34
+ "guard:migrations": "bash scripts/check-engine-tables.sh",
35
+ "guard:all": "bun run guard:deps && bun run guard:migrations",
36
+ "probe:bnc": "bun run scripts/probe-bnc-sandbox.ts",
37
+ "ci:verify": "bash scripts/check-deps.sh && bash scripts/check-engine-tables.sh && bun run typecheck && bun run typecheck:worker"
38
+ },
39
+ "dependencies": {
40
+ "@sentry/cloudflare": "^10.20.0",
41
+ "drizzle-orm": "^0.44.7",
42
+ "hono": "^4.6.0"
27
43
  },
28
44
  "peerDependencies": {
29
45
  "drizzle-orm": "^0.44.7"
30
46
  },
31
47
  "devDependencies": {
48
+ "@cloudflare/workers-types": "^4.20250101.0",
32
49
  "@types/bun": "latest",
33
- "drizzle-orm": "^0.44.7",
34
50
  "fast-check": "^3.22.0",
35
- "typescript": "^5.7.0"
51
+ "typescript": "^5.7.0",
52
+ "wrangler": "^4.0.0"
36
53
  }
37
54
  }
@@ -3,20 +3,25 @@ import type {
3
3
  AllocationResult,
4
4
  InstallmentAllocation,
5
5
  } from "../types";
6
- import { PaymentBelowMinimumError } from "../types";
7
6
  import { round2 } from "./pricing";
8
7
 
9
8
  /**
10
9
  * Allocate a payment across installments using FIFO (earliest unpaid first).
11
10
  *
11
+ * Records the payment as given; it does not judge whether the amount is enough.
12
+ * Sufficiency is an entitlement question, decided from paidThroughDate.
13
+ * See context/specs/2026-07-30-fix-engine-below-minimum-payment.md
14
+ *
12
15
  * Rules:
13
- * - Rejects payments below minPayment
16
+ * - Records any positive amount
14
17
  * - Fills installments in sequence order
15
18
  * - Partial payments create PARTIAL status
16
19
  * - Overpayment (beyond all installments) is tracked separately
17
- * - daysActivated = floor(totalAllocated / recurringPayment * freqDays)
20
+ * - daysActivated = the delta between cumulative entitlement before and after,
21
+ * NOT this payment floored on its own — see below
18
22
  *
19
23
  * INVARIANTS:
24
+ * - fragmentation invariance: any split of the same money grants the same days
20
25
  * - totalAllocated + overpayment === paymentAmount
21
26
  * - installment.amountPaid <= installment.amountDue (never overpay an installment)
22
27
  * - newTotalPaid + newRemainingDebt === totalCost
@@ -27,14 +32,6 @@ export function allocatePayment(input: AllocationInput): AllocationResult {
27
32
  .filter((i) => i.status !== "PAID" && i.status !== "WAIVED")
28
33
  .sort((a, b) => a.sequenceNumber - b.sequenceNumber);
29
34
 
30
- // Only enforce minimum on recurring payments (not deposits, not overpayments).
31
- // Deposits (sequence 0) may be below minPayment for cheap phones / 0% down promos.
32
- // Overpayments (all installments paid) should flow through regardless of amount.
33
- const hasUnpaidRecurring = unpaid.some((i) => i.sequenceNumber > 0);
34
- if (hasUnpaidRecurring && input.paymentAmount < input.minPayment) {
35
- throw new PaymentBelowMinimumError(input.paymentAmount, input.minPayment);
36
- }
37
-
38
35
  let remaining = input.paymentAmount;
39
36
  const allocations: InstallmentAllocation[] = [];
40
37
 
@@ -61,12 +58,6 @@ export function allocatePayment(input: AllocationInput): AllocationResult {
61
58
 
62
59
  const totalAllocated = round2(input.paymentAmount - remaining);
63
60
 
64
- // Days activated: proportional to payment relative to recurring
65
- const daysActivated =
66
- input.recurringPayment > 0
67
- ? Math.floor((totalAllocated / input.recurringPayment) * input.freqDays)
68
- : 0;
69
-
70
61
  // Calculate new totals from installment state
71
62
  const currentTotalPaid = input.installments.reduce(
72
63
  (sum, i) => sum + i.amountPaid,
@@ -79,6 +70,30 @@ export function allocatePayment(input: AllocationInput): AllocationResult {
79
70
  const newTotalPaid = round2(currentTotalPaid + totalAllocated);
80
71
  const newRemainingDebt = round2(totalCost - newTotalPaid);
81
72
 
73
+ // Days activated: the DELTA between cumulative entitlement before and after.
74
+ //
75
+ // This used to floor each payment on its own — and floor() discards the
76
+ // fraction every single time, so paying one installment in pieces granted
77
+ // fewer days than paying it at once. $52 as 52 x $1 granted ZERO days,
78
+ // because floor(1/52 * 15) === 0 each time.
79
+ //
80
+ // That was unreachable while allocation rejected below-minimum payments.
81
+ // Removing the rule made it reachable, and daysActivated feeds
82
+ // paidThroughDate (recordEvent -> applyPaymentToLockState), which is the
83
+ // engine's lock decision. A customer paying in full, in instalments, would
84
+ // have been locked for not paying.
85
+ //
86
+ // Deltas off a cumulative floor telescope, so any split of the same money
87
+ // sums to the same days. Deposit and overpayment behaviour are unchanged:
88
+ // the deposit still earns on the same ratio, and overpayment is excluded
89
+ // because it never enters totalAllocated.
90
+ const cumulativeDays = (total: number) =>
91
+ input.recurringPayment > 0
92
+ ? Math.floor((total / input.recurringPayment) * input.freqDays)
93
+ : 0;
94
+ const daysActivated =
95
+ cumulativeDays(newTotalPaid) - cumulativeDays(currentTotalPaid);
96
+
82
97
  // Find next due date: first installment that will still be unpaid after allocation
83
98
  const paidIds = new Set(
84
99
  allocations.filter((a) => a.newStatus === "PAID").map((a) => a.installmentId),
package/src/index.ts CHANGED
@@ -9,10 +9,36 @@ export { allocatePayment } from "./calc/allocation";
9
9
  export { checkDelinquency } from "./calc/delinquency";
10
10
  export { computeDaysActivated } from "./calc/daysActivated";
11
11
 
12
+ // --- Lock-state decision (replaces Upya nextStatusUpdate) ---
13
+ export { decideLockState } from "./lockState/decide";
14
+ export type {
15
+ LockState,
16
+ ClosureReason,
17
+ LockStateContract,
18
+ LockStatePayment,
19
+ LockStateInput,
20
+ LockStateOutput,
21
+ } from "./lockState/types";
22
+
12
23
  // --- Operations layer (D1-backed) ---
13
24
  export { originateContract } from "./operations/originate";
25
+ export { recordEvent } from "./operations/recordEvent";
14
26
  export { recordPayment } from "./operations/recordPayment";
15
27
  export { seedContract } from "./operations/seedContract";
28
+ export {
29
+ snapshotSeedContract,
30
+ planSnapshotSeed,
31
+ } from "./operations/snapshotSeed";
32
+ export type {
33
+ SnapshotSeedInput,
34
+ SnapshotSeedPlan,
35
+ SnapshotSeedResult,
36
+ } from "./operations/snapshotSeed";
37
+ export { getLockState } from "./operations/getLockState";
38
+ export type { GetLockStateInput } from "./operations/getLockState";
39
+
40
+ // --- Utilities ---
41
+ export { ulid } from "./utils/ulid";
16
42
 
17
43
  // --- Money helpers ---
18
44
  export { toCents, fromCents, formatCents } from "./utils/cents";
@@ -36,8 +62,12 @@ export type {
36
62
  // --- Operation types ---
37
63
  export type {
38
64
  EngineDb,
65
+ EngineEventSource,
66
+ EngineEventType,
39
67
  OriginateContractInput,
40
68
  OriginateContractResult,
69
+ RecordEventInput,
70
+ RecordEventResult,
41
71
  RecordPaymentInput,
42
72
  RecordPaymentResult,
43
73
  } from "./operations/types";
@@ -51,7 +81,6 @@ export type {
51
81
  // --- Errors ---
52
82
  export {
53
83
  LoanEngineError,
54
- PaymentBelowMinimumError,
55
84
  ContractAlreadyPaidOffError,
56
85
  } from "./types";
57
86