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

@classytic/revenue

v2.1.3

Published

Payment lifecycle engine — transactions, subscriptions, escrow, settlements, commissions. MongoKit-powered, Arc-compatible, framework-agnostic.

Readme

@classytic/revenue v2

Payment lifecycle engine — transactions, subscriptions, escrow, settlements, commissions.

MongoKit repositories with domain verbs. Arc-compatible event transport. No service layer — repositories ARE the API.


Install

npm install @classytic/revenue @classytic/mongokit mongoose zod
npm install @classytic/revenue-manual  # built-in manual provider

Quick Start

import { createRevenue } from '@classytic/revenue';
import { ManualProvider } from '@classytic/revenue-manual';

const revenue = await createRevenue({
  connection: mongoose.connection,
  defaultCurrency: 'BDT',
  providers: { manual: new ManualProvider() },
});

// Create payment — returns raw mongokit doc
const txn = await revenue.repositories.transaction.createPaymentIntent({
  amount: 10000,
  gateway: 'manual',
  data: { customerId: 'cust_1', sourceId: 'order_1', sourceModel: 'Order' },
});
// txn.publicId → 'txn_a7b3xk9m2p1q4d5e6f'
// txn.gateway.metadata.instructions → 'Payment Amount: 10000 BDT...'

// Verify (admin approves manual payment)
const verified = await revenue.repositories.transaction.verify(
  txn.gateway.paymentIntentId,
  { verifiedBy: 'admin_1' },
);
// verified.status → 'verified'

// Refund — returns the refund transaction doc
const refundTxn = await revenue.repositories.transaction.refund(
  txn._id.toString(), 5000, { reason: 'partial return' },
);
// refundTxn.type → 'refund', refundTxn.flow → 'outflow', refundTxn.amount → 5000

Architecture

createRevenue(config) --> RevenueEngine
  |
  |-- repositories.transaction      extends RevenueRepositoryBase
  |     CRUD inherited (mongokit Repository)
  |     createPaymentIntent, verify, refund, handleWebhook          (domain verbs)
  |     hold, release, split                                        (escrow verbs)
  |     import, match, unmatch, journalize, reject, removeByFeed    (bank-feed verbs)
  |
  |-- repositories.subscription     extends RevenueRepositoryBase
  |     CRUD inherited
  |     activate, cancel, pause, resume                             (domain verbs)
  |
  |-- repositories.settlement       extends RevenueRepositoryBase
  |     CRUD inherited
  |     schedule, processPending, complete, fail                    (domain verbs)
  |
  |-- providers                     ProviderRegistry
  |-- events                        RevenueEventTransport (Arc-compatible)
  |-- models                        Mongoose models (for Arc adapter)


      RevenueRepositoryBase (internal)
        |
        |-- extends mongokit Repository<TDoc>
        |-- protected optsFromCtx(ctx, extra?)   threads RevenueContext into mongokit
        |                                        options bag (uses repoOptionsFromCtx;
        |                                        forwards organizationId, userId,
        |                                        session, requestId + _bypassTenant)
        |-- protected dispatch(event, ctx)       outbox.save (session-bound) →
        |                                        events.publish (PACKAGE_RULES P8)
        \-- protected deps: BaseRevenueRepoDeps  events / outbox? / logger?

Three repos. One scope-threading helper. One dispatch helper. Every domain verb routes its mongokit calls through optsFromCtx(ctx) so multi-tenant scope, audit attribution, and transaction sessions land on every read/write without per-method boilerplate. Every domain event goes through dispatch(event, ctx) so outbox and transport semantics stay consistent across the package.

CRUD, pagination, querying, and policy hooks come from @classytic/mongokit. Domain verbs contain real business logic (state machine transitions, provider calls, event emission). No service layer. No proxy methods.

RevenueConfig

