أضف إشعارات SMS إلى موقع الحجوزات بالذكاء الاصطناعي

أرسل رسالة SMS تلقائية عندما يرسل شخص ما طلب حجز.

خارطة الطريق والموارد

جديد هنا؟ ابدأ من هنا أولًا

يطوّر هذا الدليل مشروع موقع الحجوزات بالذكاء الاصطناعي بإضافة إشعارات SMS تلقائية. إذا لم تكن قد أنشأت موقع الحجوزات بعد، فابدأ بالدليل الرئيسي أولًا.

ابنِ موقع حجوزات بالذكاء الاصطناعي

افتح الدليل الرئيسي

MAIN GUIDE

https://profitstudio.app/video/build-booking-website-ai

احصل على بيانات Twilio الخاصة بك

أنشئ حساب Twilio

افتح Twilio

https://www.twilio.com/

احصل على هذه القيم من لوحة تحكم Twilio:

قيم Twilio

TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN TWILIO_PHONE_NUMBER

TWILIO_ACCOUNT_SID

Account SID

TWILIO_AUTH_TOKEN

Auth Token

TWILIO_PHONE_NUMBER

رقم هاتف Twilio

اختر رقم Twilio يدعم SMS

أضف الأسرار في Supabase

افتح أسرار Edge Functions في Supabase

افتح Supabase

https://supabase.com/dashboard/projects

Supabase → Project Settings → Edge Functions → Secrets

أضف أسرار Twilio المطلوبة

الأسرار المطلوبة

TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890

رسالة SMS للعميل مُفعّلة افتراضيًا. رسالة SMS للمالك اختيارية ومُعطّلة افتراضيًا.

اختياري: أرسل SMS إلى صاحب العمل

SMS اختياري للمالك

SEND_OWNER_SMS=true OWNER_PHONE_NUMBER=+1234567890

أضِفها فقط إذا كنت تريد أيضًا أن يستلم صاحب العمل رسالة SMS عند إرسال طلب حجز جديد.

أنشئ Edge Function الخاصة بـ SMS

أنشئ Edge Function جديدة بهذا الاسم تمامًا: send-sms-notification

راجع قالب رسالة SMS

انسخ والصق كود وظيفة SMS المُنشأ

وظيفة إشعار SMS

