@classytic/revenue 2.1.0 → 2.2.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.
Files changed (26) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +41 -10
  3. package/dist/{bank-feed-DJtLvz_7.mjs → bank-feed-ClxNob_I.mjs} +1 -1
  4. package/dist/core/state-machines.mjs +2 -2
  5. package/dist/{engine-types-txFXOiQS.d.mts → engine-types-ChFPg3kw.d.mts} +230 -80
  6. package/dist/enums/index.mjs +1 -1
  7. package/dist/{errors-Dt46UZL_.mjs → errors-Bt5NRVMq.mjs} +19 -2
  8. package/dist/{escrow.schema-C-b41z_G.mjs → escrow.schema-BcKdzrJ7.mjs} +5 -0
  9. package/dist/{escrow.schema-9yh4Q-aQ.d.mts → escrow.schema-BdDHuQ8C.d.mts} +80 -20
  10. package/dist/{event-constants-CTiDNWzc.mjs → event-constants-DM_-A57b.mjs} +7 -0
  11. package/dist/events/index.d.mts +1 -1
  12. package/dist/events/index.mjs +2 -2
  13. package/dist/index.d.mts +38 -7
  14. package/dist/index.mjs +38 -9
  15. package/dist/providers/index.mjs +1 -1
  16. package/dist/repositories/create-repositories.d.mts +1 -1
  17. package/dist/repositories/create-repositories.mjs +1 -1
  18. package/dist/{revenue-event-catalog-CgZ57M-f.mjs → revenue-event-catalog-B9aZmNpL.mjs} +90 -2
  19. package/dist/{revenue-event-catalog-JpJcyK1E.d.mts → revenue-event-catalog-BU_KYN2-.d.mts} +645 -0
  20. package/dist/{settlement.repository-Ba2U17zY.mjs → settlement.repository-CfvgX3et.mjs} +315 -123
  21. package/dist/shared/index.mjs +1 -1
  22. package/dist/validators/index.d.mts +1 -1
  23. package/dist/validators/index.mjs +1 -1
  24. package/package.json +7 -7
  25. /package/dist/{splits-D8XkNWgX.mjs → splits-CNfQj92L.mjs} +0 -0
  26. /package/dist/{subscription.enums-DoIr56O6.mjs → subscription.enums-95othr0i.mjs} +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,101 @@
3
3
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [2.2.0] - 2026-05-26
7
+
8
+ ### Added — auth/capture + dispute event coverage
9
+
10
+ Seven new event constants on `REVENUE_EVENTS` aligning with the
11
+ `@classytic/primitives@0.7.1` payment event catalogue:
12
+ `PAYMENT_AUTHORIZED`, `PAYMENT_CAPTURED`, `PAYMENT_AUTH_VOIDED`,
13
+ `PAYMENT_DISPUTED`, `PAYMENT_DISPUTE_WON`, `PAYMENT_DISPUTE_LOST`,
14
+ `PAYMENT_SETTLED`. Catalog payload schemas updated to match.
15
+
16
+ ### Added — `RevenueError.httpStatus`
17
+
18
+ `RevenueError` carries an optional `httpStatus` field so Arc / Express
19
+ hosts can map errors to response codes without per-error switch
20
+ statements. Defaults to 500 in the host mapper when unset.
21
+
22
+ ### Added — `MethodKindLockedError` (409)
23
+
24
+ New error thrown by `TransactionRepository.backfillMethodKind` when the
25
+ existing doc is not backfill-eligible (methodKind already specific OR
26
+ status no longer `pending`). 409 because the request is well-formed but
27
+ conflicts with current resource state.
28
+
29
+ ### Changed — peer bump: `@classytic/primitives` `>=0.7.1`
30
+
31
+ Catalogue now imports `PAYMENT_METHOD_KIND` from
32
+ `@classytic/primitives/payment-method-kind`. Hosts must bump primitives
33
+ to `>=0.7.1`.
34
+
35
+ ## [2.1.1] — multi-tenant scope correctness across all repos
36
+
37
+ **Fix.** `SubscriptionRepository` and `SettlementRepository` lifecycle verbs
38
+ were calling internal `getById` / `update` / `getAll` without threading
39
+ `ctx.organizationId` into the mongokit options bag. The moment a host
40
+ enabled `multiTenantPlugin` (the recommended default — see PACKAGE_RULES
41
+ §9), every verb threw `Missing 'organizationId' in context for 'getById'`
42
+ mid-flow and the lifecycle was unusable.
43
+
44
+ Affected verbs (all now threaded correctly):
45
+
46
+ - `SubscriptionRepository.{activate,cancel,pause,resume}` — every internal
47
+ `getById` and `update` now forwards `ctx`.
48
+ - `SettlementRepository.{schedule,processPending,complete,fail}` — every
49
+ internal `getById`, `getAll`, `update` now forwards `ctx`.
50
+
51
+ **Refactor.** Introduces `RevenueRepositoryBase<TDoc, TDeps>` (abstract;
52
+ internal — not exported) consolidating the two cross-cutting concerns
53
+ that were previously hand-rolled in three places:
54
+
55
+ - `protected optsFromCtx(ctx, extra?)` — thin adapter over mongokit's
56
+ canonical `repoOptionsFromCtx` extractor, plus revenue's `_bypassTenant`
57
+ flag for platform-admin cross-org reads. **Adding a new canonical context
58
+ field is now a single edit in `repo-options.ts` upstream, not three.**
59
+ - `protected dispatch(event, ctx)` — outbox-save (session-bound when
60
+ `ctx.session` is present) → transport-publish, with isolated try/catch
61
+ on each step (PACKAGE_RULES P8 / §5.5).
62
+
63
+ `TransactionRepository`, `SubscriptionRepository`, `SettlementRepository`
64
+ all extend the base. `BaseRevenueRepoDeps` (the shared `events` / `outbox`
65
+ / `logger` trio) is now the canonical superset every per-repo `Deps`
66
+ interface extends.
67
+
68
+ ### Added
69
+
70
+ - **`tests/scenarios/subscription-tenancy.scenario.test.ts`** — 6 tests
71
+ proving each lifecycle verb works under `scope: { enabled, required }`,
72
+ cross-tenant access is rejected with `SubscriptionNotFoundError`, and
73
+ `multiTenantPlugin` is wired (canary: omitting ctx throws
74
+ `Missing organizationId`).
75
+ - **`tests/scenarios/settlement-tenancy.scenario.test.ts`** — 5 tests for
76
+ the same matrix on settlements: schedule, processPending, complete, fail,
77
+ cross-tenant rejection.
78
+
79
+ ### Internal
80
+
81
+ - `RevenueRepositoryBase` is unexported on purpose — kept private to
82
+ the package. Adding a new repo means subclassing it; consumers stay
83
+ on the existing engine factory surface (`createRevenue(...)`).
84
+ - No public API change. Engine factory, repo method signatures, and
85
+ exported types are all byte-stable.
86
+
87
+ ### Migration
88
+
89
+ None — this is a behavioural fix. If you were running 2.1.0 with
90
+ `scope: false` as a workaround for the lifecycle bugs, you can now turn
91
+ scope back on. Recommended config:
92
+
93
+ ```ts
94
+ await createRevenue({
95
+ connection: mongoose.connection,
96
+ scope: { enabled: true, fieldType: 'objectId', required: true },
97
+ // ...
98
+ });
99
+ ```
100
+
6
101
  ## [2.0.0] — major rewrite