const revenue = await createRevenue({
  // Required
  connection: mongoose.connection,
  defaultCurrency: 'BDT',

  // Providers — register any payment gateway
  providers: {
    manual: new ManualProvider(),
    stripe: new StripeProvider({ apiKey: '...' }),
    bkash: new BkashProvider({ ... }),
  },

  // Modules — progressive opt-in
  modules: {
    subscription: true,          // default: true
    escrow: true,                // default: false
    settlement: true,            // default: false
    commission: {                // commission calculation
      defaultRate: 0.05,
      gatewayFeeRate: 0.025,
    },
  },

  // Event transport — Arc-compatible, drop-in Redis/Outbox
  eventTransport: new RedisEventTransport(ioredis),

  // Bridges — optional external integrations
  bridges: {
    ledger: { onPaymentVerified: async (txn, ctx) => { ... } },
    tax: { computeTax: async (amount, taxClass, ctx) => { ... } },
    notification: { onPaymentVerified: async (txn, ctx) => { ... } },
    currency: { convert: async (amount, from, to) => { ... } },
    customer: { getCustomer: async (id) => { ... } },
    analytics: { trackEvent: async (name, payload) => { ... } },
  },

  // MongoKit plugins — inject per repository
  repositoryPlugins: {
    transaction: [cachePlugin({ adapter: redis })],
  },

  // Schema extensions — add custom fields to models
  schemaOptions: {
    transaction: {
      extraFields: { branch: { type: String }, vatInvoiceNumber: { type: String } },
      extraIndexes: [{ fields: { branch: 1, createdAt: -1 } }],
    },
  },

  multiTenant: true,             // default: true
});

RevenueEngine

interface RevenueEngine {
  config: Readonly<RevenueConfig>;
  models: RevenueModels;              // Mongoose models
  repositories: RevenueRepositories;  // MongoKit repositories (the API surface)
  providers: ProviderRegistry;        // Payment providers
  events: RevenueEventTransport;      // Event transport
  destroy(): Promise<void>;
}

Arc Integration

Arc auto-generates CRUD routes from mongokit repositories. State transitions go through Arc's Action Router (Stripe pattern) — one endpoint per resource, action name in body.

import { defineResource } from '@classytic/arc';
import { requireRoles } from '@classytic/arc/permissions';
import { createAdapter } from '#shared/adapter';

export default defineResource({
  name: 'transaction',
  prefix: '/revenue/transactions',
  adapter: createAdapter(revenue.models.Transaction, revenue.repositories.transaction),
  presets: ['multiTenant', 'softDelete'],

  // State transitions → unified action endpoint POST /:id/action
  actions: {
    verify: {
      handler: (id, data, req) => revenue.repositories.transaction.verify(id, data, req.scope),
      permissions: requireRoles('admin', 'finance-manager'),
      schema: { verifiedBy: { type: 'string' } },
      description: 'Verify a pending payment',
    },
    refund: {
      handler: (id, data, req) =>
        revenue.repositories.transaction.refund(id, data.amount, { reason: data.reason }, req.scope),
      permissions: requireRoles('admin'),
      schema: {
        amount: { type: 'number', minimum: 1 },
        reason: { type: 'string', minLength: 3 },
      },
    },
    hold: {
      handler: (id, data, req) => revenue.repositories.transaction.hold(id, data, req.scope),
      permissions: requireRoles('admin', 'marketplace-ops'),
      schema: { reason: { type: 'string' }, amount: { type: 'number' } },
    },
    release: {
      handler: (id, data, req) => revenue.repositories.transaction.release(id, data, req.scope),
      permissions: requireRoles('admin', 'marketplace-ops'),
      schema: {
        recipientId: { type: 'string' },
        recipientType: { type: 'string' },
        amount: { type: 'number' },
      },
    },
    split: {
      handler: (id, data, req) => revenue.repositories.transaction.split(id, data.rules, req.scope),
      permissions: requireRoles('admin'),
      schema: { rules: { type: 'array' } },
    },
  },

  // Non-state transitions stay as custom routes (webhooks, queries, batch ops)
  routes: [
    {
      method: 'POST', path: '/webhook/:provider',
      handler: (req) =>
        revenue.repositories.transaction.handleWebhook(req.params.provider, req.body, req.headers),
    },
  ],
});

Generated endpoints:

