feat(09-03): add RouteToUser types and resolver registry

- Add RouteToUser, ResolvedRecipient, NotifyResolver, UserRouteFallback,
  UserRouteFallbackReason types to lib/types/pipeline.ts (ROUTE-01)
- Create lib/services/pipeline-steps/notify-resolvers.ts with three v1
  resolvers: direct_email, pulse_user_id, autotask_resource_email (ROUTE-02)
- Resolver registry as Map<string, NotifyResolver> with registerResolver()
  and resolveRecipient() dispatcher with try/catch error handling
This commit is contained in:
lorentz 2026-05-10 07:27:00 -04:00
parent 485053c639
commit d27462f713
2 changed files with 155 additions and 0 deletions

View file

@ -192,3 +192,55 @@ export interface UserEventSubscription {
enabled: boolean;
updated_at: Date;
}
// ============================================================================
// Phase 9 — notify step per-user routing (ROUTE-01..06)
// ============================================================================
/** Resolver name; resolvers live in lib/services/pipeline-steps/notify-resolvers.ts. */
export type ResolverName = 'autotask_resource_email' | 'direct_email' | 'pulse_user_id' | string;
/** Optional block on a `notify` step's config. When present, the step attempts
* delivery via the resolved Pulse user's personal channel before falling back
* to the step's `channel_id`.
*
* v1 limitation: `field` is a single-level key; dotted paths like 'company.id'
* are NOT walked. Admins must surface needed values at top level of the source
* object (e.g., via an upstream transform step). */
export interface RouteToUser {
/** PipelineContext key whose value holds the entity (e.g. 'ticket'). */
source: string;
/** Single-level field name within context[source] (e.g. 'assignedResourceID').
* v1 only: single-level lookup dotted paths like 'company.id' are NOT walked. */
field: string;
/** Resolver name. Looks up the user from the field value. */
resolve: ResolverName;
/** Event key checked against user_event_subscriptions for muting (D-12). */
event_key: string;
/** Optional preferred channel type (D-10). When omitted: ntfy then teams then fallback. */
channel_type?: 'teams' | 'ntfy';
}
/** Output of a resolver. null = no recipient (notify.ts falls through to global). */
export type ResolvedRecipient =
| { user_id: string }
| { email: string }
| null;
/** A resolver fn. Pure async; reads from postgres if it needs to. */
export type NotifyResolver = (fieldValue: unknown) => Promise<ResolvedRecipient>;
/** Reasons recorded in execution_step.output_data when fallback occurs (D-11). */
export type UserRouteFallbackReason =
| 'no_channel'
| 'send_failed'
| 'user_not_found'
| 'no_field_value'
| 'resolver_unknown';
export interface UserRouteFallback {
reason: UserRouteFallbackReason;
user_id?: string;
channel_type: string;
error?: string;
}