7
102
 
8
103
  Payment lifecycle engine refactored around unified transactions, an
package/README.md CHANGED
@@ -48,30 +48,61 @@ const refundTxn = await revenue.repositories.transaction.refund(
48
48
  // refundTxn.type → 'refund', refundTxn.flow → 'outflow', refundTxn.amount → 5000
49
49
  ```
50
50
 
51
+ ## Hosted-checkout `methodKind` backfill
52
+
53
+ Every transaction carries a `methodKind: PaymentMethodKind` (`card`, `bank_transfer`, `wallet`, `cash`, `cheque`, `cryptocurrency`, `manual`, `other`). For hosted-checkout flows where the customer picks their method on the gateway's UI (Stripe Checkout, PayPal redirect, Razorpay Checkout), create the PaymentIntent with `methodKind: 'other'` — then call `transactionRepository.backfillMethodKind(transactionId, kind)` from your verification webhook handler once you know the actual choice. The backfill is an atomic CAS — allowed only when the doc still has `methodKind === 'other'` AND `status === 'pending'`; any other transition throws `MethodKindLockedError` (HTTP 409). For the Stripe case, `@classytic/revenue-stripe` exposes `stripePaymentIntentToKind(intent)` so the host doesn't write the mapping table itself.
54
+
55
+ ## Bank-feed `import()` requires `methodKind`
56
+
57
+ `TransactionRepository.import()` (and the higher-level `drainSync` / `parseAndImport`) require an explicit `opts.methodKind` — no silent default. Pass `'bank_transfer'` for Plaid/OFX/CAMT/MT940 drains, `'card'` for a Stripe-balance import, `'wallet'` for PayPal exports, `'cryptocurrency'` for exchange CSVs. This forces every feed integration to be intentional about how its rows show up in downstream analytics and accounting reports.
58
+
51
59
  ## Architecture
52
60
 
53
61
  ```
54
62
  createRevenue(config) --> RevenueEngine
55
63
  |
56
- |-- repositories.transaction extends mongokit Repository
57
- | getAll, getById, getByQuery, create, update, delete, count (inherited)
64
+ |-- repositories.transaction extends RevenueRepositoryBase
65
+ | CRUD inherited (mongokit Repository)
58
66
  | createPaymentIntent, verify, refund, handleWebhook (domain verbs)
59
67
  | hold, release, split (escrow verbs)
68
+ | import, match, unmatch, journalize, reject, removeByFeed (bank-feed verbs)
60
69
  |
61
- |-- repositories.subscription extends mongokit Repository
62
- | getAll, getById, create, update, delete, count (inherited)
70
+ |-- repositories.subscription extends RevenueRepositoryBase
71
+ | CRUD inherited
63
72
  | activate, cancel, pause, resume (domain verbs)
64
73
  |
65
- |-- repositories.settlement extends mongokit Repository
66
- | getAll, getById, create, update, delete, count (inherited)
74
+ |-- repositories.settlement extends RevenueRepositoryBase
75
+ | CRUD inherited
67
76
  | schedule, processPending, complete, fail (domain verbs)
68
77
  |
69
- |-- providers ProviderRegistry
70
- |-- events RevenueEventTransport (Arc-compatible)
71
- |-- models Mongoose models (for Arc adapter)
78
+ |-- providers ProviderRegistry
79
+ |-- events RevenueEventTransport (Arc-compatible)
80
+ |-- models Mongoose models (for Arc adapter)
81
+
82
+
83
+ RevenueRepositoryBase (internal)
84
+ |
85
+ |-- extends mongokit Repository<TDoc>
86
+ |-- protected optsFromCtx(ctx, extra?) threads RevenueContext into mongokit
87
+ | options bag (uses repoOptionsFromCtx;
88
+ | forwards organizationId, userId,
89
+ | session, requestId + _bypassTenant)
90
+ |-- protected dispatch(event, ctx) outbox.save (session-bound) →
91
+ | events.publish (PACKAGE_RULES P8)
92
+ \-- protected deps: BaseRevenueRepoDeps events / outbox? / logger?
72
93
  ```
73
94
 
74
- Repositories extend mongokit `Repository`. CRUD + pagination + query is inherited. Domain verbs contain real business logic (state machine transitions, provider calls, event emission). No service layer. No proxy methods.
95
+ **Three repos. One scope-threading helper. One dispatch helper.** Every
96
+ domain verb routes its mongokit calls through `optsFromCtx(ctx)` so
97
+ multi-tenant scope, audit attribution, and transaction sessions land
98
+ on every read/write without per-method boilerplate. Every domain event
99
+ goes through `dispatch(event, ctx)` so outbox and transport semantics
100
+ stay consistent across the package.
101
+
102
+ CRUD, pagination, querying, and policy hooks come from
103
+ [`@classytic/mongokit`](https://www.npmjs.com/package/@classytic/mongokit).
104
+ Domain verbs contain real business logic (state machine transitions,
105
+ provider calls, event emission). No service layer. No proxy methods.
75
106
 
76
107
  ## RevenueConfig
77
108
 
@@ -1,4 +1,4 @@
1
- import { l as ProviderNotFoundError } from "./errors-Dt46UZL_.mjs";
1
+ import { u as ProviderNotFoundError } from "./errors-Bt5NRVMq.mjs";
2
2
 
3
3
  //#region src/providers/base.ts
4
4
  /**
@@ -1,6 +1,6 @@
1
1
  import { _ as TRANSACTION_STATUS, a as TRANSACTION_KIND } from "../bank-feed.enums-kYTLTTbe.mjs";
2
- import { g as SETTLEMENT_STATUS, l as SPLIT_STATUS, r as SUBSCRIPTION_STATUS, w as HOLD_STATUS } from "../subscription.enums-DoIr56O6.mjs";
3
- import { a as InvalidStateTransitionError } from "../errors-Dt46UZL_.mjs";
2
+ import { g as SETTLEMENT_STATUS, l as SPLIT_STATUS, r as SUBSCRIPTION_STATUS, w as HOLD_STATUS } from "../subscription.enums-95othr0i.mjs";
3
+ import { a as InvalidStateTransitionError } from "../errors-Bt5NRVMq.mjs";
4
4
  import { defineStateMachine } from "@classytic/primitives/state-machine";
5
5
 
6
6
  //#region src/core/state-machines.ts
@@ -6,7 +6,8 @@ import { RepositoryPluginBundle, RevenueRepositories } from "./repositories/crea
6
6
  import { PluginType, Repository } from "@classytic/mongokit";
7
7
  import { TenantConfig } from "@classytic/repo-core/tenant";
8
8
  import mongoose, { Connection, Model, Schema } from "mongoose";
9
- import { EventTransport } from "@classytic/primitives/events";
9
+ import { PaymentMethodKind } from "@classytic/primitives/payment-method-kind";
10
+ import { DomainEvent, EventTransport } from "@classytic/primitives/events";
10
11
  import { OutboxStore } from "@classytic/primitives/outbox";
11
12
  import { BankImportReport, BankImportRowError, BankTransaction } from "@classytic/primitives/bank-transaction";
12
13
  import { ApprovalChain } from "@classytic/primitives/approval";
@@ -68,6 +69,7 @@ interface TransactionDocument {
68
69
  originalAmount?: number;
69
70
  originalCurrency?: string;
70
71
  method: string;
72
+ methodKind: PaymentMethodKind;
71
73
  status: string;
72
74
  /**
73
75
  * Optional embedded approval chain — P7. Hosts that gate manual /
@@ -325,17 +327,125 @@ interface RevenueSchemaOptions {
325
327
  };
326
328
  }
327
329
  //#endregion
328
- //#region src/repositories/transaction.repository.d.ts
329
- interface TransactionRepoDeps {
330
+ //#region src/repositories/base.repository.d.ts
331
+ /**
332
+ * Cross-cutting deps that every revenue repository needs.
333
+ *
334
+ * Subclasses extend this with their own bridges, providers, configs:
335
+ *
336
+ * ```ts
337
+ * export interface SettlementRepoDeps extends BaseRevenueRepoDeps {
338
+ * bridges: RevenueBridges;
339
+ * }
340
+ * ```
341
+ */
342
+ interface BaseRevenueRepoDeps {
343
+ /**
344
+ * Domain-event transport (in-process or arc-compatible). All four
345
+ * `revenue:*` event families (`payment.*`, `subscription.*`,
346
+ * `settlement.*`, `escrow.*`) flow through this single channel; hosts
347
+ * subscribe glob-style.
348
+ */
330
349
  events: EventTransport;
331
350
  /**
332
- * Optional host-owned outbox store (PACKAGE_RULES §5.5 + P8). When present,
333
- * every domain event is persisted via `outbox.save(event)` before the
334
- * in-process `events.publish(event)` so the host's relay (arc's EventOutbox,
335
- * a Postgres LISTEN/NOTIFY pump, Kafka Connect, …) can replay on transport
351
+ * Optional host-owned outbox (PACKAGE_RULES §5.5 + P8). When wired,
352
+ * every dispatched event is persisted via `outbox.save(event)` BEFORE
353
+ * `events.publish(event)` so a host relay (arc's EventOutbox, a Postgres
354
+ * LISTEN/NOTIFY pump, Kafka Connect, …) can replay on transport
336
355
  * failure. When absent, events fire through `events.publish` only.
337
356
  */
338
357
  outbox?: OutboxStore | undefined;
358
+ /**
359
+ * Optional structured logger. Outbox/transport failures are logged
360
+ * here rather than thrown — a downstream subscriber error never
361
+ * cancels the upstream business write.
362
+ */
363
+ logger?: {
364
+ error(...args: unknown[]): void;
365
+ } | undefined;
366
+ }
367
+ /**
368
+ * Abstract base for `TransactionRepository`, `SubscriptionRepository`,
369
+ * `SettlementRepository`.
370
+ *
371
+ * Concrete subclasses MUST:
372
+ * - declare a `Deps` interface that extends {@link BaseRevenueRepoDeps}
373
+ * - call `super(model, plugins)` from the constructor
374
+ * - call `inject(deps)` once during engine boot
375
+ *
376
+ * Subclasses SHOULD use {@link optsFromCtx} for every mongokit call
377
+ * that takes an options bag, and {@link dispatch} for every event
378
+ * publish.
379
+ */
380
+ declare abstract class RevenueRepositoryBase<TDoc, TDeps extends BaseRevenueRepoDeps> extends Repository<TDoc> {
381
+ /**
382
+ * Subclass-specific deps. `!` because the engine wires this once via
383
+ * `inject(deps)` immediately after construction; calling any domain
384
+ * verb before injection is a programming error and the runtime
385
+ * will fail-loud with `Cannot read properties of undefined`.
386
+ */
387
+ protected deps: TDeps;
388
+ constructor(model: Model<TDoc>, plugins?: PluginType[]);
389
+ /**
390
+ * Wire engine-managed deps. Called exactly once per repository
391
+ * instance during {@link createRevenue} bootstrap. Subclasses with
392
+ * extra steps (caching, prebuilding state machine maps) override and
393
+ * call `super.inject(deps)`.
394
+ */
395
+ inject(deps: TDeps): void;
396
+ /**
397
+ * Translate {@link RevenueContext} into a mongokit options bag.
398
+ *
399
+ * Forwards every canonical field mongokit's bundled plugins read —
400
+ * `organizationId` (multiTenant), `userId` / `user` (audit),
401
+ * `session` (transactions), `requestId` (observability) — plus
402
+ * the revenue-specific `_bypassTenant` flag for platform-admin
403
+ * cross-org reads.
404
+ *
405
+ * Pass `extra` for caller-specific options like `throwOnNotFound`,
406
+ * `lean`, `populate`, `select` — the spread is `extra`-first so
407
+ * ctx wins on a key collision (intentional: callers shouldn't
408
+ * be smuggling tenant fields through `extra`).
409
+ *
410
+ * @param ctx - The request-scoped revenue context.
411
+ * @param extra - Additional mongokit options merged in.
412
+ */
413
+ protected optsFromCtx(ctx?: RevenueContext, extra?: Record<string, unknown>): Record<string, unknown>;
414
+ /**
415
+ * Persist an event to the host outbox, then publish to the in-process
416
+ * transport. The two sides have asymmetric failure handling — see
417
+ * PACKAGE_RULES §P8.
418
+ *
419
+ * When `ctx.session` is set, the outbox `save` runs inside the same
420
+ * Mongoose transaction (true P8 session-bound write); when absent, the
421
+ * outbox row lands after commit — still durable via the host's relay,
422
+ * with only a small at-most-once window on process crash.
423
+ *
424
+ * 1. **`outbox.save` failures PROPAGATE.** If we can't durably record
425
+ * the event, the caller's transaction MUST roll back so the
426
+ * business doc and the event row land atomically (or neither
427
+ * lands). Swallowing a save failure breaks the transactional-
428
+ * outbox correctness argument — the parent doc would land while
429
+ * the event vanishes.
430
+ *
431
+ * 2. **`events.publish` failures are SWALLOWED.** The host's outbox
432
+ * relay re-publishes from the durable row on its next poll. Even
433
+ * without an outbox, in-process subscribers shouldn't be able to
434
+ * break the business operation — they're best-effort consumers.
435
+ *
436
+ * @param event - Pre-built domain event (use `createEvent(REVENUE_EVENTS.X, payload, ctx, meta)`).
437
+ * @param ctx - The same context that produced the business write.
438
+ */
439
+ protected dispatch(event: DomainEvent, ctx?: RevenueContext): Promise<void>;
440
+ }
441
+ //#endregion
442
+ //#region src/repositories/transaction.repository.d.ts
443
+ /**
444
+ * Deps for {@link TransactionRepository}. Extends {@link BaseRevenueRepoDeps}
445
+ * (events / outbox? / logger?) with provider/bridge/config wiring specific
446
+ * to the payment-flow + bank-feed lifecycles.
447
+ */
448
+ interface TransactionRepoDeps extends BaseRevenueRepoDeps {
339
449
  providers: ProviderRegistry;
340
450
  /**
341
451
  * Bank-feed provider registry (3.0). Optional — when omitted, the
@@ -346,47 +456,30 @@ interface TransactionRepoDeps {
346
456
  bridges: RevenueBridges;
347
457
  commission?: CommissionConfig;
348
458
  defaultCurrency: string;
349
- logger?: {
350
- error(...args: unknown[]): void;
351
- } | undefined;
352
459
  }
353
- declare class TransactionRepository extends Repository<TransactionDocument> {
354
- private deps;
460
+ declare class TransactionRepository extends RevenueRepositoryBase<TransactionDocument, TransactionRepoDeps> {
355
461
  constructor(model: Model<TransactionDocument>, plugins?: PluginType[]);
356
- inject(deps: TransactionRepoDeps): void;
357
- /**
358
- * Thread `ctx.organizationId` (and future ctx fields) into mongokit
359
- * options so the `multiTenantPlugin` can auto-scope filters, queries,
360
- * and inserts. Merges any caller-supplied extras. Centralizing this
361
- * here means every domain verb participates in scope isolation without
362
- * per-call boilerplate.
363
- */
364
- private optsFromCtx;
365
462
  /**
366
463
  * Save an event to the host-owned outbox, session-bound when available.
367
464
  *
368
- * When `session` is passed, the outbox row commits atomically with the
369
- * business write (P8 true session-bound write). When absent, the save
370
- * happens after commit still durable via the host's relay, but with a
371
- * small at-most-once window on process crash.
465
+ * Called INSIDE a `withTransaction` body. The outbox row commits
466
+ * atomically with the business write (P8 true session-bound write)
467
+ * if outbox.save fails, this method re-throws so `withTransaction`
468
+ * rolls the parent write back. Without propagation, the parent doc
469
+ * would commit while the event row vanishes, defeating the whole
470
+ * transactional-outbox correctness argument.
372
471
  *
373
- * Isolated try/catch: an outbox failure never throws out of this helper;
374
- * the caller still issues a transport.publish.
472
+ * Logging happens before the re-throw so the failure surfaces in
473
+ * observability without losing the original stack trace.
375
474
  */
376
- private saveToOutbox;
475
+ protected saveToOutbox(event: DomainEvent, session?: unknown): Promise<void>;
377
476
  /**
378
477
  * Publish an event to the in-process `EventTransport` after commit.
379
- * Transport failure is logged — the host relay will still redeliver from
380
- * the outbox, so in-process subscribers missing an event is recoverable.
381
- */
382
- private publishToTransport;
383
- /**
384
- * Non-transactional dispatch (used by verbs that don't open their own
385
- * `withTransaction` block): outbox.save (session-bound when ctx provides
386
- * one) → transport.publish. Matches arc's EventOutbox + MemoryEventTransport
387
- * wiring bit-for-bit.
478
+ * Transport failure is logged and swallowed — the host relay re-delivers
479
+ * from the durable outbox row on the next poll, so in-process
480
+ * subscribers missing an event is recoverable. Best-effort by design.
388
481
  */
389
- private dispatch;
482
+ protected publishToTransport(event: DomainEvent): Promise<void>;
390
483
  /** Creates transaction + calls provider. Returns the created transaction doc. */
391
484
  createPaymentIntent(params: {
392
485
  data?: Record<string, unknown>;
@@ -395,6 +488,7 @@ declare class TransactionRepository extends Repository<TransactionDocument> {
395
488
  amount: number;
396
489
  currency?: string;
397
490
  gateway: string;
491
+ methodKind: PaymentMethodKind;
398
492
  paymentData?: Record<string, unknown>;
399
493
  metadata?: Record<string, unknown>;
400
494
  idempotencyKey?: string;
@@ -487,6 +581,7 @@ declare class TransactionRepository extends Repository<TransactionDocument> {
487
581
  import(rows: BankTransaction[], opts: {
488
582
  bankAccountId: string;
489
583
  source: string;
584
+ methodKind: PaymentMethodKind;
490
585
  method?: string;
491
586
  }, ctx?: RevenueContext): Promise<BankImportReport>;
492
587
  /**
@@ -507,6 +602,7 @@ declare class TransactionRepository extends Repository<TransactionDocument> {
507
602
  */
508
603
  drainSync(providerName: string, params: FetchTransactionsParams & {
509
604
  bankAccountId: string;
605
+ methodKind: PaymentMethodKind;
510
606
  }, ctx?: RevenueContext): Promise<{
511
607
  totalImported: number;
512
608
  totalUpdated: number;
@@ -526,6 +622,7 @@ declare class TransactionRepository extends Repository<TransactionDocument> {
526
622
  buffer: Buffer | string | Uint8Array;
527
623
  format?: string;
528
624
  bankAccountId: string;
625
+ methodKind: PaymentMethodKind;
529
626
  }, ctx?: RevenueContext): Promise<BankImportReport>;
530
627
  /**
531
628
  * Hand-keyed entry — treasurer logs a cash deposit, owner injects
@@ -539,6 +636,7 @@ declare class TransactionRepository extends Repository<TransactionDocument> {
539
636
  currency: string;
540
637
  flow: 'inflow' | 'outflow';
541
638
  type: string;
639
+ methodKind: PaymentMethodKind;
542
640
  description?: string;
543
641
  counterparty?: TransactionDocument['counterparty'];
544
642
  reference?: string;
@@ -549,6 +647,37 @@ declare class TransactionRepository extends Repository<TransactionDocument> {
549
647
  sourceModel?: string;
550
648
  metadata?: Record<string, unknown>;
551
649
  }, ctx?: RevenueContext): Promise<TransactionDocument>;
650
+ /**
651
+ * Backfill the `methodKind` on a Transaction created with kind
652
+ * unknown — the canonical use case is hosted-checkout (Stripe
653
+ * Checkout, PayPal redirect, Razorpay Checkout) where the customer
654
+ * picks their payment method on the gateway's UI, AFTER the host has
655
+ * already created the PaymentIntent + Transaction with
656
+ * `methodKind: 'other'`.
657
+ *
658
+ * Call this from your verification / webhook handler once you know
659
+ * the customer's actual choice — e.g. inside
660
+ * `payment_intent.succeeded`:
661
+ *
662
+ * ```ts
663
+ * await transactionRepository.backfillMethodKind(
664
+ * tx._id,
665
+ * stripePaymentIntentToKind(event.data.object),
666
+ * ctx,
667
+ * );
668
+ * ```
669
+ *
670
+ * **Guard rule.** Atomic CAS — succeeds only when the doc has
671
+ * `methodKind === 'other'` AND `status === 'pending'`. Any other
672
+ * combination throws `MethodKindLockedError` (HTTP 409): once a
673
+ * transaction has a specific kind (or has settled past pending),
674
+ * silently overwriting it would corrupt downstream analytics and
675
+ * accounting reports.
676
+ *
677
+ * Emits `revenue:transaction.updated` with `changedFields:
678
+ * ['methodKind']` so subscribers can re-bucket the row.
679
+ */
680
+ backfillMethodKind(transactionId: string, methodKind: PaymentMethodKind, ctx?: RevenueContext): Promise<TransactionDocument>;
552
681
  /**
553
682
  * Match a bank-feed / manual transaction to GL accounts, optionally
554
683
  * cross-linking to an upstream payment-flow transaction.
@@ -677,33 +806,53 @@ declare class TransactionRepository extends Repository<TransactionDocument> {
677
806
  }
678
807
  //#endregion
679
808
  //#region src/repositories/subscription.repository.d.ts
680
- interface SubscriptionRepoDeps {
681
- events: EventTransport;
682
- /** Host-owned outbox (PACKAGE_RULES §5.5 + P8). See TransactionRepoDeps. */
683
- outbox?: OutboxStore | undefined;
684
- logger?: {
685
- error(...args: unknown[]): void;
686
- } | undefined;
687
- }
688
809
  /**
689
- * SubscriptionRepository data layer + domain verbs.
810
+ * Deps for {@link SubscriptionRepository}. Currently identical to
811
+ * {@link BaseRevenueRepoDeps} (events / outbox? / logger?). Kept as its
812
+ * own type alias so future subscription-specific deps (e.g. a billing
813
+ * engine handle) can land without touching every callsite — and so the
814
+ * `inject(deps)` signature reads as `SubscriptionRepoDeps` at the
815
+ * engine factory.
816
+ */
817
+ type SubscriptionRepoDeps = BaseRevenueRepoDeps;
818
+ /**
819
+ * SubscriptionRepository — data layer + domain verbs for the recurring-
820
+ * billing lifecycle.
821
+ *
822
+ * **CRUD inherited** from mongokit (via {@link RevenueRepositoryBase}):
823
+ * `getById`, `getByQuery`, `getAll`, `create`, `update`, `delete`,
824
+ * `findOneAndUpdate`, `count`, `exists`, `claim`, `cursor`, `updateMany`,
825
+ * `deleteMany`. All participate in `multiTenantPlugin` scope filtering
826
+ * when wired.
690
827
  *
691
- * CRUD inherited from mongokit. Domain verbs: activate, cancel, pause, resume.
828
+ * **Domain verbs (state transitions):** `activate`, `cancel`, `pause`,
829
+ * `resume`. Each runs the state-machine guard (`SUBSCRIPTION_STATE_MACHINE`
830
+ * — invalid transitions throw, never silently no-op), persists the
831
+ * resulting writes through {@link RevenueRepositoryBase.optsFromCtx} so
832
+ * tenant scope is preserved end-to-end, then dispatches its
833
+ * `revenue:subscription.*` event via {@link RevenueRepositoryBase.dispatch}.
692
834
  *
693
- * Events: each domain verb calls `this.deps.events.publish(createEvent(...))`
694
- * with a fully-qualified `REVENUE_EVENTS.*` name. Hosts can subscribe glob-style
695
- * via `revenue.events.subscribe('revenue:subscription.*', handler)` the
696
- * injected transport is arc-compatible (PACKAGE_RULES §13–§14).
835
+ * **Multi-tenant correctness.** Every internal `getById`/`update` call
836
+ * threads `ctx.organizationId` through `optsFromCtx(ctx)`. Without this
837
+ * threading the inner read would either throw
838
+ * `Missing 'organizationId' in context` (when `multiTenantPlugin` is
839
+ * required) or — worse — return another tenant's subscription matching
840
+ * the same `_id` shape (when `required: false`). 2.1.0 had this bug; 2.1.1+
841
+ * is correct.
842
+ *
843
+ * @example Activate a pending sub
844
+ * ```ts
845
+ * const ctx = { organizationId: 'org_42', actorId: 'user_99' };
846
+ * const sub = await subRepo.create(
847
+ * { customerId: 'cust_1', planKey: 'monthly', amount: 999, currency: 'USD',
848
+ * status: SUBSCRIPTION_STATUS.PENDING, isActive: false },
849
+ * ctx,
850
+ * );
851
+ * await subRepo.activate(String(sub._id), {}, ctx);
852
+ * ```
697
853
  */
698
- declare class SubscriptionRepository extends Repository<SubscriptionDocument> {
699
- private deps;
854
+ declare class SubscriptionRepository extends RevenueRepositoryBase<SubscriptionDocument, SubscriptionRepoDeps> {
700
855
  constructor(model: Model<SubscriptionDocument>, plugins?: PluginType[]);
701
- inject(deps: SubscriptionRepoDeps): void;
702
- /**
703
- * Host-owned outbox save → in-process transport publish (PACKAGE_RULES P8).
704
- * Session-bound when `ctx.session` is present (atomic outbox row write).
705
- */
706
- private dispatch;
707
856
  activate(subscriptionId: string, options?: {
708
857
  timestamp?: Date;
709
858
  }, ctx?: RevenueContext): Promise<SubscriptionDocument>;
@@ -720,37 +869,38 @@ declare class SubscriptionRepository extends Repository<SubscriptionDocument> {
720
869
  }
721
870
  //#endregion
722
871
  //#region src/repositories/settlement.repository.d.ts
723
- interface SettlementRepoDeps {
724
- events: EventTransport;
725
- /** Host-owned outbox (PACKAGE_RULES §5.5 + P8). See TransactionRepoDeps. */
726
- outbox?: OutboxStore | undefined;
872
+ /**
873
+ * Deps for {@link SettlementRepository}. Adds the optional
874
+ * `RevenueBridges` (ledger / notification ports) on top of
875
+ * {@link BaseRevenueRepoDeps}.
876
+ */
877
+ interface SettlementRepoDeps extends BaseRevenueRepoDeps {
727
878
  bridges: RevenueBridges;
728
- logger?: {
729
- error(...args: unknown[]): void;
730
- } | undefined;
731
879
  }
732
880
  interface SettlementProcessingError {
733
881
  settlementId: SettlementDocument['_id'];
734
882
  error: unknown;
735
883
  }
736
884
  /**
737
- * SettlementRepository — data layer + domain verbs.
885
+ * SettlementRepository — payouts to recipients (organizations, vendors,
886
+ * affiliates).
887
+ *
888
+ * **CRUD inherited** via {@link RevenueRepositoryBase}. **Domain verbs:**
889
+ * `schedule`, `processPending`, `complete`, `fail`. State machine:
890
+ * `pending → processing → completed | failed`; `failed` with
891
+ * `retry: true` resets to `pending` with a delayed `scheduledAt`.
738
892
  *
739
- * CRUD inherited from mongokit. Domain verbs: schedule, processPending, complete, fail.
893
+ * **Multi-tenant correctness.** Every read/write threads `ctx` through
894
+ * {@link RevenueRepositoryBase.optsFromCtx} so `multiTenantPlugin`
895
+ * scope filters apply. 2.1.0 had several `getById`/`update` calls that
896
+ * dropped ctx — fixed in 2.1.1+.
740
897
  *
741
- * Events are published via the injected `events` transport (arc-compatible).
742
- * Hosts subscribe glob-style via `revenue.events.subscribe('revenue:settlement.*', h)`.
743
- * See PACKAGE_RULES §13–§14.
898
+ * Bridges (`ledger`, `notification`) fire on `complete()` so a host
899
+ * can pin double-entry book-keeping or push a "you got paid" email
900
+ * without the repo knowing about either subsystem.
744
901
  */
745
- declare class SettlementRepository extends Repository<SettlementDocument> {
746
- private deps;
902
+ declare class SettlementRepository extends RevenueRepositoryBase<SettlementDocument, SettlementRepoDeps> {
747
903
  constructor(model: Model<SettlementDocument>, plugins?: PluginType[]);
748
- inject(deps: SettlementRepoDeps): void;
749
- /**
750
- * Host-owned outbox save → in-process transport publish (PACKAGE_RULES P8).
751
- * Session-bound when `ctx.session` is present (atomic outbox row write).
752
- */
753
- private dispatch;
754
904
  schedule(params: {
755
905
  organizationId: string;
756
906
  recipientId: string;
@@ -1,5 +1,5 @@
1
1
  import { _ as TRANSACTION_STATUS, a as TRANSACTION_KIND, b as isTransactionFlow, c as isBankFeedSource, d as isTransactionKind, f as statusesForKind, g as TRANSACTION_FLOW_VALUES, h as TRANSACTION_FLOW, i as BANK_FEED_STATUS_VALUES, l as isBankFeedStatus, m as LIBRARY_CATEGORY_VALUES, n as BANK_FEED_SOURCE_VALUES, o as TRANSACTION_KIND_VALUES, p as LIBRARY_CATEGORIES, r as BANK_FEED_STATUS, s as initialStatusFor, t as BANK_FEED_SOURCE, u as isStatusValidForKind, v as TRANSACTION_STATUS_VALUES, x as isTransactionStatus, y as isLibraryCategory } from "../bank-feed.enums-kYTLTTbe.mjs";
2
- import { A as isReleaseReason, C as HOLD_REASON_VALUES, D as RELEASE_REASON_VALUES, E as RELEASE_REASON, O as isHoldReason, S as HOLD_REASON, T as HOLD_STATUS_VALUES, _ as SETTLEMENT_STATUS_VALUES, a as isPlanKey, b as isSettlementStatus, c as PAYOUT_METHOD_VALUES, d as SPLIT_TYPE, f as SPLIT_TYPE_VALUES, g as SETTLEMENT_STATUS, h as isSplitType, i as SUBSCRIPTION_STATUS_VALUES, k as isHoldStatus, l as SPLIT_STATUS, m as isSplitStatus, n as PLAN_KEY_VALUES, o as isSubscriptionStatus, p as isPayoutMethod, r as SUBSCRIPTION_STATUS, s as PAYOUT_METHOD, t as PLAN_KEYS, u as SPLIT_STATUS_VALUES, v as SETTLEMENT_TYPE, w as HOLD_STATUS, x as isSettlementType, y as SETTLEMENT_TYPE_VALUES } from "../subscription.enums-DoIr56O6.mjs";
2
+ import { A as isReleaseReason, C as HOLD_REASON_VALUES, D as RELEASE_REASON_VALUES, E as RELEASE_REASON, O as isHoldReason, S as HOLD_REASON, T as HOLD_STATUS_VALUES, _ as SETTLEMENT_STATUS_VALUES, a as isPlanKey, b as isSettlementStatus, c as PAYOUT_METHOD_VALUES, d as SPLIT_TYPE, f as SPLIT_TYPE_VALUES, g as SETTLEMENT_STATUS, h as isSplitType, i as SUBSCRIPTION_STATUS_VALUES, k as isHoldStatus, l as SPLIT_STATUS, m as isSplitStatus, n as PLAN_KEY_VALUES, o as isSubscriptionStatus, p as isPayoutMethod, r as SUBSCRIPTION_STATUS, s as PAYOUT_METHOD, t as PLAN_KEYS, u as SPLIT_STATUS_VALUES, v as SETTLEMENT_TYPE, w as HOLD_STATUS, x as isSettlementType, y as SETTLEMENT_TYPE_VALUES } from "../subscription.enums-95othr0i.mjs";
3
3
  import { a as PAYMENT_GATEWAY_TYPE_VALUES, c as isPaymentGatewayType, i as PAYMENT_GATEWAY_TYPE, l as isPaymentStatus, n as MONETIZATION_TYPE_VALUES, o as PAYMENT_STATUS, r as isMonetizationType, s as PAYMENT_STATUS_VALUES, t as MONETIZATION_TYPES } from "../monetization.enums-B9HBOecd.mjs";
4
4
 
5
5
  export { BANK_FEED_SOURCE, BANK_FEED_SOURCE_VALUES, BANK_FEED_STATUS, BANK_FEED_STATUS_VALUES, HOLD_REASON, HOLD_REASON_VALUES, HOLD_STATUS, HOLD_STATUS_VALUES, LIBRARY_CATEGORIES, LIBRARY_CATEGORY_VALUES, MONETIZATION_TYPES, MONETIZATION_TYPE_VALUES, PAYMENT_GATEWAY_TYPE, PAYMENT_GATEWAY_TYPE_VALUES, PAYMENT_STATUS, PAYMENT_STATUS_VALUES, PAYOUT_METHOD, PAYOUT_METHOD_VALUES, PLAN_KEYS, PLAN_KEY_VALUES, RELEASE_REASON, RELEASE_REASON_VALUES, SETTLEMENT_STATUS, SETTLEMENT_STATUS_VALUES, SETTLEMENT_TYPE, SETTLEMENT_TYPE_VALUES, SPLIT_STATUS, SPLIT_STATUS_VALUES, SPLIT_TYPE, SPLIT_TYPE_VALUES, SUBSCRIPTION_STATUS, SUBSCRIPTION_STATUS_VALUES, TRANSACTION_FLOW, TRANSACTION_FLOW_VALUES, TRANSACTION_KIND, TRANSACTION_KIND_VALUES, TRANSACTION_STATUS, TRANSACTION_STATUS_VALUES, initialStatusFor, isBankFeedSource, isBankFeedStatus, isHoldReason, isHoldStatus, isLibraryCategory, isMonetizationType, isPaymentGatewayType, isPaymentStatus, isPayoutMethod, isPlanKey, isReleaseReason, isSettlementStatus, isSettlementType, isSplitStatus, isSplitType, isStatusValidForKind, isSubscriptionStatus, isTransactionFlow, isTransactionKind, isTransactionStatus, statusesForKind };