GET    /revenue/transactions                  ← list (QueryParser filters)
GET    /revenue/transactions/:id              ← get single
PATCH  /revenue/transactions/:id              ← raw update (gate with permissions)
DELETE /revenue/transactions/:id              ← soft delete
POST   /revenue/transactions/:id/action       ← verify | refund | hold | release | split
POST   /revenue/transactions/webhook/:provider ← provider webhooks

Frontend usage:

// State transition via action endpoint
await fetch('/revenue/transactions/txn_abc123/action', {
  method: 'POST',
  body: JSON.stringify({ action: 'verify', verifiedBy: 'admin_1' }),
});

await fetch('/revenue/transactions/txn_abc123/action', {
  method: 'POST',
  body: JSON.stringify({ action: 'refund', amount: 5000, reason: 'customer request' }),
});

// Filter list via QueryParser
await fetch('/revenue/transactions?status=verified&amount_gte=1000&sort=-createdAt&page=1&limit=20');

Why actions instead of one endpoint per verb: ~40% fewer routes, single audit point, self-documenting via OpenAPI action enum, type-safe action validation, per-action permissions and schemas. State machine validation lives inside the repository domain verb — STATE_MACHINE.validate(from, to, id) throws InvalidStateTransitionError if the transition is illegal.


Building a Custom Provider

Every payment gateway implements the PaymentProvider abstract class. See @classytic/revenue-manual as the reference implementation.

PaymentProvider Interface

import { PaymentProvider, PaymentIntent, PaymentResult, RefundResult, WebhookEvent } from '@classytic/revenue';
import type { CreateIntentParams, ProviderCapabilities } from '@classytic/revenue/providers';

export class StripeProvider extends PaymentProvider {
  public override readonly name = 'stripe';

  constructor(config: { apiKey: string }) {
    super(config);
  }

  // 1. Create payment intent — called by createPaymentIntent()
  async createIntent(params: CreateIntentParams): Promise<PaymentIntent> {
    const stripe = new Stripe(this.config.apiKey as string);
    const intent = await stripe.paymentIntents.create({
      amount: params.amount,
      currency: params.currency,
      metadata: params.metadata as Stripe.MetadataParam,
    });

    return new PaymentIntent({
      id: intent.id,
      sessionId: null,
      paymentIntentId: intent.id,
      provider: 'stripe',
      status: intent.status,
      amount: intent.amount,
      currency: intent.currency,
      clientSecret: intent.client_secret,      // frontend needs this
      metadata: params.metadata ?? {},
      raw: intent,
    });
  }

  // 2. Verify payment — called by verify()
  async verifyPayment(intentId: string): Promise<PaymentResult> {
    const stripe = new Stripe(this.config.apiKey as string);
    const intent = await stripe.paymentIntents.retrieve(intentId);

    return new PaymentResult({
      id: intent.id,
      provider: 'stripe',
      status: intent.status === 'succeeded' ? 'succeeded'
        : intent.status === 'requires_action' ? 'requires_action'
        : intent.status === 'processing' ? 'processing'
        : 'failed',
      amount: intent.amount,
      currency: intent.currency,
      paidAt: intent.status === 'succeeded' ? new Date() : undefined,
      metadata: {},
      raw: intent,
    });
  }

  // 3. Get status — same as verify for most providers
  async getStatus(intentId: string): Promise<PaymentResult> {
    return this.verifyPayment(intentId);
  }

  // 4. Refund — called by refund()
  async refund(paymentId: string, amount?: number | null, options?: { reason?: string }): Promise<RefundResult> {
    const stripe = new Stripe(this.config.apiKey as string);
    const refund = await stripe.refunds.create({
      payment_intent: paymentId,
      amount: amount ?? undefined,
      reason: options?.reason as any,
    });

    return new RefundResult({
      id: refund.id,
      provider: 'stripe',
      status: refund.status === 'succeeded' ? 'succeeded' : 'processing',
      amount: refund.amount,
      currency: refund.currency,
      refundedAt: new Date(),
      reason: options?.reason,
      metadata: {},
      raw: refund,
    });
  }

