Files
whatsapp-inbox-platform/lib/whatsapp-provider.ts
Wira Basalamah adde003fba
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
chore: initial project import
2026-04-21 09:29:29 +07:00

187 lines
4.3 KiB
TypeScript

import { DeliveryStatus } from "@prisma/client";
export type OutboundMessageRequest = {
tenantId: string;
channelId: string;
channelProvider: string;
phoneNumberId: string | null;
to: string;
content: string;
messageId: string;
};
export type OutboundMessageResult = {
success: boolean;
provider: string;
deliveryStatus: DeliveryStatus;
providerMessageId?: string | null;
failureReason?: string;
};
type MetaResponse = {
messages?: Array<{ id?: string }>;
error?: {
message?: string;
};
};
function normalizeProvider(provider: string) {
return provider.trim().toLowerCase();
}
function normalizeToPhone(phone: string) {
return phone.replace(/\D/g, "");
}
function randomMockMessageId() {
if (typeof crypto !== "undefined" && "randomUUID" in crypto && typeof crypto.randomUUID === "function") {
return `mock-${crypto.randomUUID()}`;
}
return `mock-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
}
function isMockAllowed() {
const explicit = process.env.WHATSAPP_ALLOW_SIMULATED_SEND?.trim().toLowerCase();
if (process.env.NODE_ENV === "production") {
return false;
}
if (explicit === undefined) {
return true;
}
return explicit === "true" || explicit === "1" || explicit === "yes";
}
async function sendViaMeta({
to,
content,
phoneNumberId
}: {
to: string;
content: string;
phoneNumberId: string;
}) {
const token = process.env.WHATSAPP_API_TOKEN?.trim();
const version = process.env.WHATSAPP_API_VERSION?.trim() || "v22.0";
if (!token) {
return {
success: false,
failureReason: "WHATSAPP_API_TOKEN is not configured"
};
}
const normalizedTo = normalizeToPhone(to);
const response = await fetch(`https://graph.facebook.com/${version}/${phoneNumberId}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
messaging_product: "whatsapp",
to: normalizedTo,
type: "text",
text: {
preview_url: false,
body: content
}
})
});
const payloadText = await response.text();
let parsed: MetaResponse = {};
try {
parsed = JSON.parse(payloadText) as MetaResponse;
} catch {
// ignore parse error
}
if (!response.ok) {
return {
success: false,
failureReason: parsed.error?.message
? `Meta API error: ${parsed.error.message}`
: `Meta API ${response.status}: ${payloadText.slice(0, 400)}`
};
}
const providerMessageId = parsed.messages?.[0]?.id?.trim();
if (!providerMessageId) {
return {
success: false,
failureReason: "Meta API success response does not include message id"
};
}
return {
success: true,
providerMessageId
};
}
export async function sendOutboundTextMessage(input: OutboundMessageRequest): Promise<OutboundMessageResult> {
const normalized = normalizeProvider(input.channelProvider);
const defaultResult: OutboundMessageResult = {
success: false,
provider: normalized || "unknown",
deliveryStatus: DeliveryStatus.FAILED
};
if (!input.phoneNumberId || !input.to || !input.content) {
return {
...defaultResult,
failureReason: "Missing recipient or message data"
};
}
if (!normalized.includes("meta")) {
if (isMockAllowed()) {
return {
success: true,
provider: "mock",
deliveryStatus: DeliveryStatus.SENT,
providerMessageId: randomMockMessageId()
};
}
return {
...defaultResult,
failureReason: `Unsupported provider "${input.channelProvider}". Configure WhatsApp provider or enable simulation.`
};
}
const metaResult = await sendViaMeta({
to: input.to,
content: input.content,
phoneNumberId: input.phoneNumberId
});
if (!metaResult.success) {
if (!isMockAllowed()) {
return {
...defaultResult,
provider: "meta",
failureReason: metaResult.failureReason
};
}
return {
success: true,
provider: "mock",
deliveryStatus: DeliveryStatus.SENT,
providerMessageId: randomMockMessageId(),
failureReason: metaResult.failureReason
};
}
return {
success: true,
provider: "meta",
deliveryStatus: DeliveryStatus.SENT,
providerMessageId: metaResult.providerMessageId
};
}