import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { AttributionExceptionCollection } from '../collections/AttributionExceptionCollection.js'; import { AttributionPolicyCollection } from '../collections/AttributionPolicyCollection.js'; import { ReferralCollection } from '../collections/ReferralCollection.js'; import { ReferralProgramCollection } from '../collections/ReferralProgramCollection.js'; import { ReferralTouchCollection } from '../collections/ReferralTouchCollection.js'; import { ReferrerCollection } from '../collections/ReferrerCollection.js'; import { AttributionException } from '../models/AttributionException.js'; import { Referral } from '../models/Referral.js'; import { ReferralTouch } from '../models/ReferralTouch.js'; /** * Thrown by {@link AttributionService.override} when the target already has * a QUALIFIED referral: qualified credit is governed by its term snapshot * and commission adjustments downstream — re-attribution would silently * orphan agreed terms. */ export declare class QualifiedReferralOverrideError extends Error { constructor(referralIds: string[]); } /** Collaborators for {@link AttributionService}. */ export interface AttributionServiceDeps { touches: ReferralTouchCollection; referrals: ReferralCollection; exceptions: AttributionExceptionCollection; policies: AttributionPolicyCollection; programs: ReferralProgramCollection; referrers: ReferrerCollection; } /** Why {@link AttributionService.resolve} refused to attribute. */ export type AttributionRefusalReason = 'existing_client_ineligible' | 'no_active_policy' | 'no_eligible_touches'; /** Input for {@link AttributionService.resolve}. */ export interface ResolveAttributionInput { /** Generic qualifying target the referrals will point at. */ targetKind: string; /** Identifier within the `targetKind` namespace. */ targetId: string; /** The program to attribute under. */ programId: string; /** Prospect identity to gather touches by (in addition to the target). */ subjectKind?: string; /** Identifier within the `subjectKind` namespace. */ subjectId?: string; /** * The prospect's own profile id, when known — enables the self-referral * eligibility check against each candidate referrer's `profileId`. */ subjectProfileId?: string; /** * Caller's statement that the prospect is already a client. Policies with * `allowExistingClients: false` refuse the whole resolution. */ isExistingClient?: boolean; /** Overrides the program's `defaultAttributionPolicyKey`. */ policyKey?: string; /** Clock override for deterministic tests. */ now?: Date; } /** Result of {@link AttributionService.resolve}. */ export interface AttributionResolution { /** * The attributed referral(s): one for sole credit, several siblings for * `split` mode. Empty when an exception was raised, the resolution was * refused, or nothing was eligible. */ referrals: Referral[]; /** The conflict parked for review, when the policy could not conclude. */ exception: AttributionException | null; /** Why nothing was attributed, when refused outright. */ refused?: AttributionRefusalReason; } /** Input for {@link AttributionService.recordManualAssignment}. */ export interface RecordManualAssignmentInput { referrerId: string; programId: string; targetKind: string; targetId: string; /** Prospect identity; defaults to the target pair when omitted. */ subjectKind?: string; subjectId?: string; /** Who recorded the assignment (audit). Required. */ actorProfileId: string; /** Free-form rationale recorded in the touch evidence. */ reason?: string; /** When the assignment applies from; defaults to now. */ occurredAt?: Date; } /** One credit award inside a resolution/override. */ export interface AttributionAward { referrerId: string; /** Credit share (0–1]; awards must sum to 1.0 (±0.0001). */ creditFraction: number; } /** Input for {@link AttributionService.resolveException}. */ export interface ResolveExceptionInput { exceptionId: string; awards: AttributionAward[]; /** Non-empty rationale. Required — resolutions are audited. */ resolutionReason: string; /** Who resolved the exception (audit). Required. */ actorProfileId: string; /** Clock override for deterministic tests. */ now?: Date; } /** Input for {@link AttributionService.override}. */ export interface OverrideAttributionInput { targetKind: string; targetId: string; programId: string; awards: AttributionAward[]; /** Non-empty rationale. Required — overrides are audited. */ resolutionReason: string; /** Who performed the override (audit). Required. */ actorProfileId: string; /** Clock override for deterministic tests. */ now?: Date; } /** Result of {@link AttributionService.resolveException} / {@link AttributionService.override}. */ export interface AttributionAwardResult { /** The attributed referral(s) the awards created. */ referrals: Referral[]; /** The resolved exception row auditing the decision. */ exception: AttributionException; } export declare class AttributionService { private readonly deps; constructor(deps: AttributionServiceDeps); static create(classOptions?: SmrtClassOptions): Promise; /** * Resolve attribution for one target under one program. * * IDEMPOTENT: existing non-disqualified `attributed`/`qualified` * referrals for the target+program are returned as-is (no new rows), and * an already-OPEN exception for the target+program is returned instead of * a duplicate. * * Flow: * 1. **Policy** — the program's `defaultAttributionPolicyKey` (or the * `policyKey` override) is resolved via `latestActiveByKey`; no active * version → `refused: 'no_active_policy'`. * 2. **Existing-client gate** — `isExistingClient` with a policy that * disallows it → `refused: 'existing_client_ineligible'`. * 3. **Candidates** — touches gathered by the subject pair (when given) * and by the target pair, deduplicated, restricted to the program and * to `occurredAt` within `[now - windowDays, now]`. * 4. **Eligibility** — self-referrals (candidate referrer's `profileId` * equals `subjectProfileId`) are dropped unless the policy allows * them. Nothing left → `refused: 'no_eligible_touches'`. * 5. **Conflicts** — `conflictBehavior: 'review'` with more than one * distinct eligible referrer, exact-timestamp ties between distinct * referrers in `first_touch`/`last_touch`, or competing manual * assignments in `assigned` mode → an OPEN AttributionException * carrying the candidates; NO referral rows. * 6. **Credit** — otherwise the mode elects the winner(s): * `first_touch`/`last_touch` (earliest/latest), `assigned` (most * recent manual assignment; assignments beat clicks by definition), * or `split` (every distinct eligible referrer, equal fractions * rounded to 4 dp with the remainder on the last, one shared * `splitGroupId`). Referrals are created `pending → attributed` with * the policy pin, winning touch, `attributedAt = now`, and * `expiresAt = now + windowDays`. */ resolve(input: ResolveAttributionInput): Promise; /** * Record an operator's manual assignment as an immutable * `manual_assignment` ReferralTouch (evidence carries the actor, reason, * and target) and return it. The caller then runs {@link resolve} — under * an `assigned`-mode policy the assignment wins over click evidence. */ recordManualAssignment(input: RecordManualAssignmentInput): Promise; /** * Resolve an OPEN exception by awarding credit explicitly. * * Requires a non-empty `resolutionReason` (throws otherwise) and awards * whose `creditFraction`s sum to 1.0 (±0.0001) across distinct referrers. * Creates the attributed referral(s) from the exception's target/program * (split semantics — shared `splitGroupId` — when several awards), then * marks the exception resolved (`resolutionMode: 'override'`, the award * audit fields, `resolvedReferralIds`). */ resolveException(input: ResolveExceptionInput): Promise; /** * Re-attribute an already-attributed target. * * Requires a non-empty `resolutionReason` and awards summing to 1.0. * REFUSES (throws {@link QualifiedReferralOverrideError}) when the target * has a QUALIFIED referral — those are governed by term snapshots and * commission adjustments downstream. Otherwise: disqualifies the existing * `attributed` referrals for the target+program (stamping the audit * exception's id into their metadata), creates the newly awarded * attributed referral(s), and writes a RESOLVED AttributionException as * the audit record (candidates = the displaced + awarded referrers). * * The new referrals inherit the POLICY PIN the displaced credit carried * (an override re-decides WHO earned the introduction, not which policy * version governed it); the program's current default active policy is * the fallback when nothing was displaced. */ override(input: OverrideAttributionInput): Promise; private requireProgram; /** * The active policy governing a resolution: the explicit `policyKey` * override when given, else the program's default key; resolved to its * latest ACTIVE version. `null` when no key or no active version. */ private resolvePolicy; /** * Candidate touches for a resolution: gathered by the subject pair (when * given) and by the target pair, deduplicated, restricted to the program, * to `occurredAt` within `[now - windowDays, now]`, and to referrers that * pass the self-referral gate. Sorted by `occurredAt` ascending. */ private gatherEligibleTouches; /** Create an OPEN exception carrying the competing candidates. */ private createOpenException; /** * The policy pin an exception was raised under (stamped into its * metadata at creation); falls back to the program's current default * active policy for exceptions created without one. */ private resolvePolicyPin; /** * The policy pin an override re-attributes under: the pin the displaced * credit carried (looking the exact version's `windowDays` back up for * the new expiry), falling back to the program's current default active * policy when nothing pinned was displaced. */ private resolveOverridePolicyPin; /** * Create one referral as `pending`, transition it `pending → attributed` * (stamping `attributedAt`), set the policy pin and expiry, and save. */ private createAttributedReferral; /** * Equal split fractions for `n` referrers, each rounded to 4 decimal * places, with the LAST adjusted so the set sums to exactly 1.0. */ private static equalFractions; private static assertResolutionReason; /** * Attribution decisions are audited — a blank actor would permanently * resolve/override credit with an empty `resolvedByProfileId`, defeating * the audit trail. */ private static assertActor; private static assertAwards; } export default AttributionService; //# sourceMappingURL=AttributionService.d.ts.map