  // 5. Handle webhook — called by handleWebhook()
  async handleWebhook(payload: unknown, headers?: Record<string, string>): Promise<WebhookEvent> {
    const stripe = new Stripe(this.config.apiKey as string);
    const sig = headers?.['stripe-signature'] ?? '';
    const event = stripe.webhooks.constructEvent(payload as string, sig, this.config.webhookSecret as string);

    return new WebhookEvent({
      id: event.id,
      provider: 'stripe',
      type: event.type,
      data: {
        paymentIntentId: (event.data.object as any).id,
        sessionId: (event.data.object as any).id,
      },
      createdAt: new Date(event.created * 1000),
      raw: event,
    });
  }

  // 6. Capabilities — tells revenue what this provider supports
  override getCapabilities(): ProviderCapabilities {
    return {
      supportsWebhooks: true,
      supportsRefunds: true,
      supportsPartialRefunds: true,
      requiresManualVerification: false,
    };
  }
}

Required Methods

| Method | Called By | Returns | Purpose | |---|---|---|---| | createIntent(params) | repo.createPaymentIntent() | PaymentIntent | Initialize payment with gateway | | verifyPayment(intentId) | repo.verify() | PaymentResult | Check payment status with gateway | | getStatus(intentId) | Direct call | PaymentResult | Poll payment status | | refund(paymentId, amount?, options?) | repo.refund() | RefundResult | Process refund with gateway | | handleWebhook(payload, headers?) | repo.handleWebhook() | WebhookEvent | Parse incoming webhook | | getCapabilities() | Engine | ProviderCapabilities | Declare supported features |

PaymentResult Status Map

| Provider Status | Maps To | Revenue Action | |---|---|---| | 'succeeded' | TRANSACTION_STATUS.VERIFIED | Mark verified, call ledger bridge | | 'failed' | TRANSACTION_STATUS.FAILED | Mark failed | | 'processing' | TRANSACTION_STATUS.PROCESSING | Wait for webhook | | 'requires_action' | TRANSACTION_STATUS.REQUIRES_ACTION | Return to frontend for 3DS/OTP |

Register Provider

const revenue = await createRevenue({
  connection,
  defaultCurrency: 'BDT',
  providers: {
    manual: new ManualProvider(),
    stripe: new StripeProvider({ apiKey: process.env.STRIPE_KEY }),
    bkash: new BkashProvider({ appKey: '...', appSecret: '...' }),
  },
});

// Use by gateway name
const txn = await revenue.repositories.transaction.createPaymentIntent({
  amount: 5000,
  gateway: 'bkash',   // matches key in providers map
});

Event System

Revenue uses RevenueEventTransport — a structural superset of Arc's DomainEvent. Any Arc transport drops in with zero adapters.

RevenueDomainEvent

interface RevenueDomainEvent<T = unknown> {
  type: string;              // 'revenue:payment.verified'
  payload: T;                // event-specific data
  meta: {
    id: string;              // crypto.randomUUID()
    timestamp: Date;
    resource?: string;       // 'transaction', 'subscription', 'settlement'
    resourceId?: string;     // publicId (txn_..., sub_..., stl_...)
    userId?: string;         // from RevenueContext.actorId
    organizationId?: string; // from RevenueContext.organizationId
    correlationId?: string;  // from RevenueContext.traceId
    aggregate?: string;      // 'revenue'
    version?: number;
    causationId?: string;
    tags?: string[];
  };
}

RevenueEventTransport

interface RevenueEventTransport {
  publish(event: RevenueDomainEvent): Promise<void>;
  subscribe?(pattern: string, handler: RevenueEventHandler): Promise<() => void>;
  close?(): Promise<void>;
}

Drop-in Transports

// Arc Redis
import { RedisEventTransport } from '@classytic/arc/events';
const revenue = await createRevenue({
  eventTransport: new RedisEventTransport(ioredis),
});

// Arc Outbox (guaranteed delivery)
import { EventOutbox } from '@classytic/arc/events';
const outbox = new EventOutbox({ store: mongoOutboxStore, transport: redisTransport });
const revenue = await createRevenue({
  eventTransport: { publish: (event) => outbox.store(event) },
});

// No events (testing)
import { NoopRevenueEventTransport } from '@classytic/revenue';
const revenue = await createRevenue({
  eventTransport: new NoopRevenueEventTransport(),
});

Pattern Matching

