wacrm/src/types/index.ts
2026-06-22 13:12:28 +05:30

567 lines
14 KiB
TypeScript

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<AccountRole, "owner">;
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<string, unknown>;
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<string, unknown>;
audience_filter?: Record<string, unknown>;
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<string, never>
| KeywordMatchTriggerConfig
| TagTriggerConfig
| TimeBasedTriggerConfig
| Record<string, unknown>;
export interface SendMessageStepConfig {
text: string;
}
export interface SendTemplateStepConfig {
template_name: string;
language?: string;
variables?: Record<string, string>;
}
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:<custom_field_id>`. 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<string, string>;
body_template?: string;
}
export type AutomationStepConfig =
| SendMessageStepConfig
| SendTemplateStepConfig
| TagStepConfig
| AssignConversationStepConfig
| UpdateContactFieldStepConfig
| CreateDealStepConfig
| WaitStepConfig
| ConditionStepConfig
| SendWebhookStepConfig
| Record<string, never>
| Record<string, unknown>;
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;
}