// supabase/functions/send-sms-notification/index.ts // // Sends SMS notifications when a new booking request is created. // Default: sends booking request SMS to the customer. // Optional: also sends SMS notification to the business owner. // // Required Supabase Secrets: // TWILIO_ACCOUNT_SID // TWILIO_AUTH_TOKEN // TWILIO_PHONE_NUMBER // // Optional Owner SMS Secrets: // SEND_OWNER_SMS=true // OWNER_PHONE_NUMBER=+1234567890 const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; // ===== CUSTOMIZE THESE VALUES ===== const BUSINESS_NAME = "Your Business Name"; const CUSTOMER_SMS_TEMPLATE = "Hi {{customer_name}}, we received your {{service_name}} booking request for {{appointment_date}} at {{appointment_time}}. We'll contact you soon to confirm."; const OWNER_SMS_TEMPLATE = "New booking request: {{customer_name}} requested {{service_name}} on {{appointment_date}} at {{appointment_time}}. Phone: {{customer_phone}}"; // Default booking field names used in this guide. // If your project uses different field names, update them here. const FIELD_MAP = { customerName: ["full_name", "customer_name", "name"], customerPhone: ["phone", "phone_number", "customer_phone"], appointmentDate: ["appointment_date", "date", "booking_date"], appointmentTime: ["start_time", "appointment_time", "time"], serviceName: ["service_name", "service"], serviceId: ["service_id"], }; // ===== END CUSTOMIZATION ===== function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { ...CORS_HEADERS, "Content-Type": "application/json" }, }); } function getEnvFlag(name: string, defaultValue = false): boolean { const value = Deno.env.get(name); if (!value) return defaultValue; return value.toLowerCase() === "true"; } function getField(record: Record<string, unknown>, keys: string[]): string { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) { return value.trim(); } } return ""; } function formatTime(time: string): string { if (!time) return ""; return time.length >= 5 ? time.slice(0, 5) : time; } function renderTemplate( template: string, values: Record<string, string>, ): string { return template.replace(/\{\{(.*?)\}\}/g, (_, key) => { return values[key.trim()] ?? ""; }); } async function fetchServiceName( serviceId: string, supabaseUrl: string, serviceRoleKey: string, ): Promise<string> { try { const url = `${supabaseUrl}/rest/v1/services?id=eq.${encodeURIComponent( serviceId, )}&select=name`; const response = await fetch(url, { headers: { apikey: serviceRoleKey, Authorization: `Bearer ${serviceRoleKey}`, }, }); if (!response.ok) return ""; const rows = await response.json(); return rows?.[0]?.name ?? ""; } catch { return ""; } } async function sendTwilioSms(options: { accountSid: string; authToken: string; from: string; to: string; body: string; }) { const { accountSid, authToken, from, to, body } = options; const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const form = new URLSearchParams({ From: from, To: to, Body: body, }); const response = await fetch(url, { method: "POST", headers: { Authorization: "Basic " + btoa(`${accountSid}:${authToken}`), "Content-Type": "application/x-www-form-urlencoded", }, body: form.toString(), }); const data = await response.json().catch(() => null); return { ok: response.ok, status: response.status, data, }; } Deno.serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: CORS_HEADERS }); } if (req.method !== "POST") { return json({ success: false, error: "Method not allowed" }, 405); } const accountSid = Deno.env.get("TWILIO_ACCOUNT_SID"); const authToken = Deno.env.get("TWILIO_AUTH_TOKEN"); const twilioPhoneNumber = Deno.env.get("TWILIO_PHONE_NUMBER"); const sendCustomerSms = getEnvFlag("SEND_CUSTOMER_SMS", true); const sendOwnerSms = getEnvFlag("SEND_OWNER_SMS", false); const ownerPhoneNumber = Deno.env.get("OWNER_PHONE_NUMBER"); if (!accountSid || !authToken || !twilioPhoneNumber) { return json( { success: false, error: "Missing Twilio secrets. Please add TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER.", }, 500, ); } let payload; try { payload = await req.json(); } catch { return json({ success: false, error: "Invalid JSON body" }, 400); } const record = payload?.record; if (!record) { return json({ success: false, error: "Missing record in payload" }, 400); } const customerName = getField(record, FIELD_MAP.customerName) || "Customer"; const customerPhone = getField(record, FIELD_MAP.customerPhone); const appointmentDate = getField(record, FIELD_MAP.appointmentDate) || "your selected date"; const appointmentTime = formatTime(getField(record, FIELD_MAP.appointmentTime)) || "your selected time"; let serviceName = getField(record, FIELD_MAP.serviceName) || "your service"; const serviceId = getField(record, FIELD_MAP.serviceId); const supabaseUrl = Deno.env.get("SUPABASE_URL"); const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (serviceId && supabaseUrl && serviceRoleKey) { const fetchedServiceName = await fetchServiceName( serviceId, supabaseUrl, serviceRoleKey, ); if (fetchedServiceName) { serviceName = fetchedServiceName; } } const templateValues = { business_name: BUSINESS_NAME, customer_name: customerName, customer_phone: customerPhone || "Not provided", service_name: serviceName, appointment_date: appointmentDate, appointment_time: appointmentTime, }; const results = { customer_sms: "not_sent", owner_sms: "not_sent", errors: [] as unknown[], }; if (sendCustomerSms) { if (!customerPhone) { results.customer_sms = "skipped_missing_customer_phone"; } else { const customerMessage = renderTemplate( CUSTOMER_SMS_TEMPLATE, templateValues, ); const customerResult = await sendTwilioSms({ accountSid, authToken, from: twilioPhoneNumber, to: customerPhone, body: customerMessage, }); if (!customerResult.ok) { results.customer_sms = "failed"; results.errors.push({ type: "customer_sms_failed", status: customerResult.status, twilio: customerResult.data, }); } else { results.customer_sms = "sent"; } } } if (sendOwnerSms) { if (!ownerPhoneNumber) { results.owner_sms = "skipped_missing_owner_phone"; } else { const ownerMessage = renderTemplate( OWNER_SMS_TEMPLATE, templateValues, ); const ownerResult = await sendTwilioSms({ accountSid, authToken, from: twilioPhoneNumber, to: ownerPhoneNumber, body: ownerMessage, }); if (!ownerResult.ok) { results.owner_sms = "failed"; results.errors.push({ type: "owner_sms_failed", status: ownerResult.status, twilio: ownerResult.data, }); } else { results.owner_sms = "sent"; } } } if (results.errors.length > 0) { return json( { success: false, message: "One or more SMS messages failed.", results, }, 502, ); } return json({ success: true, message: "SMS notification process completed.", results, }); });

