feat(db): WhatsApp channel schema (phone links, messages, inbox source) (#1337)
Foundation for WhatsApp receipt intake (plan 2026-08-01), no runtime callers yet: - whatsapp_phone_links + whatsapp_link_codes: verified phone -> user binding via single-use 10-min codes (invite-token pattern). Peppered HMAC lookup hash + AES-GCM encrypted number; one ACTIVE link per phone and per user (partial unique, revocation preserves history). User-scoped RLS (a binding belongs to a person, not a company). - whatsapp_conversations + whatsapp_messages: deterministic state machine state and the message log, which doubles as the durable job record for persist-first webhook processing. Partial unique index on inbound wamid = the at-least-once dedupe key. Service-role only. - whatsapp_sender_rate_counters + check_and_increment_whatsapp_sender_quota: the pre-binding limiter keyed by phone hash; EXECUTE granted to service_role only. - invoice_inbox_items: source CHECK widened to 'whatsapp', plus whatsapp_message_id (one item per delivering message) and channel_context jsonb, kept separate from extracted_data so verified human answers never share a container with untrusted OCR output. document_attachments.upload_source CHECK gains 'whatsapp'. - whatsapp_conversations triaged into ARCHIVE_EXCLUDED_TABLES (full-archive coverage contract). pg-real on a fresh DB: 975/975 incl. the new whatsapp-channel suite (RLS visibility, unique/rebinding semantics, wamid dedupe, CHECK widenings, quota RPC caps + grant lockdown). Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
02e48efe11
commit
bd3592cb99
+105
-1
@@ -2652,7 +2652,7 @@ export interface SIEAccountMapping {
|
||||
// ============================================================
|
||||
|
||||
export type InboxItemStatus = 'received' | 'error'
|
||||
export type InboxItemSource = 'email' | 'upload'
|
||||
export type InboxItemSource = 'email' | 'upload' | 'whatsapp'
|
||||
|
||||
export type CompanyInboxStatus = 'active' | 'deprecated' | 'blocked'
|
||||
|
||||
@@ -2715,6 +2715,13 @@ export interface InvoiceInboxItem {
|
||||
error_message: string | null
|
||||
raw_email_payload: Record<string, unknown> | null
|
||||
|
||||
// WhatsApp channel (migration 20260802092000). whatsapp_message_id links
|
||||
// back to the delivering chat message; channel_context holds verified
|
||||
// human answers from the chat (kept OUT of extracted_data on purpose:
|
||||
// retry-extraction overwrites that container wholesale).
|
||||
whatsapp_message_id?: string | null
|
||||
channel_context?: InboxChannelContext | null
|
||||
|
||||
// Audit chain (processing_history correlation)
|
||||
correlation_id: string | null
|
||||
|
||||
@@ -2727,6 +2734,102 @@ export interface InvoiceInboxItem {
|
||||
supplier_invoice?: SupplierInvoice
|
||||
}
|
||||
|
||||
// Chat-sourced context attached to an inbox item. `raw_answer` + timestamps
|
||||
// double as the Skatteverket representation documentation trail.
|
||||
export interface InboxChannelContext {
|
||||
channel: 'whatsapp'
|
||||
caption?: string | null
|
||||
company_selected_via?: 'button' | 'pin' | 'default' | 'single'
|
||||
representation?: {
|
||||
participants: { name: string; company: string | null }[]
|
||||
purpose: string | null
|
||||
event_date: string | null
|
||||
raw_answer: string
|
||||
answered_at: string
|
||||
}
|
||||
user_note?: string | null
|
||||
quality?: { resend_requested_at: string; resent: boolean }
|
||||
pending_question?: {
|
||||
type: 'representation' | 'context' | 'resend'
|
||||
asked_at: string
|
||||
status: 'open' | 'answered' | 'moved_to_app'
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WhatsApp Channel Types (migrations 20260802090000/091000)
|
||||
// ============================================================
|
||||
|
||||
export interface WhatsAppPhoneLink {
|
||||
id: string
|
||||
user_id: string
|
||||
phone_hash: string
|
||||
phone_enc: string
|
||||
phone_masked: string
|
||||
wa_profile_name: string | null
|
||||
default_company_id: string | null
|
||||
last_company_id: string | null
|
||||
verified_at: string
|
||||
revoked_at: string | null
|
||||
muted_at: string | null
|
||||
last_message_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WhatsAppConversationState =
|
||||
| 'idle'
|
||||
| 'awaiting_company'
|
||||
| 'awaiting_representation'
|
||||
| 'awaiting_context'
|
||||
| 'awaiting_resend'
|
||||
|
||||
export interface WhatsAppConversation {
|
||||
id: string
|
||||
phone_link_id: string
|
||||
state: WhatsAppConversationState
|
||||
context: Record<string, unknown>
|
||||
company_id: string | null
|
||||
service_window_expires_at: string | null
|
||||
debounce_until: string | null
|
||||
pending_ack: boolean
|
||||
last_inbound_at: string | null
|
||||
last_outbound_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type WhatsAppMessageProcessingStatus =
|
||||
| 'received'
|
||||
| 'processing'
|
||||
| 'done'
|
||||
| 'skipped'
|
||||
| 'error'
|
||||
|
||||
export interface WhatsAppMessage {
|
||||
id: string
|
||||
direction: 'inbound' | 'outbound'
|
||||
wamid: string | null
|
||||
sender_phone_hash: string | null
|
||||
phone_link_id: string | null
|
||||
conversation_id: string | null
|
||||
message_type: string
|
||||
body_text: string | null
|
||||
media_id: string | null
|
||||
media_mime: string | null
|
||||
media_sha256: string | null
|
||||
media_filename: string | null
|
||||
raw_payload: Record<string, unknown> | null
|
||||
processing_status: WhatsAppMessageProcessingStatus
|
||||
attempts: number
|
||||
error_message: string | null
|
||||
inbox_item_id: string | null
|
||||
delivery_status: string | null
|
||||
correlation_id: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Receipt Types
|
||||
// ============================================================
|
||||
@@ -3140,6 +3243,7 @@ export type DocumentUploadSource =
|
||||
| 'scan'
|
||||
| 'api'
|
||||
| 'system'
|
||||
| 'whatsapp'
|
||||
|
||||
export interface DocumentAttachment {
|
||||
id: string
|
||||
|
||||
Reference in New Issue
Block a user