await revenue.events.subscribe?.('revenue:payment.*', handler);   // payment.verified, payment.refunded, ...
await revenue.events.subscribe?.('revenue:*', handler);            // all revenue events
await revenue.events.subscribe?.('*', handler);                    // everything
await revenue.events.subscribe?.('revenue:escrow.held', handler);  // exact match

Event Reference

| Event | Payload | |---|---| | revenue:monetization.created | { monetizationType, transaction } | | revenue:payment.verified | { transaction, paymentResult, verifiedBy } | | revenue:payment.failed | { transaction, paymentResult } | | revenue:payment.refunded | { transaction, refundTransaction, refundAmount, reason } | | revenue:payment.requires_action | { transaction, paymentResult } | | revenue:payment.processing | { transaction, paymentResult } | | revenue:subscription.activated | { subscription, activatedAt } | | revenue:subscription.cancelled | { subscription, immediate, reason } | | revenue:subscription.paused | { subscription, reason } | | revenue:subscription.resumed | { subscription, extendPeriod } | | revenue:escrow.held | { transaction, heldAmount, reason } | | revenue:escrow.released | { transaction, releaseAmount, recipientId, isFullRelease } | | revenue:escrow.split | { transaction, splits, organizationPayout } | | revenue:settlement.scheduled | { settlement, scheduledAt } | | revenue:settlement.processing | { settlement, processedAt } | | revenue:settlement.completed | { settlement, completedAt } | | revenue:settlement.failed | { settlement, reason, retry } | | revenue:webhook.processed | { webhookType, provider, transaction } |


Bridges

All bridges are optional. Every method is optional. Features degrade gracefully when bridge is absent.

interface RevenueBridges {
  ledger?: LedgerBridge;             // post journal entries on payment events
  tax?: TaxBridge;                    // compute tax for amounts
  notification?: NotificationBridge;  // send emails/SMS on lifecycle events
  currency?: CurrencyBridge;          // multi-currency conversion
  customer?: CustomerBridge;          // resolve customer details
  analytics?: AnalyticsBridge;        // track events for BI
  source?: SourceBridge;              // resolve polymorphic source documents (Order, Invoice, Stripe charge, etc.)
}

SourceBridge — Polymorphic Source Resolution

Revenue stores sourceId as a String so it works with any ID format: ObjectId hex, UUIDs, Stripe IDs, REST API resource IDs. Hosts implement SourceBridge to teach revenue how to load source documents — works for any deployment topology.

// Same MongoDB, single connection (most common)
const revenue = await createRevenue({
  connection,
  bridges: {
    source: {
      async resolve(sourceId, sourceModel) {
        const Model = mongoose.connection.models[sourceModel];
        return Model ? await Model.findById(sourceId).lean() : null;
      },
    },
  },
});

// Microservices (different DBs / HTTP)
bridges: {
  source: {
    async resolve(sourceId, sourceModel) {
      if (sourceModel === 'Order') return await fetch(`http://orders-svc/${sourceId}`).then(r => r.json());
      if (sourceModel === 'Invoice') return await invoiceDb.collection('invoices').findOne({ _id: sourceId });
      return null;
    },
  },
}

// External systems (Stripe, Postgres)
bridges: {
  source: {
    async resolve(sourceId, sourceModel) {
      if (sourceModel === 'StripeCharge') return await stripe.charges.retrieve(sourceId);
      if (sourceModel === 'PostgresOrder') {
        const { rows } = await pg.query('SELECT * FROM orders WHERE id = $1', [sourceId]);
        return rows[0];
      }
      return null;
    },
  },
}

Use it in custom Arc routes for enrichment:

{
  method: 'GET',
  path: '/:id/with-source',
  handler: async (req) => {
    const txn = await revenue.repositories.transaction.getById(req.params.id);
    const source = txn.sourceId
      ? await revenue.config.bridges?.source?.resolve?.(txn.sourceId, txn.sourceModel, req.scope)
      : null;
    return { ...txn, source };
  },
}

For batch/list endpoints, use resolveMany to avoid N+1 queries:

async resolveMany(refs, ctx) {
  // Group by sourceModel, batch fetch, return Map<sourceId, doc>
}

