import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { CommissionCollection } from '../collections/CommissionCollection.js'; import { EarnerCollection } from '../collections/EarnerCollection.js'; import { Commission } from '../models/Commission.js'; import { EarningEvent } from '../models/EarningEvent.js'; import { CommissionPlanComponent } from '../types.js'; /** Input for {@link CommissionCalculationService.calculateForEvent}. */ export interface CommissionCalculationInput { /** The (persisted) earning event to calculate from. */ event: EarningEvent; /** Snapshot reference recorded on every created Commission. */ planKey: string; /** Snapshot reference recorded on every created Commission. */ planVersion: number; /** The calculation terms to apply (typically from a frozen snapshot). */ components: CommissionPlanComponent[]; /** The earner the commissions belong to. */ earnerId: string; /** * Split share (0–1) this earner receives; defaults to 1. Callers running * a split invoke the service once per earner with the shares and a shared * `splitGroupId`. */ shareFraction?: number; /** Groups the sibling commissions of one split. */ splitGroupId?: string; /** Generic polymorphic terms-snapshot reference (kind). */ termsSnapshotKind?: string; /** Generic polymorphic terms-snapshot reference (id). */ termsSnapshotId?: string; /** * Clearing window in days: created commissions get * `clearingEndsAt = event.occurredAt + clearingDays`. Omitted → no * clearing (`clearingEndsAt: null`, immediately sweepable). */ clearingDays?: number; /** * Currency the plan/terms are denominated in. When provided and different * from `event.currency`, every matching component skips with reason * `'currency_mismatch'` (this module performs no FX). Omitted → the event * currency is taken as authoritative and no mismatch is possible. */ currency?: string; /** * Resolves how many occurrences of a component this earner has already * consumed under these terms (commissions from PRIOR events — the current * event must not be counted). Drives `one_time` / `maxOccurrences` limits * and the `occurrenceIndex` in the dedupe key and trace. Omitted → * occurrence count `0`, i.e. recurrence limits are NOT enforced. */ occurrenceCountResolver?: (componentKey: string) => Promise; /** * Anchor for `windowMonths` recurrence checks (e.g. an agreement's * effective date). Omitted → window checks are skipped. */ anchorAt?: Date; } /** One component the calculation declined, and why. */ export interface CommissionComponentSkip { componentKey: string; /** * `'net_basis_undefined'` | `'margin_basis_undefined'` | * `'fixed_amount_missing'` | `'rate_missing'` | `'custom_basis_missing'` * | `'currency_mismatch'` | `'occurrence_limit_reached'` | * `'outside_recurrence_window'` */ reason: string; } /** Result of {@link CommissionCalculationService.calculateForEvent}. */ export interface CommissionCalculationResult { /** Commissions newly created by THIS call. */ created: Commission[]; /** Components that produced nothing, with reasons. */ skipped: CommissionComponentSkip[]; /** * Idempotent replays: commissions that already existed for this * (event, terms, component, earner) tuple. Never re-created, never * mutated, and never in `created`. */ existing: Commission[]; } export declare class CommissionCalculationService { private readonly commissions; /** * Optional earner lookup for the tenant-lane guard. When provided, * `calculateForEvent` refuses an earner from a different tenant lane * than the event (a cross-tenant `earnerId` would create a commission * payable to another tenant's account). `static create()` always wires * it; direct constructors may omit it for narrow test fixtures. */ private readonly earners?; constructor(commissions: CommissionCollection, /** * Optional earner lookup for the tenant-lane guard. When provided, * `calculateForEvent` refuses an earner from a different tenant lane * than the event (a cross-tenant `earnerId` would create a commission * payable to another tenant's account). `static create()` always wires * it; direct constructors may omit it for narrow test fixtures. */ earners?: EarnerCollection | undefined); static create(classOptions?: SmrtClassOptions): Promise; /** * Calculate commissions for one event × one earner × a component set. * * For each component whose `trigger` matches `event.eventKind` (or `'*'` * — non-matching components are silently filtered, not "skipped"): * * 1. **Idempotency** — if a Commission already exists for this * (event, terms, component, earner) tuple, it is returned in * `existing` and nothing else runs for the component. * 2. **Currency** — `input.currency` (when given) must equal the event's; * otherwise skip `'currency_mismatch'`. * 3. **Recurrence** — `one_time` components skip * `'occurrence_limit_reached'` once the resolver reports ≥ 1 prior * occurrence; `recurring` components honor `maxOccurrences` and * `windowMonths` (events after `anchorAt + windowMonths` skip * `'outside_recurrence_window'`). * 4. **Basis** — gross → `grossAmountCents`; net → `netAmountCents` * (skip `'net_basis_undefined'` when null — net is explicit, NEVER * derived from gross); margin → `marginCents` (skip * `'margin_basis_undefined'` when null); fixed → `fixedAmountCents`; * custom → `getCustomBases()[customBasisKey]` (skip * `'custom_basis_missing'`). * 5. **Amount** — `roundCents(base * rate * shareFraction)`; for `fixed`, * `roundCents(fixedAmountCents * shareFraction)` with `rate` recorded * as `0`. Rounding happens exactly once, on the final product. * * Every created Commission is persisted `pending`, carries the event's * tenant/currency/source, a complete {@link CommissionCalculationTrace}, * `clearingEndsAt` when `clearingDays` was given, and the dedupe key * `` `${event.dedupeKey}:${termsSnapshotId || planKey + '@' + planVersion}:${componentKey}:${earnerId}:${occurrenceIndex}` ``. */ calculateForEvent(input: CommissionCalculationInput): Promise; /** * Calendar-month addition (UTC). JS `setUTCMonth` semantics: day-of-month * overflow rolls into the next month (Jan 31 + 1 month → Mar 2/3), which * is acceptable for coarse recurrence windows. */ private static addMonths; } export default CommissionCalculationService; //# sourceMappingURL=CommissionCalculationService.d.ts.map