import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { EarnerCollection } from '../collections/EarnerCollection.js'; import { EarnerSourceAttributionCollection } from '../collections/EarnerSourceAttributionCollection.js'; import { Earner } from '../models/Earner.js'; import { EarnerSourceAttribution } from '../models/EarnerSourceAttribution.js'; import { EarnerSourceAttributionStatus } from '../types.js'; /** Collaborators for {@link EarnerAttributionService}. */ export interface EarnerAttributionServiceDeps { earners: EarnerCollection; attributions: EarnerSourceAttributionCollection; } /** * Why a source key did not resolve to an active earner. Every reason is * fail-closed — the key resolves to nothing rather than to a guess. * * - `no_mapping` — no attribution row for the key. * - `mapping_inactive` — row(s) exist but none is `active`. * - `ambiguous_mapping` — more than one ACTIVE row for the key (resolving * without tenant context across tenants, or duplicate global rows minted * outside the model layer — the unique index treats NULL tenants as * distinct). Repair by deactivating/deleting the extras, then re-resolve. * - `earner_not_found` — the mapping's earner does not exist or is not * visible in the current tenant scope. * - `earner_not_active` — the earner exists but is `pending`/`suspended`. */ export type EarnerSourceResolutionRefusal = 'no_mapping' | 'mapping_inactive' | 'ambiguous_mapping' | 'earner_not_found' | 'earner_not_active'; /** Result of {@link EarnerAttributionService.resolveActiveEarnerBySource}. */ export interface ResolveActiveEarnerResult { /** The resolved ACTIVE earner, or `null` with a {@link reason}. */ earner: Earner | null; /** The active mapping that resolved, when {@link earner} is set. */ attribution: EarnerSourceAttribution | null; /** Why resolution failed — set exactly when {@link earner} is `null`. */ reason?: EarnerSourceResolutionRefusal; } /** Result of {@link EarnerAttributionService.resolveActiveEarnersBySources}. */ export interface ResolveActiveEarnersBySourcesResult { /** Requested sourceId → resolved active earner (resolved keys only). */ earnersBySourceId: Map; /** Requested sourceId → the active mapping that resolved it. */ attributionsBySourceId: Map; /** Keys that did not resolve, each with its fail-closed reason. */ unresolved: { sourceId: string; reason: EarnerSourceResolutionRefusal; }[]; } /** Input for {@link EarnerAttributionService.registerAttribution}. */ export interface RegisterAttributionInput { earnerId: string; sourceKind: string; sourceId: string; /** * Tenant for the mapping. Defaults to the earner's own `tenantId` so * registrations from operator/scheduled contexts land in the earner's * tenant. When given explicitly it MUST equal the earner's tenant — a * mapping lives in its earner's tenant (model save guard). */ tenantId?: string | null; status?: EarnerSourceAttributionStatus; metadata?: string; } /** Result of {@link EarnerAttributionService.registerAttribution}. */ export interface RegisterAttributionResult { attribution: EarnerSourceAttribution; /** `true` when THIS call created the mapping (vs updating in place). */ created: boolean; /** The earner the key previously mapped to, when re-pointed. */ previousEarnerId: string | null; } export declare class EarnerAttributionService { private readonly deps; constructor(deps: EarnerAttributionServiceDeps); static create(classOptions?: SmrtClassOptions): Promise; /** * Register (or re-point) the mapping for one external key WITHIN ONE * TENANT. The registration's target tenant is `input.tenantId` when * given, else the earner's own `tenantId` — and the update-vs-create * decision considers only that tenant's rows, so an operator/scheduled * registration (no tenant context) can never re-point or re-tenant * ANOTHER tenant's mapping for the same key, and legitimate per-tenant * mappings of one key are never mistaken for duplicates. A mapping's * tenant is fixed at registration (re-tenanting is not supported). * * Idempotent: an existing target-tenant mapping is updated in place — * never duplicated — and the displaced earner is reported. Throws when * the earner does not exist, when the key is incomplete, or when the key * already holds MULTIPLE rows within the target tenant (duplicates * minted outside the model layer — repair before registering again). * * Two concurrent first registrations of the same key converge through the * natural-key upsert (the adapters' null-aware upsert covers NULL-tenant * keys too — last write wins). Should a duplicate global row still arrive * outside the model layer, the lookups fail closed on it until repaired. */ registerAttribution(input: RegisterAttributionInput): Promise; /** * Resolve the ACTIVE earner for one external key. Exactly two queries * regardless of how many earners exist. Fail-closed: `earner` is `null` * with a typed {@link ResolveActiveEarnerResult.reason} unless the key * maps unambiguously to one active mapping whose earner is active. */ resolveActiveEarnerBySource(input: { sourceKind: string; sourceId: string; }): Promise; /** * Resolve the ACTIVE earners for a batch of external keys sharing one * kind. Query work is bounded by the REQUESTED ids — one indexed * attribution `IN` query plus one earner load for the mapped ids — never * a scan of all active earners. Duplicate/empty requested ids are * deduped; every requested id comes back either in * `earnersBySourceId` or in `unresolved` with its fail-closed reason. */ resolveActiveEarnersBySources(input: { sourceKind: string; sourceIds: string[]; }): Promise; } export default EarnerAttributionService; //# sourceMappingURL=EarnerAttributionService.d.ts.map