LedgerBridge

interface LedgerBridge {
  onPaymentVerified?(transaction: Record<string, unknown>, ctx: RevenueContext): Promise<void>;
  onRefundProcessed?(original: Record<string, unknown>, refund: Record<string, unknown>, ctx: RevenueContext): Promise<void>;
  onSettlementCompleted?(settlement: Record<string, unknown>, ctx: RevenueContext): Promise<void>;
}

TaxBridge

interface TaxBridge {
  computeTax?(amount: number, taxClass: string, ctx: RevenueContext): Promise<{ rate: number; amount: number; inclusive: boolean }>;
}

Soft Delete & Force Cleanup

All financial repositories use mongokit's softDeletePlugin with ttlDays: 365. Calling delete() sets deletedAt instead of removing the document. After 365 days, MongoDB's TTL index automatically removes the document.

Inherited methods (from softDeletePlugin)

// Soft delete (sets deletedAt)
await revenue.repositories.transaction.delete(id);

// Restore a soft-deleted document
await revenue.repositories.transaction.restore(id);

// List soft-deleted documents
const trash = await revenue.repositories.transaction.getDeleted({ page: 1, limit: 50 });

// Read a specific soft-deleted document
const doc = await revenue.repositories.transaction.getById(id, { includeDeleted: true });

Custom retention period

For compliance (US/EU financial records: ~7 years), override the plugin via repositoryPlugins:

import { softDeletePlugin } from '@classytic/mongokit';

const revenue = await createRevenue({
  connection,
  defaultCurrency: 'USD',
  repositoryPlugins: {
    transaction: [softDeletePlugin({ ttlDays: 2555 })],   // 7 years
    subscription: [softDeletePlugin({ ttlDays: 2555 })],
  },
});

Force-delete (admin / GDPR right-to-be-forgotten)

The repository's Model is the underlying Mongoose model — use it for raw operations when needed:

// Custom Arc route for surgical force-delete
{
  method: 'DELETE',
  path: '/:id/force',
  permissions: requireRoles('superadmin', 'compliance-officer'),
  handler: async (req) => {
    const id = req.params.id;

    // Verify it IS soft-deleted first
    const doc = await revenue.repositories.transaction.getById(id, {
      includeDeleted: true,
      throwOnNotFound: false,
    });
    if (!doc) return { error: 'Not found' };
    if (!(doc as any).deletedAt) {
      return { error: 'Document is not soft-deleted. Soft-delete first.' };
    }

    // Hard delete via raw Mongoose model
    await revenue.repositories.transaction.Model.deleteOne({ _id: id });

    // Audit
    await auditBridge.log({
      action: 'force_delete',
      resource: 'transaction',
      resourceId: doc.publicId,
      actor: req.user.id,
      reason: req.body.reason,
    });

    return { success: true, publicId: doc.publicId };
  },
}

Bulk force-cleanup (admin)

{
  method: 'POST',
  path: '/force-cleanup',
  permissions: requireRoles('superadmin'),
  handler: async (req) => {
    const { olderThanDays = 30, dryRun = true } = req.body;
    const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
    const query = { deletedAt: { $ne: null, $lte: cutoff } };

    if (dryRun) {
      const count = await revenue.repositories.transaction.Model.countDocuments(query);
      return { dryRun: true, wouldDelete: count };
    }

    const result = await revenue.repositories.transaction.Model.deleteMany(query);
    return { deleted: result.deletedCount };
  },
}

Trash bin endpoint

{
  method: 'GET',
  path: '/trash',
  permissions: requireRoles('admin'),
  handler: (req) => revenue.repositories.transaction.getDeleted({
    page: req.query.page ?? 1,
    limit: req.query.limit ?? 50,
    sort: { deletedAt: -1 },
  }),
}

Cleanup strategies

| Scenario | Approach | |---|---| | Default retention | TTL plugin handles it — no code needed | | Compliance retention (7yr) | Override softDeletePlugin({ ttlDays: 2555 }) | | Test data cleanup | Model.deleteMany({ deletedAt: { $ne: null } }) in test teardown | | GDPR right-to-be-forgotten | Custom force-delete endpoint with audit log | | Database size emergency | Bulk force-cleanup with dry-run support |


