import type { AccountRole } from "@/lib/auth/roles"; export interface Profile { id: string; user_id: string; full_name: string; email: string; avatar_url?: string; /** * Legacy free-form role column from migration 001. Never read * by the app since 017_account_sharing.sql introduced the typed * `account_role` enum. Flagged for removal in a later cleanup * migration — kept on the type so existing destructures don't * break. */ role: string; /** * Opted-in beta feature keys for this account. The column survives * for future beta gates; no current feature reads it (Flows was * the last user and went to soft-GA in PR #134). Defaults to `[]` * for every profile; toggled per-account via a direct UPDATE on * the `profiles` row. */ beta_features?: string[]; /** * Account this profile is a member of. Added by * `017_account_sharing.sql`; NOT NULL in the DB post-backfill. * Optional on the type only because older serialised payloads * (cached client state, test fixtures) may not have it yet. */ account_id?: string; /** * Caller's role within their account. Source of truth for every * role-gated UI / API check — call `hasMinRole` from * `@/lib/auth/roles` rather than comparing this string directly. */ account_role?: AccountRole; created_at: string; } // ============================================================ // Account-sharing entities (017_account_sharing.sql) // ============================================================ export interface Account { id: string; name: string; /** auth.users.id of the immutable owner. */ owner_user_id: string; created_at: string; updated_at: string; } /** * Hydrated member row for the Settings → Members tab. Combines * the profile and its account_role for a single member of the * caller's account. Sensitive fields (email) are populated only * when the caller has admin+ — agents and viewers see name + * avatar + role only. */ export interface AccountMember { user_id: string; full_name: string; email: string | null; avatar_url: string | null; role: AccountRole; joined_at: string; } /** * Outstanding invite link row. `token_hash` is intentionally * absent — it lives only in the DB and on the server. The * plaintext token is returned once at creation time and surfaced * via the invite URL; never re-emitted. */ export interface AccountInvitation { id: string; account_id: string; /** Roles offered via invite — owner is never offered. */ role: Exclude; created_by_user_id: string | null; label: string | null; created_at: string; expires_at: string; accepted_at: string | null; accepted_by_user_id: string | null; } export interface Contact { id: string; user_id: string; account_id: string; phone: string; /** Digits-only form of `phone`, generated by the DB (migration 022) * and unique per account. Read-only. */ phone_normalized?: string; name?: string; email?: string; company?: string; avatar_url?: string; created_at: string; updated_at: string; } export interface Tag { id: string; user_id: string; name: string; color: string; created_at: string; } export interface ContactTag { id: string; contact_id: string; tag_id: string; } export interface CustomField { id: string; user_id: string; /** Tenancy key — NOT NULL since migration 017. */ account_id: string; field_name: string; field_type: string; field_options?: Record; created_at: string; } export interface ContactCustomValue { id: string; contact_id: string; custom_field_id: string; value?: string; } export interface ContactNote { id: string; contact_id: string; user_id: string; note_text: string; created_at: string; } export type ConversationStatus = 'open' | 'pending' | 'closed'; export interface Conversation { id: string; user_id: string; contact_id: string; status: ConversationStatus; assigned_agent_id?: string; last_message_text?: string; last_message_at?: string; unread_count: number; created_at: string; updated_at: string; contact?: Contact; } // ============================================================ // Notifications (migration 023) // ============================================================ export type NotificationType = 'conversation_assigned'; export interface Notification { id: string; account_id: string; /** Recipient — the agent this notification is for. */ user_id: string; type: NotificationType; conversation_id?: string; contact_id?: string; /** Who triggered it. Null when an automation/system assigned it. */ actor_user_id?: string; title: string; body?: string; read_at?: string; created_at: string; } export type SenderType = 'customer' | 'agent' | 'bot'; export type ContentType = | 'text' | 'image' | 'document' | 'audio' | 'video' | 'location' | 'template' /** Customer tapped a reply button or list row on a message we sent. */ | 'interactive'; export type MessageStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed'; export interface Message { id: string; conversation_id: string; sender_type: SenderType; sender_id?: string; content_type: ContentType; content_text?: string; media_url?: string; template_name?: string; message_id?: string; status: MessageStatus; created_at: string; reply_to_message_id?: string; /** * Only set when `content_type === 'interactive'` — the stable id of * the button or list row the customer tapped. The Flows engine uses * this to route the next node; the inbox bubble uses it as a styling * cue (renders with a "↩ button reply" affordance). */ interactive_reply_id?: string; } export type ReactionActor = 'customer' | 'agent'; export interface MessageReaction { id: string; message_id: string; conversation_id: string; actor_type: ReactionActor; actor_id?: string; emoji: string; created_at: string; } export interface WhatsAppConfig { id: string; user_id: string; phone_number_id: string; waba_id?: string; access_token: string; verify_token?: string; status: 'connected' | 'disconnected'; connected_at?: string; /** * Set when POST /{phone_number_id}/register last succeeded. NULL * means the number was saved but never actually subscribed for * webhooks on Meta's side — inbound events will be silently lost. */ registered_at?: string; /** Set when POST /{waba_id}/subscribed_apps last succeeded. */ subscribed_apps_at?: string; /** Last error from /register; cleared on success. */ last_registration_error?: string; } // Raw Meta status enum. We persist this verbatim from Meta (sync + webhook) // rather than collapsing to a local TitleCase set — distinctions like // PAUSED vs DISABLED vs IN_APPEAL drive the edit/resubmit/delete flows. // DRAFT is the local-only state before the row is submitted to Meta. export type MessageTemplateStatus = | 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'PAUSED' | 'DISABLED' | 'IN_APPEAL' | 'PENDING_DELETION'; export type TemplateButton = | { type: 'QUICK_REPLY'; text: string } | { type: 'URL'; text: string; url: string; example?: string } | { type: 'PHONE_NUMBER'; text: string; phone_number: string } | { type: 'COPY_CODE'; text: string; example: string }; export interface TemplateSampleValues { body?: string[]; header?: string[]; } export interface MessageTemplate { id: string; user_id: string; name: string; category: 'Marketing' | 'Utility' | 'Authentication'; language?: string; header_type?: 'text' | 'image' | 'video' | 'document'; header_content?: string; header_handle?: string; header_media_url?: string; body_text: string; footer_text?: string; buttons?: TemplateButton[]; sample_values?: TemplateSampleValues; status?: MessageTemplateStatus; meta_template_id?: string; rejection_reason?: string; quality_score?: 'GREEN' | 'YELLOW' | 'RED'; submission_error?: string; last_submitted_at?: string; created_at: string; } export interface Pipeline { id: string; user_id: string; name: string; created_at: string; } export interface PipelineStage { id: string; pipeline_id: string; name: string; position: number; color: string; created_at: string; } export type DealStatus = 'open' | 'won' | 'lost'; export interface Deal { id: string; user_id: string; pipeline_id: string; stage_id: string; /** * Nullable after migration 004 — becomes NULL when the referenced * contact is deleted (ON DELETE SET NULL). History preserved. */ contact_id: string | null; conversation_id?: string; assigned_to?: string; title: string; value: number; currency?: string; notes?: string; expected_close_date?: string; status?: DealStatus; created_at: string; updated_at?: string; contact?: Contact; stage?: PipelineStage; assignee?: Profile; } export type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'failed'; export type RecipientStatus = 'pending' | 'sent' | 'delivered' | 'read' | 'replied' | 'failed'; export interface Broadcast { id: string; user_id: string; name: string; template_name: string; template_language: string; template_variables?: Record; audience_filter?: Record; scheduled_at?: string; status: BroadcastStatus; total_recipients: number; sent_count: number; delivered_count: number; read_count: number; replied_count: number; failed_count: number; created_at: string; } export interface BroadcastRecipient { id: string; broadcast_id: string; /** * Nullable after migration 004 — becomes NULL when the referenced * contact is deleted (ON DELETE SET NULL). History preserved; the * UI renders "Unknown" for orphaned rows. */ contact_id: string | null; status: RecipientStatus; sent_at?: string; delivered_at?: string; read_at?: string; replied_at?: string; error_message?: string; /** * Meta's message id, persisted when the broadcast send succeeds so * the webhook can mirror status updates back onto the recipient row. * Added in migration 003. */ whatsapp_message_id?: string; created_at: string; contact?: Contact; } // ============================================================ // Automations (migration 006) // ============================================================ export type AutomationTriggerType = | 'new_message_received' | 'first_inbound_message' | 'keyword_match' | 'new_contact_created' | 'conversation_assigned' | 'tag_added' | 'time_based'; export type AutomationStepType = | 'send_message' | 'send_template' | 'add_tag' | 'remove_tag' | 'assign_conversation' | 'update_contact_field' | 'create_deal' | 'wait' | 'condition' | 'send_webhook' | 'close_conversation'; export type AutomationLogStatus = 'success' | 'partial' | 'failed'; export interface KeywordMatchTriggerConfig { keywords: string[]; match_type: 'exact' | 'contains'; case_sensitive?: boolean; } export interface TagTriggerConfig { tag_id: string; } export interface TimeBasedTriggerConfig { /** Cron expression or simple HH:mm string; engine can accept either. */ schedule: string; timezone?: string; } export type AutomationTriggerConfig = | Record | KeywordMatchTriggerConfig | TagTriggerConfig | TimeBasedTriggerConfig | Record; export interface SendMessageStepConfig { text: string; } export interface SendTemplateStepConfig { template_name: string; language?: string; variables?: Record; } export interface TagStepConfig { tag_id: string; } export interface AssignConversationStepConfig { mode: 'specific' | 'round_robin'; agent_id?: string; } export interface UpdateContactFieldStepConfig { /** * Either a built-in contact column (`name` | `email` | `company`) or a * custom field encoded as `custom:`. The `custom:` prefix * is how the engine distinguishes a `contact_custom_values` write from a * direct `contacts` column update. Older configs store the bare column name, * so this stays backward compatible. */ field: string; /** Supports `{{ vars.* }}` / `{{ message.text }}` interpolation at runtime. */ value: string; } export interface CreateDealStepConfig { pipeline_id: string; stage_id: string; title: string; value?: number; } export interface WaitStepConfig { amount: number; unit: 'minutes' | 'hours' | 'days'; } export type ConditionSubject = | 'contact_field' | 'tag_presence' | 'message_content' | 'time_of_day'; export interface ConditionStepConfig { subject: ConditionSubject; /** e.g. field name, tag id, substring, or "HH:mm-HH:mm" depending on subject */ operand?: string; /** For contact_field equals / message_content contains — comparison value */ value?: string; } export interface SendWebhookStepConfig { url: string; headers?: Record; body_template?: string; } export type AutomationStepConfig = | SendMessageStepConfig | SendTemplateStepConfig | TagStepConfig | AssignConversationStepConfig | UpdateContactFieldStepConfig | CreateDealStepConfig | WaitStepConfig | ConditionStepConfig | SendWebhookStepConfig | Record | Record; export interface Automation { id: string; /** Account tenancy key — every automation belongs to one account * (migration 017 made the column NOT NULL). The engine looks up * active automations by this field on inbound webhook events. */ account_id: string; /** Original author. Used for log audit + outbound message * sender-of-record, never for tenancy isolation. */ user_id: string; name: string; description?: string; trigger_type: AutomationTriggerType; trigger_config: AutomationTriggerConfig; is_active: boolean; execution_count: number; last_executed_at?: string | null; created_at: string; updated_at: string; } export interface AutomationStep { id: string; automation_id: string; parent_step_id?: string | null; branch?: 'yes' | 'no' | null; step_type: AutomationStepType; step_config: AutomationStepConfig; position: number; created_at: string; } export interface AutomationLogStepResult { step_id: string; step_type: AutomationStepType; status: 'success' | 'skipped' | 'failed'; detail?: string; } export interface AutomationLog { id: string; automation_id: string; user_id: string; contact_id: string | null; trigger_event: string; steps_executed: AutomationLogStepResult[]; status: AutomationLogStatus; error_message?: string | null; created_at: string; contact?: Contact; }