mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Complete the public API roadmap on top of the existing key-auth layer (#290). Re-lands POST /api/v1/messages (lost when the stacked #291 never reached main) by extracting the dashboard send route's core into `sendMessageToConversation`, then adds: - Contacts: GET/POST /api/v1/contacts, GET/PATCH /api/v1/contacts/{id} - Conversations: GET list + /{id} + /{id}/messages - Broadcasts: POST /api/v1/broadcasts + GET /{id} (persist rows, fan out in after(), poll for status) All list endpoints share one keyset-pagination contract ({ data, meta: { next_cursor } }). No migration required — scopes and tables already exist. Versioned as 0.3.0 (folded into [Unreleased]); docs/public-api.md updated; outbound webhooks left as the sole roadmap item. API stays v1 (additive). Includes fixes from an adversarial code review: cursor validation against filter injection, broadcast counts deferred to the DB aggregate trigger, terminal broadcast status reflects failures, duplicate-phone de-dup, up-front send-param validation (no orphan conversations), PATCH null-clear semantics, error-checked tag sync, bounded tag filter, unified audit-user attribution, and a maxDuration on the broadcast fan-out. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
3.0 KiB
TypeScript
101 lines
3.0 KiB
TypeScript
// ============================================================
|
|
// Public API (v1) serializers for conversations + messages.
|
|
//
|
|
// The dashboard's `Conversation`/`Message` rows carry internal columns
|
|
// (account_id, user_id, sender_id) that shouldn't leak onto the public
|
|
// wire. These serializers project the stable public subset and rename
|
|
// the Meta id (`message_id` → `whatsapp_message_id`) to match the send
|
|
// endpoint's response vocabulary.
|
|
// ============================================================
|
|
|
|
import type { Conversation, Message } from '@/types';
|
|
|
|
export interface ApiConversation {
|
|
id: string;
|
|
contact_id: string;
|
|
status: string;
|
|
assigned_agent_id: string | null;
|
|
last_message_text: string | null;
|
|
last_message_at: string | null;
|
|
unread_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
contact: {
|
|
id: string;
|
|
phone: string;
|
|
name: string | null;
|
|
email: string | null;
|
|
company: string | null;
|
|
tags: { id: string; name: string; color: string }[];
|
|
} | null;
|
|
}
|
|
|
|
export interface ApiMessage {
|
|
id: string;
|
|
conversation_id: string;
|
|
direction: 'inbound' | 'outbound';
|
|
sender_type: string;
|
|
content_type: string;
|
|
content_text: string | null;
|
|
media_url: string | null;
|
|
template_name: string | null;
|
|
whatsapp_message_id: string | null;
|
|
status: string;
|
|
reply_to_message_id: string | null;
|
|
interactive_reply_id: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
/**
|
|
* Project a normalized `Conversation` (from `normalizeConversation`,
|
|
* which has already flattened `contact.tags`) into the public shape.
|
|
*/
|
|
export function serializeConversation(conv: Conversation): ApiConversation {
|
|
const c = conv.contact;
|
|
return {
|
|
id: conv.id,
|
|
contact_id: conv.contact_id,
|
|
status: conv.status,
|
|
assigned_agent_id: conv.assigned_agent_id ?? null,
|
|
last_message_text: conv.last_message_text ?? null,
|
|
last_message_at: conv.last_message_at ?? null,
|
|
unread_count: conv.unread_count ?? 0,
|
|
created_at: conv.created_at,
|
|
updated_at: conv.updated_at,
|
|
contact: c
|
|
? {
|
|
id: c.id,
|
|
phone: c.phone,
|
|
name: c.name ?? null,
|
|
email: c.email ?? null,
|
|
company: c.company ?? null,
|
|
tags: (c.tags ?? []).map((t) => ({
|
|
id: t.id,
|
|
name: t.name,
|
|
color: t.color,
|
|
})),
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
/** Project a `messages` row into the public shape. */
|
|
export function serializeMessage(m: Message): ApiMessage {
|
|
return {
|
|
id: m.id,
|
|
conversation_id: m.conversation_id,
|
|
// `customer` = inbound (from the contact); anything else is outbound.
|
|
direction: m.sender_type === 'customer' ? 'inbound' : 'outbound',
|
|
sender_type: m.sender_type,
|
|
content_type: m.content_type,
|
|
content_text: m.content_text ?? null,
|
|
media_url: m.media_url ?? null,
|
|
template_name: m.template_name ?? null,
|
|
whatsapp_message_id: m.message_id ?? null,
|
|
status: m.status,
|
|
reply_to_message_id: m.reply_to_message_id ?? null,
|
|
interactive_reply_id: m.interactive_reply_id ?? null,
|
|
created_at: m.created_at,
|
|
};
|
|
}
|