Domain Verbs Reference

TransactionRepository

| Method | Input | Returns | Description | |---|---|---|---| | createPaymentIntent(params, ctx?) | { amount, gateway, data?, metadata?, idempotencyKey? } | TransactionDocument | Create transaction + call provider | | verify(intentId, options?, ctx?) | intentId, { verifiedBy? } | TransactionDocument | Verify via provider, update status | | refund(txnId, amount?, options?, ctx?) | txnId, amount?, { reason? } | TransactionDocument (refund) | Create refund transaction | | handleWebhook(provider, payload, headers?, ctx?) | provider name + raw payload | TransactionDocument \| null | Process webhook, update transaction | | hold(txnId, options?, ctx?) | txnId, { amount?, reason?, holdUntil? } | TransactionDocument | Place escrow hold | | release(txnId, options, ctx?) | txnId, { recipientId, recipientType, amount? } | TransactionDocument | Release escrow | | split(txnId, rules, ctx?) | txnId, [{ type, recipientId, recipientType, rate }] | TransactionDocument | Multi-party split |

SubscriptionRepository

| Method | Input | Returns | Description | |---|---|---|---| | activate(subId, options?, ctx?) | subId, { timestamp? } | SubscriptionDocument | Activate, calculate period end | | cancel(subId, options?, ctx?) | subId, { immediate?, reason? } | SubscriptionDocument | Cancel immediately or at period end | | pause(subId, options?, ctx?) | subId, { reason? } | SubscriptionDocument | Pause subscription | | resume(subId, options?, ctx?) | subId, { extendPeriod? } | SubscriptionDocument | Resume, optionally extend |

SettlementRepository

| Method | Input | Returns | Description | |---|---|---|---| | schedule(params, ctx?) | { organizationId, recipientId, amount, payoutMethod, ... } | SettlementDocument | Schedule payout | | processPending(options?, ctx?) | { limit?, organizationId?, dryRun? } | { processed, succeeded, failed, settlements } | Batch process pending | | complete(stlId, details?, ctx?) | stlId, { transferReference?, transactionHash? } | SettlementDocument | Mark completed | | fail(stlId, reason, options?, ctx?) | stlId, reason, { retry?, code? } | SettlementDocument | Mark failed or retry |

All inherited mongokit methods also available: getAll, getById, getByQuery, getOne, create, update, delete, count, exists, distinct, aggregate, withTransaction.


Stripe-Style IDs

Via mongokit customIdPlugin + prefixedId:

Transaction:  txn_a7b3xk9m2p1q4d5e6f
Subscription: sub_x1y2z3a4b5c6d7e8f9g
Settlement:   stl_m9n8o7p6q5r4s3t2u1v

Internal _id stays as MongoDB ObjectId. publicId is the external-facing identifier.

Zod Schemas

Exported at @classytic/revenue/schemas for Arc OpenAPI auto-generation and runtime validation.

import {
  transactionCreateSchema, transactionUpdateSchema, transactionListFilterSchema,
  subscriptionCreateSchema, subscriptionListFilterSchema,
  settlementCreateSchema, settlementListFilterSchema,
  paymentIntentSchema, paymentVerifySchema, refundSchema,
  escrowHoldSchema, escrowReleaseSchema, splitRuleSchema,
} from '@classytic/revenue/schemas';

Subpath Exports

| Import | Contents | |---|---| | @classytic/revenue | Main entry — engine, repos, types, everything | | @classytic/revenue/schemas | Zod validators | | @classytic/revenue/enums | Status/flow/type enums | | @classytic/revenue/events | Event types, constants, transports | | @classytic/revenue/providers | PaymentProvider base, response classes | | @classytic/revenue/bridges | Bridge interfaces | | @classytic/revenue/utils | Calculators (commission, tax, splits), Money class | | @classytic/revenue/core | State machines, errors, Result type |

Peer Dependencies

{
  "@classytic/mongokit": ">=3.5.6",
  "mongoose": ">=9.0.0",
  "zod": ">=4.0.0"
}

License

MIT