اسم العمل

اسم عملك

رسالة SMS للعميل

مرحبًا {{customer_name}}، لقد استلمنا طلب حجزك لـ {{service_name}} في {{appointment_date}} الساعة {{appointment_time}}. سنتواصل معك قريبًا للتأكيد.

رسالة SMS للمالك

طلب حجز جديد: طلب {{customer_name}} {{service_name}} في {{appointment_date}} الساعة {{appointment_time}}. الهاتف: {{customer_phone}}

اختياري: حدّث أسماء الحقول عند الحاجة

تعمل وظيفة SMS هذه مع حقول الحجز الشائعة مثل: full_name و phone و appointment_date و start_time و service_id و service_name. إذا كان مشروعك يستخدم أسماء حقول مختلفة، فحدّث FIELD_MAP داخل الوظيفة لتطابق جدول الحجز لديك.

FIELD_MAP

const FIELD_MAP = { customerName: ["full_name", "customer_name", "name"], customerPhone: ["phone", "phone_number", "customer_phone"], appointmentDate: ["appointment_date", "date", "booking_date"], appointmentTime: ["start_time", "appointment_time", "time"], serviceName: ["service_name", "service"], serviceId: ["service_id"], };

وصّل الـ Webhook

اختر جدول الحجوزات الخاص بك

في هذا الفيديو نستخدم: public.appointments إذا كان مشروعك يستخدم جدول حجوزات مختلفًا، فاختر ذلك الجدول بدلًا من ذلك.

اذهب إلى Webhooks قاعدة بيانات Supabase

Supabase → Database → Webhooks → Create webhook

استخدم إعدادات الـ Webhook التالية

إعدادات الـ Webhook

Table: public.appointments Event: Insert Type: Supabase Edge Functions Function: send-sms-notification Method: POST

فعّل حدث Insert فقط

اختبر إشعار SMS

أنشئ حجزًا جديدًا من موقعك

تحقق ممّا إذا كان العميل قد استلم رسالة SMS

تحقق من سجلات Edge Function في Supabase

Supabase → Edge Functions → send-sms-notification → Logs

تحقق من سجلات Messaging في Twilio عند الحاجة

Twilio → Messaging → Logs

قبل الإطلاق، تحقّق من قواعد SMS في بلدك

تختلف متطلبات SMS حسب البلد

قد تختلف متطلبات SMS حسب بلد الوجهة. قبل استخدام هذا في الإنتاج، تحقّق من متطلبات المراسلة الخاصة بـ Twilio لبلدك المستهدف. على سبيل المثال، قد تتطلّب رسائل SMS المُرسَلة من التطبيقات إلى أرقام الولايات المتحدة تسجيل A2P 10DLC.