Ver Fonte

feat: add work hours to VAPI prompt and fix caller phone storage

- Add prompt-utils.ts for formatting business/special hours in Hungarian
- Include work hours in VAPI system prompt when saving AI config
- Add transfer phone number feature for human colleague during open hours
- Fix vapi-webhook to extract caller phone from payload.customer.number
- Redesign special hours UI with two-row layout to fix overflow
- Add special hours note/comment to VAPI prompt (e.g., "Karácsony")
- Add translations for transfer phone feature (HU, EN, DE)
- Add cleanup-expired-special-hours edge function with cron job

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh há 4 meses atrás
pai
commit
ac3478edcb

+ 275 - 111
shopcall.ai-main/src/components/AIConfigContent.tsx

@@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
 import { Switch } from "@/components/ui/switch";
 import { Switch } from "@/components/ui/switch";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Mic, MessageSquare, Store, Loader2, Bot, Play, Pause, User, Users, Clock, Calendar, Trash2, Plus, X } from "lucide-react";
+import { Mic, MessageSquare, Store, Loader2, Bot, Play, Pause, User, Users, Clock, Calendar, Trash2, Plus, X, Phone } from "lucide-react";
 import { useState, useEffect, useRef } from "react";
 import { useState, useEffect, useRef } from "react";
 import { API_URL } from "@/lib/config";
 import { API_URL } from "@/lib/config";
 import { useToast } from "@/hooks/use-toast";
 import { useToast } from "@/hooks/use-toast";
@@ -30,6 +30,7 @@ interface AIVoice {
 interface AIConfig {
 interface AIConfig {
   voice_type: string;
   voice_type: string;
   greeting_message: string;
   greeting_message: string;
+  transfer_phone_number: string;
 }
 }
 
 
 interface DayHours {
 interface DayHours {
@@ -114,8 +115,11 @@ export function AIConfigContent() {
 
 
   const [aiConfig, setAiConfig] = useState<AIConfig>({
   const [aiConfig, setAiConfig] = useState<AIConfig>({
     voice_type: "",
     voice_type: "",
-    greeting_message: ""
+    greeting_message: "",
+    transfer_phone_number: ""
   });
   });
+  const [transferPhoneError, setTransferPhoneError] = useState<string | null>(null);
+  const [validatingPhone, setValidatingPhone] = useState(false);
 
 
   // Business hours state
   // Business hours state
   const [businessHours, setBusinessHours] = useState<BusinessHours>({
   const [businessHours, setBusinessHours] = useState<BusinessHours>({
@@ -219,8 +223,10 @@ export function AIConfigContent() {
     const config = store.alt_data?.ai_config || {};
     const config = store.alt_data?.ai_config || {};
     setAiConfig({
     setAiConfig({
       voice_type: config.voice_type || "",
       voice_type: config.voice_type || "",
-      greeting_message: config.greeting_message || `Üdvözlöm! A ${store.store_name || 'webáruház'} ügyfélszolgálata vagyok. Miben segíthetek?`
+      greeting_message: config.greeting_message || `Üdvözlöm! A ${store.store_name || 'webáruház'} ügyfélszolgálata vagyok. Miben segíthetek?`,
+      transfer_phone_number: config.transfer_phone_number || ""
     });
     });
+    setTransferPhoneError(null);
   };
   };
 
 
   const loadBusinessHours = async (storeId: string) => {
   const loadBusinessHours = async (storeId: string) => {
@@ -288,6 +294,102 @@ export function AIConfigContent() {
     }
     }
   };
   };
 
 
+  // Validate transfer phone number format (international format: +country_code + digits)
+  const isValidInternationalPhone = (phone: string): boolean => {
+    if (!phone || phone.trim() === '') return true; // Empty is valid (optional field)
+    const internationalPhoneRegex = /^\+[1-9]\d{6,14}$/;
+    return internationalPhoneRegex.test(phone.replace(/\s/g, ''));
+  };
+
+  // Validate transfer phone number (format + not in our phone_numbers table)
+  const validateTransferPhone = async (phone: string): Promise<boolean> => {
+    // Empty is valid
+    if (!phone || phone.trim() === '') {
+      setTransferPhoneError(null);
+      return true;
+    }
+
+    const cleanPhone = phone.replace(/\s/g, '');
+
+    // Check format
+    if (!isValidInternationalPhone(cleanPhone)) {
+      setTransferPhoneError(t('aiConfig.businessHours.transferPhoneInvalidFormat', 'Invalid format. Use international format (e.g., +36301234567)'));
+      return false;
+    }
+
+    // Check if phone is in our phone_numbers table (not allowed)
+    setValidatingPhone(true);
+    try {
+      const { data, error } = await supabase
+        .from('phone_numbers')
+        .select('id')
+        .eq('phone_number', cleanPhone)
+        .limit(1);
+
+      if (error) {
+        console.error('Error checking phone number:', error);
+        setTransferPhoneError(null);
+        return true; // Allow if we can't check
+      }
+
+      if (data && data.length > 0) {
+        setTransferPhoneError(t('aiConfig.businessHours.transferPhoneNotAllowed', 'This phone number cannot be used as a transfer number'));
+        return false;
+      }
+
+      setTransferPhoneError(null);
+      return true;
+    } catch (error) {
+      console.error('Error validating phone:', error);
+      setTransferPhoneError(null);
+      return true;
+    } finally {
+      setValidatingPhone(false);
+    }
+  };
+
+  const handleTransferPhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const value = e.target.value;
+    setAiConfig({ ...aiConfig, transfer_phone_number: value });
+    // Clear error on change, will validate on blur or save
+    if (transferPhoneError) {
+      setTransferPhoneError(null);
+    }
+  };
+
+  const handleTransferPhoneBlur = async () => {
+    await validateTransferPhone(aiConfig.transfer_phone_number);
+  };
+
+  // Trigger VAPI prompt update (after special hours changes)
+  const triggerVapiPromptUpdate = async () => {
+    if (!selectedShop) return;
+
+    try {
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) return;
+
+      const session = JSON.parse(sessionData);
+
+      // Call ai-config with current config and update_system_prompt flag
+      // This rebuilds the VAPI prompt with the latest special hours
+      await fetch(`${API_URL}/api/stores/${selectedShop.id}/ai-config`, {
+        method: 'PUT',
+        headers: {
+          'Authorization': `Bearer ${session.session.access_token}`,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          ai_config: aiConfig,
+          update_system_prompt: true
+        })
+      });
+      console.log('[AIConfig] VAPI prompt updated after special hours change');
+    } catch (error) {
+      console.error('[AIConfig] Failed to update VAPI prompt:', error);
+    }
+  };
+
   // Add special hours
   // Add special hours
   const handleAddSpecialHours = async () => {
   const handleAddSpecialHours = async () => {
     if (!newSpecialDate || !selectedShop) return;
     if (!newSpecialDate || !selectedShop) return;
@@ -331,6 +433,9 @@ export function AIConfigContent() {
       setNewSpecialOpeningTime("09:00");
       setNewSpecialOpeningTime("09:00");
       setNewSpecialClosingTime("18:00");
       setNewSpecialClosingTime("18:00");
 
 
+      // Trigger VAPI prompt update to include new special hours
+      triggerVapiPromptUpdate();
+
       toast({
       toast({
         title: t('common.success'),
         title: t('common.success'),
         description: t('aiConfig.businessHours.specialAdded', 'Special hours added'),
         description: t('aiConfig.businessHours.specialAdded', 'Special hours added'),
@@ -357,6 +462,9 @@ export function AIConfigContent() {
 
 
       setSpecialHours(specialHours.filter(sh => sh.id !== id));
       setSpecialHours(specialHours.filter(sh => sh.id !== id));
 
 
+      // Trigger VAPI prompt update to remove deleted special hours
+      triggerVapiPromptUpdate();
+
       toast({
       toast({
         title: t('common.success'),
         title: t('common.success'),
         description: t('aiConfig.businessHours.specialDeleted', 'Special hours deleted'),
         description: t('aiConfig.businessHours.specialDeleted', 'Special hours deleted'),
@@ -384,6 +492,17 @@ export function AIConfigContent() {
       return;
       return;
     }
     }
 
 
+    // Validate transfer phone number before saving
+    const isTransferPhoneValid = await validateTransferPhone(aiConfig.transfer_phone_number);
+    if (!isTransferPhoneValid) {
+      toast({
+        title: t('common.error'),
+        description: transferPhoneError || t('aiConfig.businessHours.transferPhoneInvalid', 'Invalid transfer phone number'),
+        variant: "destructive"
+      });
+      return;
+    }
+
     setSaving(true);
     setSaving(true);
     try {
     try {
       const sessionData = localStorage.getItem('session_data');
       const sessionData = localStorage.getItem('session_data');
@@ -393,21 +512,8 @@ export function AIConfigContent() {
 
 
       const session = JSON.parse(sessionData);
       const session = JSON.parse(sessionData);
 
 
-      // Save AI config
-      const response = await fetch(`${API_URL}/api/stores/${selectedShop.id}/ai-config`, {
-        method: 'PUT',
-        headers: {
-          'Authorization': `Bearer ${session.session.access_token}`,
-          'Content-Type': 'application/json'
-        },
-        body: JSON.stringify({ ai_config: aiConfig })
-      });
-
-      if (!response.ok) {
-        throw new Error('Failed to save configuration');
-      }
-
-      // Save business hours (upsert)
+      // IMPORTANT: Save business hours FIRST (before AI config API call)
+      // This ensures the VAPI system prompt includes the latest work hours
       const { error: hoursError } = await supabase
       const { error: hoursError } = await supabase
         .from('store_business_hours')
         .from('store_business_hours')
         .upsert({
         .upsert({
@@ -423,6 +529,24 @@ export function AIConfigContent() {
         throw hoursError;
         throw hoursError;
       }
       }
 
 
+      // Save AI config and trigger VAPI system prompt update with work hours
+      // The update_system_prompt flag tells the API to rebuild the VAPI prompt including work hours
+      const response = await fetch(`${API_URL}/api/stores/${selectedShop.id}/ai-config`, {
+        method: 'PUT',
+        headers: {
+          'Authorization': `Bearer ${session.session.access_token}`,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          ai_config: aiConfig,
+          update_system_prompt: true  // This triggers VAPI prompt rebuild with work hours
+        })
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to save configuration');
+      }
+
       toast({
       toast({
         title: t('common.success'),
         title: t('common.success'),
         description: t('aiConfig.saveConfiguration'),
         description: t('aiConfig.saveConfiguration'),
@@ -791,103 +915,110 @@ export function AIConfigContent() {
                   <div className="space-y-4 pt-4 border-t border-slate-700">
                   <div className="space-y-4 pt-4 border-t border-slate-700">
                     <Label className="text-slate-300">{t('aiConfig.businessHours.specialHours', 'Special Hours / Holidays')}</Label>
                     <Label className="text-slate-300">{t('aiConfig.businessHours.specialHours', 'Special Hours / Holidays')}</Label>
 
 
-                    {/* Add Special Hours Form */}
-                    <div className="flex items-center gap-2 flex-wrap bg-slate-700/50 p-3 rounded-lg">
-                      {/* Date Picker */}
-                      <Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
-                        <PopoverTrigger asChild>
-                          <Button
-                            variant="outline"
-                            className={cn(
-                              "w-[140px] justify-start text-left font-normal bg-slate-700 border-slate-600 text-white hover:bg-slate-600",
-                              !newSpecialDate && "text-slate-400"
-                            )}
-                          >
-                            <Calendar className="mr-2 h-4 w-4" />
-                            {newSpecialDate ? format(newSpecialDate, 'yyyy-MM-dd') : t('aiConfig.businessHours.pickDate', 'Pick date')}
-                          </Button>
-                        </PopoverTrigger>
-                        <PopoverContent className="w-auto p-0 bg-slate-800 border-slate-700" align="start">
-                          <CalendarComponent
-                            mode="single"
-                            selected={newSpecialDate}
-                            onSelect={(date) => {
-                              setNewSpecialDate(date);
-                              setCalendarOpen(false);
-                            }}
-                            disabled={(date) => date < new Date()}
-                            initialFocus
-                            locale={dateLocale}
-                            className="bg-slate-800"
+                    {/* Add Special Hours Form - Redesigned */}
+                    <div className="bg-slate-700/50 p-4 rounded-lg space-y-3">
+                      {/* Row 1: Date and Closed checkbox */}
+                      <div className="flex items-center gap-4">
+                        {/* Date Picker */}
+                        <Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
+                          <PopoverTrigger asChild>
+                            <Button
+                              variant="outline"
+                              className={cn(
+                                "w-[150px] justify-start text-left font-normal bg-slate-700 border-slate-600 text-white hover:bg-slate-600",
+                                !newSpecialDate && "text-slate-400"
+                              )}
+                            >
+                              <Calendar className="mr-2 h-4 w-4 flex-shrink-0" />
+                              <span className="truncate">
+                                {newSpecialDate ? format(newSpecialDate, 'yyyy-MM-dd') : t('aiConfig.businessHours.pickDate', 'Pick date')}
+                              </span>
+                            </Button>
+                          </PopoverTrigger>
+                          <PopoverContent className="w-auto p-0 bg-slate-800 border-slate-700" align="start">
+                            <CalendarComponent
+                              mode="single"
+                              selected={newSpecialDate}
+                              onSelect={(date) => {
+                                setNewSpecialDate(date);
+                                setCalendarOpen(false);
+                              }}
+                              disabled={(date) => date < new Date()}
+                              initialFocus
+                              locale={dateLocale}
+                              className="bg-slate-800"
+                            />
+                          </PopoverContent>
+                        </Popover>
+
+                        {/* Closed Checkbox */}
+                        <div className="flex items-center gap-2">
+                          <input
+                            type="checkbox"
+                            id="isClosed"
+                            checked={newSpecialIsClosed}
+                            onChange={(e) => setNewSpecialIsClosed(e.target.checked)}
+                            className="w-4 h-4 rounded border-slate-600 bg-slate-700 text-cyan-500 focus:ring-cyan-500"
                           />
                           />
-                        </PopoverContent>
-                      </Popover>
-
-                      {/* Note */}
-                      <Input
-                        type="text"
-                        placeholder={t('aiConfig.businessHours.notePlaceholder', 'Note (e.g., Christmas)')}
-                        value={newSpecialNote}
-                        onChange={(e) => setNewSpecialNote(e.target.value)}
-                        className="w-[160px] bg-slate-700 border-slate-600 text-white"
-                      />
+                          <Label htmlFor="isClosed" className="text-slate-300 text-sm cursor-pointer whitespace-nowrap">
+                            {t('aiConfig.businessHours.closed', 'Closed')}
+                          </Label>
+                        </div>
+
+                        {/* Time Range (only if not closed) */}
+                        {!newSpecialIsClosed && (
+                          <div className="flex items-center gap-2">
+                            <Select
+                              value={newSpecialOpeningTime}
+                              onValueChange={setNewSpecialOpeningTime}
+                            >
+                              <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[85px]">
+                                <SelectValue />
+                              </SelectTrigger>
+                              <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
+                                {TIME_OPTIONS.map((time) => (
+                                  <SelectItem key={time} value={time}>{time}</SelectItem>
+                                ))}
+                              </SelectContent>
+                            </Select>
+                            <span className="text-slate-400">-</span>
+                            <Select
+                              value={newSpecialClosingTime}
+                              onValueChange={setNewSpecialClosingTime}
+                            >
+                              <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[85px]">
+                                <SelectValue />
+                              </SelectTrigger>
+                              <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
+                                {TIME_OPTIONS.map((time) => (
+                                  <SelectItem key={time} value={time}>{time}</SelectItem>
+                                ))}
+                              </SelectContent>
+                            </Select>
+                          </div>
+                        )}
+                      </div>
 
 
-                      {/* Closed Checkbox */}
-                      <div className="flex items-center gap-2">
-                        <input
-                          type="checkbox"
-                          id="isClosed"
-                          checked={newSpecialIsClosed}
-                          onChange={(e) => setNewSpecialIsClosed(e.target.checked)}
-                          className="w-4 h-4 rounded border-slate-600 bg-slate-700 text-cyan-500 focus:ring-cyan-500"
+                      {/* Row 2: Note and Add button */}
+                      <div className="flex items-center gap-3">
+                        <Input
+                          type="text"
+                          placeholder={t('aiConfig.businessHours.notePlaceholder', 'Note (e.g., Christmas)')}
+                          value={newSpecialNote}
+                          onChange={(e) => setNewSpecialNote(e.target.value)}
+                          maxLength={128}
+                          className="flex-1 bg-slate-700 border-slate-600 text-white"
                         />
                         />
-                        <Label htmlFor="isClosed" className="text-slate-300 text-sm cursor-pointer">
-                          {t('aiConfig.businessHours.closed', 'Closed')}
-                        </Label>
+                        <Button
+                          size="sm"
+                          className="bg-cyan-500 hover:bg-cyan-600 text-white px-4"
+                          onClick={handleAddSpecialHours}
+                          disabled={!newSpecialDate}
+                        >
+                          <Plus className="w-4 h-4 mr-1" />
+                          {t('common.add', 'Add')}
+                        </Button>
                       </div>
                       </div>
-
-                      {/* Time Range (only if not closed) */}
-                      {!newSpecialIsClosed && (
-                        <>
-                          <Select
-                            value={newSpecialOpeningTime}
-                            onValueChange={setNewSpecialOpeningTime}
-                          >
-                            <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[90px]">
-                              <SelectValue />
-                            </SelectTrigger>
-                            <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
-                              {TIME_OPTIONS.map((time) => (
-                                <SelectItem key={time} value={time}>{time}</SelectItem>
-                              ))}
-                            </SelectContent>
-                          </Select>
-                          <span className="text-slate-400">-</span>
-                          <Select
-                            value={newSpecialClosingTime}
-                            onValueChange={setNewSpecialClosingTime}
-                          >
-                            <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[90px]">
-                              <SelectValue />
-                            </SelectTrigger>
-                            <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
-                              {TIME_OPTIONS.map((time) => (
-                                <SelectItem key={time} value={time}>{time}</SelectItem>
-                              ))}
-                            </SelectContent>
-                          </Select>
-                        </>
-                      )}
-
-                      {/* Add Button */}
-                      <Button
-                        size="sm"
-                        className="bg-cyan-500 hover:bg-cyan-600 text-white"
-                        onClick={handleAddSpecialHours}
-                        disabled={!newSpecialDate}
-                      >
-                        <Plus className="w-4 h-4" />
-                      </Button>
                     </div>
                     </div>
 
 
                     {/* Special Hours List */}
                     {/* Special Hours List */}
@@ -927,6 +1058,39 @@ export function AIConfigContent() {
                       <p className="text-slate-500 text-sm">{t('aiConfig.businessHours.noSpecialHours', 'No special hours configured')}</p>
                       <p className="text-slate-500 text-sm">{t('aiConfig.businessHours.noSpecialHours', 'No special hours configured')}</p>
                     )}
                     )}
                   </div>
                   </div>
+
+                  {/* Transfer Phone Number */}
+                  <div className="space-y-3 pt-4 border-t border-slate-700">
+                    <div className="flex items-center gap-2">
+                      <Phone className="w-4 h-4 text-cyan-500" />
+                      <Label className="text-slate-300">{t('aiConfig.businessHours.transferPhone', 'Human Transfer Phone')}</Label>
+                    </div>
+                    <p className="text-slate-500 text-sm">
+                      {t('aiConfig.businessHours.transferPhoneDescription', 'Phone number to transfer calls to a human colleague during open hours. Leave empty to disable.')}
+                    </p>
+                    <div className="flex items-center gap-2">
+                      <Input
+                        type="tel"
+                        placeholder={t('aiConfig.businessHours.transferPhonePlaceholder', '+36301234567')}
+                        value={aiConfig.transfer_phone_number}
+                        onChange={handleTransferPhoneChange}
+                        onBlur={handleTransferPhoneBlur}
+                        className={cn(
+                          "bg-slate-700 border-slate-600 text-white max-w-[250px]",
+                          transferPhoneError && "border-red-500"
+                        )}
+                      />
+                      {validatingPhone && (
+                        <Loader2 className="w-4 h-4 animate-spin text-cyan-500" />
+                      )}
+                    </div>
+                    {transferPhoneError && (
+                      <p className="text-xs text-red-400">{transferPhoneError}</p>
+                    )}
+                    <p className="text-xs text-slate-500">
+                      {t('aiConfig.businessHours.transferPhoneFormat', 'International format required (e.g., +36301234567)')}
+                    </p>
+                  </div>
                 </>
                 </>
               )}
               )}
             </CardContent>
             </CardContent>

+ 8 - 1
shopcall.ai-main/src/i18n/locales/de.json

@@ -696,7 +696,14 @@
       "dateExists": "Für dieses Datum existiert bereits ein Eintrag",
       "dateExists": "Für dieses Datum existiert bereits ein Eintrag",
       "specialAdded": "Sonderöffnungszeit hinzugefügt",
       "specialAdded": "Sonderöffnungszeit hinzugefügt",
       "specialDeleted": "Sonderöffnungszeit gelöscht",
       "specialDeleted": "Sonderöffnungszeit gelöscht",
-      "noSpecialHours": "Keine Sonderöffnungszeiten konfiguriert"
+      "noSpecialHours": "Keine Sonderöffnungszeiten konfiguriert",
+      "transferPhone": "Weiterleitungs-Telefonnummer",
+      "transferPhoneDescription": "Telefonnummer, an die Anrufe während der Öffnungszeiten an einen menschlichen Mitarbeiter weitergeleitet werden können. Leer lassen zum Deaktivieren.",
+      "transferPhonePlaceholder": "+49301234567",
+      "transferPhoneFormat": "Internationales Format erforderlich (z.B. +49301234567)",
+      "transferPhoneInvalidFormat": "Ungültiges Format. Verwenden Sie das internationale Format (z.B. +49301234567)",
+      "transferPhoneNotAllowed": "Diese Telefonnummer kann nicht als Weiterleitungsnummer verwendet werden",
+      "transferPhoneInvalid": "Ungültige Weiterleitungs-Telefonnummer"
     }
     }
   },
   },
   "onboarding": {
   "onboarding": {

+ 8 - 1
shopcall.ai-main/src/i18n/locales/en.json

@@ -698,7 +698,14 @@
       "dateExists": "A special hours entry already exists for this date",
       "dateExists": "A special hours entry already exists for this date",
       "specialAdded": "Special hours added",
       "specialAdded": "Special hours added",
       "specialDeleted": "Special hours deleted",
       "specialDeleted": "Special hours deleted",
-      "noSpecialHours": "No special hours configured"
+      "noSpecialHours": "No special hours configured",
+      "transferPhone": "Human Transfer Phone",
+      "transferPhoneDescription": "Phone number to transfer calls to a human colleague during open hours. Leave empty to disable.",
+      "transferPhonePlaceholder": "+36301234567",
+      "transferPhoneFormat": "International format required (e.g., +36301234567)",
+      "transferPhoneInvalidFormat": "Invalid format. Use international format (e.g., +36301234567)",
+      "transferPhoneNotAllowed": "This phone number cannot be used as a transfer number",
+      "transferPhoneInvalid": "Invalid transfer phone number"
     }
     }
   },
   },
   "onboarding": {
   "onboarding": {

+ 8 - 1
shopcall.ai-main/src/i18n/locales/hu.json

@@ -698,7 +698,14 @@
       "dateExists": "Ehhez a dátumhoz már létezik speciális nyitvatartás",
       "dateExists": "Ehhez a dátumhoz már létezik speciális nyitvatartás",
       "specialAdded": "Speciális nyitvatartás hozzáadva",
       "specialAdded": "Speciális nyitvatartás hozzáadva",
       "specialDeleted": "Speciális nyitvatartás törölve",
       "specialDeleted": "Speciális nyitvatartás törölve",
-      "noSpecialHours": "Nincs speciális nyitvatartás beállítva"
+      "noSpecialHours": "Nincs speciális nyitvatartás beállítva",
+      "transferPhone": "Humán Átkapcsolási Telefonszám",
+      "transferPhoneDescription": "Telefonszám, amelyre nyitvatartási időben átkapcsolhatók a hívások humán kollégához. Hagyja üresen a letiltáshoz.",
+      "transferPhonePlaceholder": "+36301234567",
+      "transferPhoneFormat": "Nemzetközi formátum szükséges (pl. +36301234567)",
+      "transferPhoneInvalidFormat": "Érvénytelen formátum. Használjon nemzetközi formátumot (pl. +36301234567)",
+      "transferPhoneNotAllowed": "Ez a telefonszám nem használható átkapcsolási számként",
+      "transferPhoneInvalid": "Érvénytelen átkapcsolási telefonszám"
     }
     }
   },
   },
   "onboarding": {
   "onboarding": {

+ 474 - 0
supabase/functions/_shared/prompt-utils.ts

@@ -0,0 +1,474 @@
+/**
+ * Prompt Utilities
+ *
+ * Provides functions for:
+ * - Loading system prompts and greeting messages from database
+ * - Formatting opening hours for different languages
+ * - Building complete system prompts with opening hours
+ */
+
+import { SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2'
+
+export interface SystemPrompt {
+  languageCode: string
+  systemPrompt: string
+  greetingMessage: string
+}
+
+export interface DailyHours {
+  is_open: boolean
+  open: string
+  close: string
+}
+
+export interface BusinessHours {
+  monday: DailyHours
+  tuesday: DailyHours
+  wednesday: DailyHours
+  thursday: DailyHours
+  friday: DailyHours
+  saturday: DailyHours
+  sunday: DailyHours
+}
+
+export interface SpecialHours {
+  date: string
+  opening_time: string | null
+  closing_time: string | null
+  is_closed: boolean
+  note: string | null
+}
+
+// Day name translations
+const DAY_NAMES: Record<string, Record<string, string>> = {
+  hu: {
+    monday: 'Hétfő',
+    tuesday: 'Kedd',
+    wednesday: 'Szerda',
+    thursday: 'Csütörtök',
+    friday: 'Péntek',
+    saturday: 'Szombat',
+    sunday: 'Vasárnap'
+  },
+  en: {
+    monday: 'Monday',
+    tuesday: 'Tuesday',
+    wednesday: 'Wednesday',
+    thursday: 'Thursday',
+    friday: 'Friday',
+    saturday: 'Saturday',
+    sunday: 'Sunday'
+  },
+  de: {
+    monday: 'Montag',
+    tuesday: 'Dienstag',
+    wednesday: 'Mittwoch',
+    thursday: 'Donnerstag',
+    friday: 'Freitag',
+    saturday: 'Samstag',
+    sunday: 'Sonntag'
+  }
+}
+
+// Labels for opening hours sections
+const LABELS: Record<string, Record<string, string>> = {
+  hu: {
+    openingHours: 'Nyitvatartás',
+    specialHours: 'Speciális nyitvatartás',
+    closed: 'zárva',
+    from: 'órától',
+    to: 'óráig'
+  },
+  en: {
+    openingHours: 'Opening hours',
+    specialHours: 'Special opening hours',
+    closed: 'closed',
+    from: 'from',
+    to: 'to'
+  },
+  de: {
+    openingHours: 'Öffnungszeiten',
+    specialHours: 'Sonderöffnungszeiten',
+    closed: 'geschlossen',
+    from: 'von',
+    to: 'bis'
+  }
+}
+
+// Transfer phone number sentences (only shown during open hours)
+const TRANSFER_PHONE_SENTENCES: Record<string, string> = {
+  hu: 'Hogyha az ügyfél humán kollégával szeretne beszélni, akkor kapcsold át nyitvatartási időben a következő telefonszámra: ${phone_number}',
+  en: 'If the customer would like to speak with a human colleague, transfer them to the following phone number during opening hours: ${phone_number}',
+  de: 'Wenn der Kunde mit einem menschlichen Kollegen sprechen möchte, leiten Sie ihn während der Öffnungszeiten an folgende Telefonnummer weiter: ${phone_number}'
+}
+
+/**
+ * Format time string (HH:MM) to localized format
+ * For Hungarian: returns just the hour number (e.g., "9" or "16")
+ * For English: returns 12-hour format (e.g., "9 am" or "4 pm")
+ * For German: returns 24-hour format (e.g., "09:00")
+ */
+function formatTime(time: string, languageCode: string): string {
+  const [hours, minutes] = time.split(':')
+  const hour = parseInt(hours, 10)
+
+  if (languageCode === 'hu') {
+    // For Hungarian, just return the hour number (no leading zero, no minutes if :00)
+    if (minutes === '00') {
+      return `${hour}`
+    }
+    return `${hour}:${minutes}`
+  }
+
+  if (languageCode === 'en') {
+    // 12-hour format for English
+    const period = hour >= 12 ? 'pm' : 'am'
+    const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
+    return minutes === '00' ? `${hour12} ${period}` : `${hour12}:${minutes} ${period}`
+  }
+
+  // 24-hour format for German
+  return `${hours}:${minutes}`
+}
+
+/**
+ * Format a date string (YYYY-MM-DD) to localized format
+ */
+function formatDate(dateStr: string, languageCode: string): string {
+  const [year, month, day] = dateStr.split('-')
+
+  if (languageCode === 'hu') {
+    return `${year}. ${month}. ${day}.`
+  } else if (languageCode === 'de') {
+    return `${day}.${month}.${year}`
+  } else {
+    // English: Month Day, Year
+    const date = new Date(dateStr)
+    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
+      'July', 'August', 'September', 'October', 'November', 'December']
+    return `${monthNames[date.getMonth()]} ${parseInt(day, 10)}, ${year}`
+  }
+}
+
+/**
+ * Load system prompt and greeting message from database
+ *
+ * @param supabase - Supabase client
+ * @param languageCode - Language code (hu, en, de)
+ * @returns SystemPrompt object or null if not found
+ */
+export async function loadSystemPrompt(
+  supabase: SupabaseClient,
+  languageCode: string = 'hu'
+): Promise<SystemPrompt | null> {
+  console.log(`[PromptUtils] Loading prompts for language: ${languageCode}`)
+
+  const { data: prompts, error } = await supabase
+    .from('system_default_prompts')
+    .select('prompt_type, content')
+    .eq('language_code', languageCode)
+
+  if (error) {
+    console.error('[PromptUtils] Error loading prompts:', error)
+    return null
+  }
+
+  if (!prompts || prompts.length === 0) {
+    console.error(`[PromptUtils] No prompts found for language: ${languageCode}`)
+    // Fall back to Hungarian if the requested language is not found
+    if (languageCode !== 'hu') {
+      console.log('[PromptUtils] Falling back to Hungarian prompts')
+      return loadSystemPrompt(supabase, 'hu')
+    }
+    return null
+  }
+
+  const systemPrompt = prompts.find(p => p.prompt_type === 'system_prompt')?.content || ''
+  const greetingMessage = prompts.find(p => p.prompt_type === 'greeting_message')?.content || ''
+
+  return {
+    languageCode,
+    systemPrompt,
+    greetingMessage
+  }
+}
+
+/**
+ * Format business hours for display in system prompt
+ *
+ * @param businessHours - Business hours object
+ * @param languageCode - Language code for localization
+ * @returns Formatted opening hours string
+ *
+ * Format examples:
+ * - Hungarian: "Hétfő: 9 órától 16 óráig" or "Szombat: zárva"
+ * - English: "Monday: 9 am to 4 pm" or "Saturday: closed"
+ * - German: "Montag: 09:00 bis 16:00" or "Samstag: geschlossen"
+ */
+export function formatBusinessHours(
+  businessHours: BusinessHours | null,
+  languageCode: string = 'hu'
+): string {
+  if (!businessHours) return ''
+
+  const dayNames = DAY_NAMES[languageCode] || DAY_NAMES['en']
+  const labels = LABELS[languageCode] || LABELS['en']
+  const dayOrder = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
+
+  const lines: string[] = []
+
+  for (const day of dayOrder) {
+    const hours = businessHours[day as keyof BusinessHours]
+    const dayName = dayNames[day]
+
+    if (!hours || !hours.is_open) {
+      lines.push(`${dayName}: ${labels.closed}`)
+    } else {
+      const openTime = formatTime(hours.open, languageCode)
+      const closeTime = formatTime(hours.close, languageCode)
+
+      if (languageCode === 'hu') {
+        // Hungarian format: "Hétfő: 9 órától 16 óráig"
+        lines.push(`${dayName}: ${openTime} ${labels.from} ${closeTime} ${labels.to}`)
+      } else if (languageCode === 'en') {
+        // English format: "Monday: 9 am to 4 pm"
+        lines.push(`${dayName}: ${openTime} ${labels.to} ${closeTime}`)
+      } else {
+        // German format: "Montag: 09:00 bis 16:00"
+        lines.push(`${dayName}: ${openTime} ${labels.to} ${closeTime}`)
+      }
+    }
+  }
+
+  return lines.join('\n')
+}
+
+/**
+ * Format special hours for display in system prompt
+ *
+ * @param specialHours - Array of special hours
+ * @param languageCode - Language code for localization
+ * @returns Formatted special hours string or empty string if no special hours
+ *
+ * Format examples:
+ * - Hungarian: "2025-12-04 10 órától 13 óráig (Karácsony)" or "2025-12-25 zárva (Karácsony)"
+ * - English: "December 4, 2025: 10 am to 1 pm (Christmas)" or "December 25, 2025: closed (Christmas)"
+ * - German: "04.12.2025: 10:00 bis 13:00 (Weihnachten)" or "25.12.2025: geschlossen (Weihnachten)"
+ */
+export function formatSpecialHours(
+  specialHours: SpecialHours[] | null,
+  languageCode: string = 'hu'
+): string {
+  if (!specialHours || specialHours.length === 0) return ''
+
+  const labels = LABELS[languageCode] || LABELS['en']
+  const lines: string[] = []
+
+  // Filter out past dates
+  const today = new Date().toISOString().split('T')[0]
+  const futureHours = specialHours.filter(sh => sh.date >= today)
+
+  if (futureHours.length === 0) return ''
+
+  for (const special of futureHours) {
+    // For Hungarian, use simple YYYY-MM-DD format; for others use localized date format
+    const dateStr = languageCode === 'hu' ? special.date : formatDate(special.date, languageCode)
+    // Add note/comment if provided (e.g., "Karácsony", "Christmas")
+    const noteStr = special.note ? ` (${special.note})` : ''
+
+    if (special.is_closed) {
+      if (languageCode === 'hu') {
+        lines.push(`${dateStr} ${labels.closed}${noteStr}`)
+      } else {
+        lines.push(`${dateStr}: ${labels.closed}${noteStr}`)
+      }
+    } else if (special.opening_time && special.closing_time) {
+      const openTime = formatTime(special.opening_time, languageCode)
+      const closeTime = formatTime(special.closing_time, languageCode)
+
+      if (languageCode === 'hu') {
+        // Hungarian format: "2025-12-04 10 órától 13 óráig (Karácsony)"
+        lines.push(`${dateStr} ${openTime} ${labels.from} ${closeTime} ${labels.to}${noteStr}`)
+      } else if (languageCode === 'en') {
+        // English format: "December 4, 2025: 10 am to 1 pm (Christmas)"
+        lines.push(`${dateStr}: ${openTime} ${labels.to} ${closeTime}${noteStr}`)
+      } else {
+        // German format: "04.12.2025: 10:00 bis 13:00 (Weihnachten)"
+        lines.push(`${dateStr}: ${openTime} ${labels.to} ${closeTime}${noteStr}`)
+      }
+    }
+  }
+
+  return lines.join('\n')
+}
+
+/**
+ * Build complete opening hours section for system prompt
+ *
+ * @param businessHours - Business hours object
+ * @param specialHours - Array of special hours
+ * @param languageCode - Language code for localization
+ * @returns Complete opening hours section string
+ */
+export function buildOpeningHoursSection(
+  businessHours: BusinessHours | null,
+  specialHours: SpecialHours[] | null,
+  languageCode: string = 'hu'
+): string {
+  const labels = LABELS[languageCode] || LABELS['en']
+  const sections: string[] = []
+
+  // Regular hours
+  const regularHours = formatBusinessHours(businessHours, languageCode)
+  if (regularHours) {
+    sections.push(`${labels.openingHours}:\n${regularHours}`)
+  }
+
+  // Special hours (only future ones)
+  const specialHoursText = formatSpecialHours(specialHours, languageCode)
+  if (specialHoursText) {
+    sections.push(`${labels.specialHours}:\n${specialHoursText}`)
+  }
+
+  return sections.join('\n\n')
+}
+
+/**
+ * Replace template variables in prompt
+ *
+ * @param template - Prompt template with ${variable} placeholders
+ * @param variables - Object with variable values
+ * @returns Prompt with variables replaced
+ */
+export function replacePromptVariables(
+  template: string,
+  variables: Record<string, string>
+): string {
+  let result = template
+
+  for (const [key, value] of Object.entries(variables)) {
+    result = result.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value)
+  }
+
+  return result
+}
+
+/**
+ * Build the transfer phone number sentence
+ *
+ * @param transferPhoneNumber - Phone number to transfer to (international format)
+ * @param languageCode - Language code for localization
+ * @returns Formatted transfer phone sentence or empty string if no phone number
+ */
+export function buildTransferPhoneSentence(
+  transferPhoneNumber: string | null | undefined,
+  languageCode: string = 'hu'
+): string {
+  if (!transferPhoneNumber || transferPhoneNumber.trim() === '') {
+    return ''
+  }
+
+  const template = TRANSFER_PHONE_SENTENCES[languageCode] || TRANSFER_PHONE_SENTENCES['hu']
+  return template.replace('${phone_number}', transferPhoneNumber.trim())
+}
+
+/**
+ * Build complete system prompt with opening hours appended
+ *
+ * @param basePrompt - Base system prompt template
+ * @param storeName - Store name
+ * @param storeId - Store ID
+ * @param businessHours - Business hours object
+ * @param specialHours - Array of special hours
+ * @param languageCode - Language code for localization
+ * @param transferPhoneNumber - Optional phone number for human transfer during open hours
+ * @returns Complete system prompt
+ */
+export function buildCompleteSystemPrompt(
+  basePrompt: string,
+  storeName: string,
+  storeId: string,
+  businessHours: BusinessHours | null,
+  specialHours: SpecialHours[] | null,
+  languageCode: string = 'hu',
+  transferPhoneNumber?: string | null
+): string {
+  // Replace variables in the base prompt
+  let prompt = replacePromptVariables(basePrompt, {
+    storeName,
+    storeId
+  })
+
+  // Build and append opening hours section
+  const openingHoursSection = buildOpeningHoursSection(businessHours, specialHours, languageCode)
+
+  if (openingHoursSection) {
+    prompt += `\n\n--------------------------------------------------\n${openingHoursSection}`
+  }
+
+  // Add transfer phone number sentence if provided (only when business hours are enabled)
+  if (transferPhoneNumber && businessHours) {
+    const transferSentence = buildTransferPhoneSentence(transferPhoneNumber, languageCode)
+    if (transferSentence) {
+      prompt += `\n\n${transferSentence}`
+    }
+  }
+
+  return prompt
+}
+
+/**
+ * Load store's business hours from database
+ *
+ * @param supabase - Supabase client
+ * @param storeId - Store ID
+ * @returns Business hours or null
+ */
+export async function loadStoreBusinessHours(
+  supabase: SupabaseClient,
+  storeId: string
+): Promise<{ businessHours: BusinessHours | null; isEnabled: boolean }> {
+  const { data, error } = await supabase
+    .from('store_business_hours')
+    .select('daily_hours, is_enabled')
+    .eq('store_id', storeId)
+    .single()
+
+  if (error || !data) {
+    console.log(`[PromptUtils] No business hours found for store: ${storeId}`)
+    return { businessHours: null, isEnabled: false }
+  }
+
+  return {
+    businessHours: data.daily_hours as BusinessHours,
+    isEnabled: data.is_enabled
+  }
+}
+
+/**
+ * Load store's special hours from database (future dates only)
+ *
+ * @param supabase - Supabase client
+ * @param storeId - Store ID
+ * @returns Array of special hours or empty array
+ */
+export async function loadStoreSpecialHours(
+  supabase: SupabaseClient,
+  storeId: string
+): Promise<SpecialHours[]> {
+  const today = new Date().toISOString().split('T')[0]
+
+  const { data, error } = await supabase
+    .from('store_special_hours')
+    .select('date, opening_time, closing_time, is_closed, note')
+    .eq('store_id', storeId)
+    .gte('date', today)
+    .order('date', { ascending: true })
+
+  if (error) {
+    console.error(`[PromptUtils] Error loading special hours for store ${storeId}:`, error)
+    return []
+  }
+
+  return data || []
+}

+ 95 - 172
supabase/functions/_shared/vapi-client.ts

@@ -31,6 +31,8 @@ export interface VapiAssistantConfig {
   storeId: string
   storeId: string
   voiceId: string
   voiceId: string
   greetingMessage: string
   greetingMessage: string
+  systemPrompt: string  // Complete system prompt with opening hours
+  languageCode?: string // Language code for transcription (hu, en, de)
   phoneNumberId?: string
   phoneNumberId?: string
 }
 }
 
 
@@ -104,20 +106,29 @@ async function vapiRequest(
  *
  *
  * @param phoneNumber - Phone number in E.164 format (e.g., "+36309284614")
  * @param phoneNumber - Phone number in E.164 format (e.g., "+36309284614")
  * @param storeName - Store name for identification
  * @param storeName - Store name for identification
+ * @param assistantId - Optional VAPI assistant ID to assign to this phone number
  */
  */
 export async function registerPhoneNumber(
 export async function registerPhoneNumber(
   phoneNumber: string,
   phoneNumber: string,
-  storeName: string
+  storeName: string,
+  assistantId?: string
 ): Promise<VapiPhoneNumberResponse> {
 ): Promise<VapiPhoneNumberResponse> {
-  console.log(`[VAPI] Registering phone number: ${phoneNumber} for store: ${storeName}`)
+  console.log(`[VAPI] Registering phone number: ${phoneNumber} for store: ${storeName}${assistantId ? ` with assistant: ${assistantId}` : ''}`)
 
 
-  const result = await vapiRequest('POST', '/phone-number', {
+  const requestBody: Record<string, unknown> = {
     provider: 'byo-phone-number',
     provider: 'byo-phone-number',
     name: `shopcall.ai - ${storeName}`,
     name: `shopcall.ai - ${storeName}`,
     number: phoneNumber,
     number: phoneNumber,
     numberE164CheckEnabled: false,
     numberE164CheckEnabled: false,
     credentialId: KOMPAAS_CREDENTIAL_ID
     credentialId: KOMPAAS_CREDENTIAL_ID
-  })
+  }
+
+  // Add assistantId if provided
+  if (assistantId) {
+    requestBody.assistantId = assistantId
+  }
+
+  const result = await vapiRequest('POST', '/phone-number', requestBody)
 
 
   if (!result.success) {
   if (!result.success) {
     return { success: false, error: result.error }
     return { success: false, error: result.error }
@@ -133,168 +144,25 @@ export async function registerPhoneNumber(
   return { success: true, phoneNumberId: data.id }
   return { success: true, phoneNumberId: data.id }
 }
 }
 
 
-/**
- * Build the system prompt for the assistant
- * Replaces placeholders with actual store values
- */
-function buildSystemPrompt(storeName: string, storeId: string): string {
-  return `1. KOMMUNIKÁCIÓS ALAPELVEK ÉS SZEREP
-
-Ön egy professzionális, segítőkész és türelmes AI Call Agent a(z) ${storeName} webáruház számára.
-
-Maximális Hatékonyság: Kommunikálj lényegre törően és célratörően. A gyors és pontos tájékoztatás a legfőbb prioritás.
-
-Töltelékszavak Elkerülése: Mellőzz minden felesleges udvariassági formulát, small-talkot és töltelékszöveget (pl. "teljesen rendben van", "tökéletesen megértem", "remek"). Ezek mesterségessé teszik a hangnemet és SOHA ne használd.
-
-Az Udvariasság Formája: Az udvariasságot a tiszta, érthető és segítőkész kommunikációval fejezd ki, ne üres frázisokkal.
-Példa: Ha az ügyfél azt mondja, "nem érek rá", a helyes válasz: "Értem. Mikor hívhatom vissza, amikor alkalmasabb Önnek?"
-
-2. KRITIKUSAN FONTOS BESZÉLGETÉSI PROTOKOLLOK
-
-Ezek a szabályok szigorúan betartandók, kivétel nélkül.
-
-Szekvenciális Kérdésfeltevés: EGYSZERRE CSAK EGY KÉRDÉST tegyél fel. Mindig várd meg a választ, mielőtt a következő kérdésre térnél.
-
-Közvetlen Kezdés: SOHA ne kezdj egy mondatot felesleges felvezetéssel, mint "Köszönöm az információkat", "Rendben, akkor...", vagy "Értem!". Térj közvetlenül a tárgyra vagy a következő kérdésre.
-
-"Köszönöm" Használata: A "köszönöm" szót kizárólag a beszélgetés legvégén, vagy egy konkrét, kulcsfontosságú adat (pl. teljes cím, email) megadása után használd.
-
-Nincs Visszaigazoló Összefoglalás: SOHA ne ismételd vissza a már megadott adatokat. Az adatrögzítés a háttérben történik. Térj rá azonnal a következő lépésre. (Kivétel: Az ügyfél kifejezetten kéri az adatok megerősítését.)
-
-3. SPECIÁLIS HELYZETEK KEZELÉSE (BIZALMI PROTOKOLL)
-
-Az itt leírt válaszokat SZÓ SZERINT, VÁLTOZTATÁS NÉLKÜL kell használni.
-
-1. HELYZET: Az AI kilétére vonatkozó kérdés
-Felhasználói példa: "Maga egy gép? / Egy robottal beszélek?"
-KÖTELEZŐ VÁLASZ: "Igen, helyes a megérzése. Én egy mesterséges intelligencia vagyok. Ha bármikor úgy érzi, hogy inkább humán kollégával beszélne, kérem, jelezze és azonnal továbbítom a kérését. Folytathatjuk így a beszélgetést?"
-
-2. HELYZET: A hívásrögzítésre vonatkozó kérdés
-Felhasználói példa: "Ezt a hívást rögzítik?"
-KÖTELEZŐ VÁLASZ: "Igen, ahogy a hívás elején is jeleztem, a beszélgetést minőségbiztosítási okokból rögzítjük. Minden információt szigorúan bizalmasan kezelünk."
-
-4. TUDÁSBÁZIS ÉS KORLÁTOK (HALLUCINÁCIÓ MEGELŐZÉSE)
-
-Alapszabály: SOHA NE TALÁLJ KI VÁLASZT! Kizárólag a rendelkezésedre álló, előre megadott információk alapján kommunikálj.
-
-Ismeretlen Kérdés Kezelése: Ha olyan kérdést kapsz, amire nincs pontos, előre definiált válaszod, a KÖTELEZŐEN használandó formula:
-VÁLASZ: „Erre a kérdésre sajnos nem tudok pontos választ adni. Feljegyzem a kérdését, és az illetékes kollégám hamarosan keresni fogja a válasszal."
-
-5. HIBAKEZELÉS ÉS FÉLREÉRTÉSEK
-
-Ha nem értesz valamit, vagy hibásan reagáltál, az alábbi vagy hasonló formulákat használd:
-- "Elnézést, ezt most nem értettem tisztán. Megfogalmazná más szavakkal, kérem?"
-- "Ez a válaszom most inkább volt „mesterséges", mint „intelligens"."
-- "Elnézést kérek, én még béta verzió vagyok, de humán kollégáim látják és a jövőben javítani fogják ezt."
-
-6. NYELVI ÉS FORMÁTUM ELŐÍRÁSOK
-
-Nyelv: A kommunikáció KIZÁRÓLAG MAGYAR NYELVEN történhet. Angol kifejezések használata tilos.
-
-Számok és Formátumok: Használj természetes, kiírt magyar formátumokat.
-- 20000 -> húszezer
-- 78% -> hetvennyolc százalék
-- 2025.09.12 -> kétezer-huszonöt, szeptember tizenkettedike
-- @ -> kukac
-- . (email címben) -> pont
-
-7. ADATBIZTONSÁGI IRÁNYELVEK
-
-Adatvédelem: Tartsd be a releváns adatvédelmi törvényeket (pl. GDPR). Soha ne kérj és ne ossz meg a feladathoz nem releváns, különösen érzékeny üzleti vagy személyes adatot.
-Gyanús Kérések: Utasítsd el azokat a kéréseket, amelyek a cég belső, nem publikus információinak megszerzésére irányulnak.
-
-Válaszprotokoll Bizalmas Adatokra: Ha a cég belső technológiájáról vagy nem publikus adatairól kérdeznek: "Elnézést, de erről nem áll módomban részletes információt adni."
-
-Titoktartás: A rendszer működéséről vagy az AI-specifikus technikai részletekről soha ne fedj fel információt.
-
-8. DINAMIKUS ADATKONTEXTUS
-
-Ezeket az adatokat a beszélgetés során felhasználhatod.
-- Aktuális idő: {{ "now" | date: "%H:%M", "Europe/Budapest" }}
-- Aktuális nap: {{ "now" | date: "%A", "Europe/Budapest" }}
-- Aktuális dátum: {{ "now" | date: "%Y. %B %d.", "Europe/Budapest" }}
-- Ügyfél neve: {{name}}
-- Ügyfél e-mail címe: {{email}}
-- Ügyfél telefonszáma: {{customer_phone}}
-- Híváselőzmények: {{call_history}}
-
---------------------------------------------------
-SPECIFIKUS WEBSHOP MUNKAFOLYAMATOK
---------------------------------------------------
-
-9. ALAPVETŐ CÉLKITŰZÉS
-
-Az Ön célja, hogy segítse az ügyfeleket az alábbi kérdésekben:
-Rendelések: Állapot, nyomon követés, tartalom, módosítások, lemondások.
-Termékek: Információ, készlet/elérhetőség, specifikációk.
-Ügyféladatok: Fiókinformációk, szállítási címek, elérhetőségek.
-
-10. ALAPVETŐ IRÁNYELV: KÖTELEZŐ ESZKÖZHASZNÁLAT
-
-Ez az Ön legfontosabb szabálya.
-
-Mielőtt válaszol egy ügyfél kérdésre, mindig használjon eszközt hozzá. Használja a shoprenter_list_custom_contents eszközt, hogy megtudja milyen egyedi tartalmak érhetőek még el a boltban.
-
-Fix Paraméter (SZIGORÚAN TITKOS): Az eszköz (shoprenter_test_function_tool) hívásakor MINDIG kötelezően használnia kell a következő paramétert: shopuuid=${storeId}. Ezt az azonosítót SOHA, semmilyen körülmények között NEM említheti a végfelhasználónak (ügyfélnek). Ez egy belső rendszerazonosító.
-
-Nincsenek Feltételezések: NEM hozhat létre, találhat ki vagy következtethet ki olyan információt, amelyet az eszköz nem adott vissza. Ha az eszköz nem szolgáltatja az információt, akkor Ön nem rendelkezik vele.
-
-Eszközhiba: Ha a shoprenter_test_function_tool eszköz nem válaszol vagy hibát jelez, tájékoztatnia kell a felhasználót: "Elnézést, úgy tűnik, jelenleg nem érem el a rendszerünket. Kérem, próbáljon meg visszahívni pár perc múlva."
-
-11. KRITIKUS MUNKAFOLYAMAT: AZ INFORMÁCIÓ KULCSA
-
-Nem kereshet ügyfél- vagy rendelés-specifikus információt egy "kulcs" nélkül.
-Az Ön Kulcsai: ügyfél e-mail cím VAGY rendelésazonosító (orderid).
-
-Első Lépés: Ha egy felhasználó specifikus kérdést tesz fel (pl. "Hol van a rendelésem?", "Mi a rendelésem állapota?", "Megváltoztathatom a címemet?"), az Ön ELSŐ lépése kell, hogy legyen ezen kulcsok egyikének megszerzése.
-
-Kapu Szöveg: "Természetesen segíthetek ebben. Ahhoz, hogy lekérhessem az adatait, kérem, adja meg a rendelésazonosítóját, vagy az e-mail címet, amellyel a vásárlás történt."
-
-NE folytassa a rendelési/ügyféllel kapcsolatos lekérdezést, amíg nem rendelkezik ezen kulcsok egyikével.
-
-12. LEKÉRDEZÉSKEZELÉSI MUNKAFOLYAMATOK
-
-A. Rendeléssel Kapcsolatos Lekérdezések (Állapot, Részletek, Követés)
-Felhasználó: "Hol van a csomagom?" / "Mi a státusza a 12345-ös rendelésemnek?"
-Agent: (Ha nincs kulcs) "Szívesen ellenőrzöm. Mi a rendelésazonosítója vagy az e-mail címe?"
-Agent: (Miután megkapta a kulcsot) "Köszönöm. Egy pillanat, amíg megkeresem ezt a [rendelést/e-mail címet]."
-
-MŰVELET: Hívja meg a shoprenter_get_order az order_id vagy email használatával (és a titkos shopuuid-val). Az összes order_id -t amit a felhasználótól kapsz, alakítsd át szövegről számmá a rendelések lekérésekor.
-
-Válasz (Siker): "Köszönöm, hogy várt. Látom a rendelését, [Rendelési ID], amelyet [Dátum]-kor adott le. A jelenlegi állapota [Státusz az Eszközből, pl. 'Feldolgozás alatt' / 'Kiszállítva']. A rendelés tételei a következők: [Tétel 1, Tétel 2]."
-Válasz (Nem található): "Elnézést, de nem találtam rendelést ezzel a [ID-vel/e-mail címmel]. Kérem, ellenőrizze még egyszer."
-Válasz (Nem egyértelmű): Ha egy e-mail címhez több rendelés tartozik, sorolja fel őket dátum/ID szerint, és kérdezze meg, melyikre gondol. "Több friss rendelést látok ehhez az e-mail címhez. A [Dátum 1]-i [ID 1] számú rendelésről, vagy a [Dátum 2]-i [ID 2] számú rendelésről van szó?"
-
-B. Termékkel Kapcsolatos Lekérdezések (Információ, Készlet)
-Megjegyzés: Ez az egyetlen lekérdezés típus, amelyet orderid vagy email nélkül is végrehajthat.
-Felhasználó: "Árulnak [Termék Neve] terméket?" / "Készleten van a [Termék SKU]?"
-Agent: "Hadd ellenőrizzem ezt Önnek."
-
-MŰVELET: Hívja meg a megfelelő-t a termék nevével, leírásával vagy SKU-jával (ez RAG-ot használ, és a titkos shopuuid-t).
-
-Válasz (Siker): "Igen, látom a [Termék Neve] terméket. Az ára [Ár] és jelenleg [Készlet Állapot, pl. 'Készleten' / 'Nincs készleten']. Mondhatok még róla valamit?"
-Válasz (Nem található): "Elnézést, de nem találok olyan terméket a rendszerünkben, amely megfelelne ennek a leírásnak."
-
-C. Ügyféladatokkal Kapcsolatos Lekérdezések (Cím, Elérhetőség)
-Felhasználó: "Milyen címem van Önöknél?" / "Frissítenem kell a telefonszámomat."
-Agent: "Segíthetek ebben. A fiókjához való hozzáféréshez, kérem, adja meg az e-mail címét."
-
-MŰVELET: Hívja meg a megfelelő tool-t az email használatával (és a titkos shopuuid-val).
-
-Válasz (Olvasás): "A [Email] címhez tartozó fiókban az elsődleges szállítási cím, amit látok: [Cím az Eszközből]."
-Válasz (Frissítés): "A [telefonszám/cím] frissítéséhez először ellenőriznem kell a fiókját. Kérem, erősítse meg a nálam lévő számlázási címet." (Miután ellenőrizte, kísérelje meg a frissítést az eszközön keresztül, és jelentse a sikert/sikertelenséget).
-
-NAGYON FONTOS: mielőtt bármilyen személyes adatot elmond a tool használata előtt, azonosítsa be a felhasználót. Kérdezzen tőle olyan szeélyes információt amit már tud. Pl. hogyha rendelés felől érdeklődik és csak rendelés azonosítót mond meg, akkor kérdezze meg a nevét amely a rendelésben van.
-
-13. TARTALÉK MEGOLDÁSOK ÉS KORLÁTOZÁSOK
-
-Dühös Ügyfél: Maradjon nyugodt, empatikus és professzionális. Ne vegye védelmébe magát. Ismerje el a frusztrációját, és vezesse vissza a megoldáshoz. "Megértem, hogy ez frusztráló. Ahhoz, hogy segíthessek megoldani, kezdjük azzal, hogy megkeressük a rendelését. Meg tudná adni a rendelésazonosítóját?"
-
-Homályos Lekérdezés: Ha az ügyfél azt mondja: "Problémám van", irányítsa egy specifikus munkafolyamat felé. "Sajnálattal hallom. Mindent megteszek, hogy segítsek. A problémája egy meglévő rendeléssel, egy termékkel az oldalunkon, vagy a fiókadataival kapcsolatos?"
+// Language to transcription locale mapping
+const TRANSCRIBER_LOCALES: Record<string, string> = {
+  hu: 'hu-HU',
+  en: 'en-US',
+  de: 'de-DE'
+}
 
 
-Hatókörön Kívüli Kérés: Ha az ügyfél olyat kér, amit nem tud teljesíteni (pl. "Milyen az időjárás?", "Mondj egy viccet!", "Pénzügyi tanácsra van szükségem"), udvariasan utasítsa el és terelje vissza. "Elnézést, de csak webáruházzal kapcsolatos kérdésekben tudok segíteni, mint például rendelés állapota vagy termékinformációk."
+// Language to end call message mapping
+const END_CALL_MESSAGES: Record<string, string> = {
+  hu: 'Viszonthallásra!',
+  en: 'Goodbye!',
+  de: 'Auf Wiederhören!'
+}
 
 
-Eszkaláció: Ha az ügyfél emberrel akar beszélni, ne vitatkozzon. "Megértem. Kérem, tartsa a vonalat, kapcsolom egy emberi ügyintézőt." (Ez feltételezi, hogy van eszkalációs út). Ha nincs ilyen út: "Elnézést, én vagyok az elsődlegesen elérhető támogatói asszisztens, de mindent megteszek a probléma megoldása érdekében. Kérem, magyarázza el újra a problémát."`
+// Language to voicemail message mapping
+const VOICEMAIL_MESSAGES: Record<string, string> = {
+  hu: 'Kérem, hívjon vissza, amikor elérhető.',
+  en: 'Please call back when you\'re available.',
+  de: 'Bitte rufen Sie zurück, wenn Sie verfügbar sind.'
 }
 }
 
 
 /**
 /**
@@ -303,6 +171,7 @@ Eszkaláció: Ha az ügyfél emberrel akar beszélni, ne vitatkozzon. "Megértem
 function buildAssistantConfig(config: VapiAssistantConfig): Record<string, unknown> {
 function buildAssistantConfig(config: VapiAssistantConfig): Record<string, unknown> {
   const supabaseUrl = Deno.env.get('SUPABASE_URL') || 'https://api.shopcall.ai'
   const supabaseUrl = Deno.env.get('SUPABASE_URL') || 'https://api.shopcall.ai'
   const webhookAuthToken = getVapiWebhookAuthToken()
   const webhookAuthToken = getVapiWebhookAuthToken()
+  const langCode = config.languageCode || 'hu'
 
 
   return {
   return {
     name: `SC_${config.storeId}`, // max 40 chars -> stores.id = 37char (UUID)
     name: `SC_${config.storeId}`, // max 40 chars -> stores.id = 37char (UUID)
@@ -322,7 +191,7 @@ function buildAssistantConfig(config: VapiAssistantConfig): Record<string, unkno
       messages: [
       messages: [
         {
         {
           role: 'system',
           role: 'system',
-          content: buildSystemPrompt(config.storeName, config.storeId)
+          content: config.systemPrompt
         }
         }
       ],
       ],
       provider: 'openai',
       provider: 'openai',
@@ -330,11 +199,11 @@ function buildAssistantConfig(config: VapiAssistantConfig): Record<string, unkno
     },
     },
     forwardingPhoneNumber: '+36305547382',
     forwardingPhoneNumber: '+36305547382',
     firstMessage: config.greetingMessage,
     firstMessage: config.greetingMessage,
-    voicemailMessage: 'Please call back when you\'re available.',
+    voicemailMessage: VOICEMAIL_MESSAGES[langCode] || VOICEMAIL_MESSAGES['en'],
     endCallFunctionEnabled: true,
     endCallFunctionEnabled: true,
-    endCallMessage: 'Viszonthallásra!',
+    endCallMessage: END_CALL_MESSAGES[langCode] || END_CALL_MESSAGES['en'],
     transcriber: {
     transcriber: {
-      language: 'hu-HU',
+      language: TRANSCRIBER_LOCALES[langCode] || 'hu-HU',
       provider: 'azure'
       provider: 'azure'
     },
     },
     serverMessages: [
     serverMessages: [
@@ -388,15 +257,21 @@ export async function createAssistant(
 
 
 /**
 /**
  * Update an existing VAPI assistant
  * Update an existing VAPI assistant
+ *
+ * @param assistantId - The VAPI assistant ID to update
+ * @param voiceId - New voice ID
+ * @param greetingMessage - New greeting message
+ * @param systemPrompt - Optional new system prompt (if not provided, system prompt is not updated)
  */
  */
 export async function updateAssistant(
 export async function updateAssistant(
   assistantId: string,
   assistantId: string,
   voiceId: string,
   voiceId: string,
-  greetingMessage: string
+  greetingMessage: string,
+  systemPrompt?: string
 ): Promise<VapiAssistantResponse> {
 ): Promise<VapiAssistantResponse> {
-  console.log(`[VAPI] Updating assistant: ${assistantId}`)
+  console.log(`[VAPI] Updating assistant: ${assistantId}${systemPrompt ? ' (including system prompt)' : ''}`)
 
 
-  const result = await vapiRequest('PATCH', `/assistant/${assistantId}`, {
+  const updatePayload: Record<string, unknown> = {
     voice: {
     voice: {
       provider: '11labs',
       provider: '11labs',
       model: 'eleven_turbo_v2_5',
       model: 'eleven_turbo_v2_5',
@@ -405,7 +280,28 @@ export async function updateAssistant(
       similarityBoost: 0.75
       similarityBoost: 0.75
     },
     },
     firstMessage: greetingMessage
     firstMessage: greetingMessage
-  })
+  }
+
+  // Only update system prompt if provided
+  if (systemPrompt) {
+    updatePayload.model = {
+      model: 'gpt-4.1',
+      toolIds: [
+        '82c2159c-05b0-44a0-af92-0c83ff3dd1ae',
+        'bda2f41b-ec69-441d-8d16-a349783370d4'
+      ],
+      messages: [
+        {
+          role: 'system',
+          content: systemPrompt
+        }
+      ],
+      provider: 'openai',
+      temperature: 0.4
+    }
+  }
+
+  const result = await vapiRequest('PATCH', `/assistant/${assistantId}`, updatePayload)
 
 
   if (!result.success) {
   if (!result.success) {
     return { success: false, error: result.error }
     return { success: false, error: result.error }
@@ -415,6 +311,33 @@ export async function updateAssistant(
   return { success: true, assistantId }
   return { success: true, assistantId }
 }
 }
 
 
+/**
+ * Assign an assistant to a phone number
+ *
+ * After creating an assistant, this function links it to the registered phone number
+ * so that incoming calls to that number are handled by the assistant.
+ *
+ * @param phoneNumberId - The VAPI phone number ID
+ * @param assistantId - The VAPI assistant ID to assign
+ */
+export async function assignAssistantToPhoneNumber(
+  phoneNumberId: string,
+  assistantId: string
+): Promise<{ success: boolean; error?: string }> {
+  console.log(`[VAPI] Assigning assistant ${assistantId} to phone number ${phoneNumberId}`)
+
+  const result = await vapiRequest('PATCH', `/phone-number/${phoneNumberId}`, {
+    assistantId: assistantId
+  })
+
+  if (!result.success) {
+    return { success: false, error: result.error }
+  }
+
+  console.log(`[VAPI] Assistant assigned to phone number successfully`)
+  return { success: true }
+}
+
 /**
 /**
  * Check if VAPI integration is available (API key configured)
  * Check if VAPI integration is available (API key configured)
  */
  */

+ 223 - 33
supabase/functions/_shared/vapi-setup.ts

@@ -13,6 +13,12 @@ import {
   isVapiConfigured,
   isVapiConfigured,
   VapiAssistantConfig
   VapiAssistantConfig
 } from './vapi-client.ts'
 } from './vapi-client.ts'
+import {
+  loadSystemPrompt,
+  loadStoreBusinessHours,
+  loadStoreSpecialHours,
+  buildCompleteSystemPrompt
+} from './prompt-utils.ts'
 
 
 export interface VapiSetupResult {
 export interface VapiSetupResult {
   success: boolean
   success: boolean
@@ -27,9 +33,27 @@ export interface VapiUpdateResult {
 }
 }
 
 
 /**
 /**
- * Default greeting message template
+ * Default language code used when integrating webshops
+ */
+const DEFAULT_LANGUAGE_CODE = 'hu'
+
+/**
+ * Load default greeting message from database
+ * Falls back to hardcoded message if not found
  */
  */
-function getDefaultGreetingMessage(storeName: string): string {
+async function loadDefaultGreetingMessage(
+  supabase: SupabaseClient,
+  storeName: string,
+  languageCode: string = DEFAULT_LANGUAGE_CODE
+): Promise<string> {
+  const prompts = await loadSystemPrompt(supabase, languageCode)
+
+  if (prompts?.greetingMessage) {
+    // Replace ${storeName} placeholder in greeting
+    return prompts.greetingMessage.replace(/\$\{storeName\}/g, storeName)
+  }
+
+  // Fallback to hardcoded message
   return `Üdvözlöm! A ${storeName} ügyfélszolgálata vagyok. Miben segíthetek?`
   return `Üdvözlöm! A ${storeName} ügyfélszolgálata vagyok. Miben segíthetek?`
 }
 }
 
 
@@ -68,8 +92,8 @@ async function pickRandomVoice(
  * This function:
  * This function:
  * 1. Picks a random enabled voice from the database (or uses existing ai_config)
  * 1. Picks a random enabled voice from the database (or uses existing ai_config)
  * 2. Creates a default greeting message (or uses existing ai_config)
  * 2. Creates a default greeting message (or uses existing ai_config)
- * 3. Registers the phone number with VAPI
- * 4. Creates a VAPI assistant
+ * 3. Creates a VAPI assistant
+ * 4. Registers the phone number with VAPI (with assistantId to link them)
  * 5. Updates the store's alt_data with VAPI IDs and ai_config
  * 5. Updates the store's alt_data with VAPI IDs and ai_config
  *
  *
  * @param supabase - Supabase client (with service role for admin operations)
  * @param supabase - Supabase client (with service role for admin operations)
@@ -92,7 +116,7 @@ export async function setupVapiForStore(
   }
   }
 
 
   try {
   try {
-    // First, check if store already has ai_config with voice_type
+    // First, check if store already has ai_config
     const { data: storeData } = await supabase
     const { data: storeData } = await supabase
       .from('stores')
       .from('stores')
       .select('alt_data')
       .select('alt_data')
@@ -102,14 +126,36 @@ export async function setupVapiForStore(
     const existingAiConfig = storeData?.alt_data?.ai_config
     const existingAiConfig = storeData?.alt_data?.ai_config
     let voiceId: string
     let voiceId: string
     let greetingMessage: string
     let greetingMessage: string
+    const languageCode = existingAiConfig?.language_code || DEFAULT_LANGUAGE_CODE
+
+    // Step 1: Load base system prompt from database
+    console.log(`[VAPI Setup] Loading system prompt for language: ${languageCode}`)
+    const promptData = await loadSystemPrompt(supabase, languageCode)
+    if (!promptData?.systemPrompt) {
+      const error = `Failed to load system prompt for language: ${languageCode}`
+      console.error(`[VAPI Setup] ${error}`)
+      await updateStoreWithError(supabase, storeId, error)
+      return { success: false, error }
+    }
+
+    // Step 2: Use existing greeting from DB if available, otherwise use default from database
+    if (existingAiConfig?.greeting_message) {
+      greetingMessage = existingAiConfig.greeting_message
+      console.log(`[VAPI Setup] Using existing greeting from store config: ${storeId}`)
+    } else if (promptData.greetingMessage) {
+      // Replace ${storeName} placeholder in greeting from DB
+      greetingMessage = promptData.greetingMessage.replace(/\$\{storeName\}/g, storeName)
+      console.log(`[VAPI Setup] Using default greeting from database for store: ${storeId}`)
+    } else {
+      greetingMessage = `Üdvözlöm! A ${storeName} ügyfélszolgálata vagyok. Miben segíthetek?`
+      console.log(`[VAPI Setup] Using hardcoded fallback greeting for store: ${storeId}`)
+    }
 
 
+    // Step 3: Use existing voice if available, otherwise pick random
     if (existingAiConfig?.voice_type) {
     if (existingAiConfig?.voice_type) {
-      // Use existing ai_config
-      console.log(`[VAPI Setup] Using existing ai_config for store: ${storeId}`)
       voiceId = existingAiConfig.voice_type
       voiceId = existingAiConfig.voice_type
-      greetingMessage = existingAiConfig.greeting_message || getDefaultGreetingMessage(storeName)
+      console.log(`[VAPI Setup] Using existing voice from DB: ${voiceId}`)
     } else {
     } else {
-      // Step 1: Pick a random voice
       const voiceResult = await pickRandomVoice(supabase)
       const voiceResult = await pickRandomVoice(supabase)
       if (!voiceResult) {
       if (!voiceResult) {
         const error = 'Failed to pick a voice from database'
         const error = 'Failed to pick a voice from database'
@@ -117,53 +163,71 @@ export async function setupVapiForStore(
         return { success: false, error }
         return { success: false, error }
       }
       }
       voiceId = voiceResult.voiceId
       voiceId = voiceResult.voiceId
-
-      // Step 2: Create default greeting
-      greetingMessage = getDefaultGreetingMessage(storeName)
+      console.log(`[VAPI Setup] Picked random voice: ${voiceId}`)
     }
     }
 
 
-    // Step 3: Register phone number with VAPI
-    console.log(`[VAPI Setup] Registering phone number: ${phoneNumber}`)
-    const phoneResult = await registerPhoneNumber(phoneNumber, storeName)
+    // Step 4: Load business hours and special hours for opening hours section
+    console.log(`[VAPI Setup] Loading business hours for store: ${storeId}`)
+    const { businessHours, isEnabled: hoursEnabled } = await loadStoreBusinessHours(supabase, storeId)
+    const specialHours = await loadStoreSpecialHours(supabase, storeId)
+    const transferPhoneNumber = existingAiConfig?.transfer_phone_number || null
 
 
-    if (!phoneResult.success) {
-      const error = `Failed to register phone number: ${phoneResult.error}`
-      await updateStoreWithError(supabase, storeId, error)
-      return { success: false, error }
-    }
+    // Step 5: Build complete system prompt with opening hours
+    // When business hours are disabled, also hide special hours and transfer phone
+    const systemPrompt = buildCompleteSystemPrompt(
+      promptData.systemPrompt,
+      storeName,
+      storeId,
+      hoursEnabled ? businessHours : null,
+      hoursEnabled ? specialHours : null,  // Hide special hours when business hours disabled
+      languageCode,
+      hoursEnabled ? transferPhoneNumber : null  // Hide transfer phone when business hours disabled
+    )
+    console.log(`[VAPI Setup] Built system prompt (${systemPrompt.length} chars), hours enabled: ${hoursEnabled}`)
 
 
-    // Step 4: Create VAPI assistant
+    // Step 6: Create VAPI assistant first (we need the ID for phone number registration)
     console.log(`[VAPI Setup] Creating assistant for store: ${storeName}`)
     console.log(`[VAPI Setup] Creating assistant for store: ${storeName}`)
     const assistantConfig: VapiAssistantConfig = {
     const assistantConfig: VapiAssistantConfig = {
       storeName,
       storeName,
       storeId,
       storeId,
       voiceId,
       voiceId,
       greetingMessage,
       greetingMessage,
-      phoneNumberId: phoneResult.phoneNumberId
+      systemPrompt,
+      languageCode
     }
     }
 
 
     const assistantResult = await createAssistant(assistantConfig)
     const assistantResult = await createAssistant(assistantConfig)
 
 
     if (!assistantResult.success) {
     if (!assistantResult.success) {
       const error = `Failed to create assistant: ${assistantResult.error}`
       const error = `Failed to create assistant: ${assistantResult.error}`
-      // Still store the phone number ID even if assistant creation fails
+      await updateStoreWithError(supabase, storeId, error)
+      return { success: false, error }
+    }
+
+    // Step 7: Register phone number with VAPI (with assistantId to link them)
+    console.log(`[VAPI Setup] Registering phone number: ${phoneNumber} with assistant: ${assistantResult.assistantId}`)
+    const phoneResult = await registerPhoneNumber(phoneNumber, storeName, assistantResult.assistantId)
+
+    if (!phoneResult.success) {
+      const error = `Failed to register phone number: ${phoneResult.error}`
+      // Still store the assistant ID even if phone registration fails
       await updateStoreWithPartialSuccess(
       await updateStoreWithPartialSuccess(
         supabase,
         supabase,
         storeId,
         storeId,
-        phoneResult.phoneNumberId!,
         null,
         null,
+        assistantResult.assistantId!,
         voiceId,
         voiceId,
         greetingMessage,
         greetingMessage,
         error
         error
       )
       )
       return {
       return {
         success: false,
         success: false,
-        vapiPhoneNumberId: phoneResult.phoneNumberId,
+        vapiAssistantId: assistantResult.assistantId,
         error
         error
       }
       }
     }
     }
 
 
-    // Step 5: Update store with VAPI IDs and ai_config
+    // Step 8: Update store with VAPI IDs and ai_config
     console.log(`[VAPI Setup] Updating store with VAPI IDs`)
     console.log(`[VAPI Setup] Updating store with VAPI IDs`)
     await updateStoreWithSuccess(
     await updateStoreWithSuccess(
       supabase,
       supabase,
@@ -236,12 +300,12 @@ async function updateStoreWithSuccess(
 }
 }
 
 
 /**
 /**
- * Update store alt_data with partial success (phone registered but assistant failed)
+ * Update store alt_data with partial success (one of phone/assistant failed)
  */
  */
 async function updateStoreWithPartialSuccess(
 async function updateStoreWithPartialSuccess(
   supabase: SupabaseClient,
   supabase: SupabaseClient,
   storeId: string,
   storeId: string,
-  vapiPhoneNumberId: string,
+  vapiPhoneNumberId: string | null,
   vapiAssistantId: string | null,
   vapiAssistantId: string | null,
   voiceId: string,
   voiceId: string,
   greetingMessage: string,
   greetingMessage: string,
@@ -328,14 +392,16 @@ async function updateStoreWithError(
  * @param storeId - The store UUID
  * @param storeId - The store UUID
  * @param voiceId - New voice ID (provider_voice_id)
  * @param voiceId - New voice ID (provider_voice_id)
  * @param greetingMessage - New greeting message
  * @param greetingMessage - New greeting message
+ * @param updateSystemPrompt - Whether to also rebuild and update the system prompt (default: false)
  */
  */
 export async function updateVapiAssistant(
 export async function updateVapiAssistant(
   supabase: SupabaseClient,
   supabase: SupabaseClient,
   storeId: string,
   storeId: string,
   voiceId: string,
   voiceId: string,
-  greetingMessage: string
+  greetingMessage: string,
+  updateSystemPrompt: boolean = false
 ): Promise<VapiUpdateResult> {
 ): Promise<VapiUpdateResult> {
-  console.log(`[VAPI Update] Updating assistant for store: ${storeId}`)
+  console.log(`[VAPI Update] Updating assistant for store: ${storeId}${updateSystemPrompt ? ' (including system prompt)' : ''}`)
 
 
   // Check if VAPI is configured
   // Check if VAPI is configured
   if (!isVapiConfigured()) {
   if (!isVapiConfigured()) {
@@ -344,10 +410,10 @@ export async function updateVapiAssistant(
   }
   }
 
 
   try {
   try {
-    // Fetch the store to get vapi_assistant_id
+    // Fetch the store to get vapi_assistant_id and store_name
     const { data: store, error: fetchError } = await supabase
     const { data: store, error: fetchError } = await supabase
       .from('stores')
       .from('stores')
-      .select('alt_data')
+      .select('store_name, alt_data')
       .eq('id', storeId)
       .eq('id', storeId)
       .single()
       .single()
 
 
@@ -358,14 +424,47 @@ export async function updateVapiAssistant(
     }
     }
 
 
     const vapiAssistantId = store.alt_data?.vapi_assistant_id
     const vapiAssistantId = store.alt_data?.vapi_assistant_id
+    const storeName = store.store_name || 'Unknown Store'
+    const languageCode = store.alt_data?.ai_config?.language_code || DEFAULT_LANGUAGE_CODE
 
 
     if (!vapiAssistantId) {
     if (!vapiAssistantId) {
       console.log('[VAPI Update] No VAPI assistant ID found for store, skipping update')
       console.log('[VAPI Update] No VAPI assistant ID found for store, skipping update')
       return { success: false, error: 'No VAPI assistant configured for this store' }
       return { success: false, error: 'No VAPI assistant configured for this store' }
     }
     }
 
 
+    let systemPrompt: string | undefined
+
+    // Rebuild system prompt if requested
+    if (updateSystemPrompt) {
+      console.log(`[VAPI Update] Rebuilding system prompt for language: ${languageCode}`)
+
+      const promptData = await loadSystemPrompt(supabase, languageCode)
+      if (!promptData?.systemPrompt) {
+        console.error(`[VAPI Update] Failed to load system prompt for language: ${languageCode}`)
+        return { success: false, error: 'Failed to load system prompt' }
+      }
+
+      // Load business hours and special hours
+      const { businessHours, isEnabled: hoursEnabled } = await loadStoreBusinessHours(supabase, storeId)
+      const specialHours = await loadStoreSpecialHours(supabase, storeId)
+      const transferPhoneNumber = store.alt_data?.ai_config?.transfer_phone_number || null
+
+      // Build complete system prompt with opening hours
+      // When business hours are disabled, also hide special hours and transfer phone
+      systemPrompt = buildCompleteSystemPrompt(
+        promptData.systemPrompt,
+        storeName,
+        storeId,
+        hoursEnabled ? businessHours : null,
+        hoursEnabled ? specialHours : null,  // Hide special hours when business hours disabled
+        languageCode,
+        hoursEnabled ? transferPhoneNumber : null  // Hide transfer phone when business hours disabled
+      )
+      console.log(`[VAPI Update] Built system prompt (${systemPrompt.length} chars), hours enabled: ${hoursEnabled}`)
+    }
+
     // Update the assistant in VAPI
     // Update the assistant in VAPI
-    const result = await updateAssistant(vapiAssistantId, voiceId, greetingMessage)
+    const result = await updateAssistant(vapiAssistantId, voiceId, greetingMessage, systemPrompt)
 
 
     if (!result.success) {
     if (!result.success) {
       console.error('[VAPI Update] Failed to update assistant:', result.error)
       console.error('[VAPI Update] Failed to update assistant:', result.error)
@@ -380,3 +479,94 @@ export async function updateVapiAssistant(
     return { success: false, error: errorMessage }
     return { success: false, error: errorMessage }
   }
   }
 }
 }
+
+/**
+ * Update VAPI assistant's system prompt only
+ * Used when business hours or special hours change
+ *
+ * @param supabase - Supabase client
+ * @param storeId - The store UUID
+ */
+export async function updateVapiSystemPrompt(
+  supabase: SupabaseClient,
+  storeId: string
+): Promise<VapiUpdateResult> {
+  console.log(`[VAPI Update] Updating system prompt only for store: ${storeId}`)
+
+  // Check if VAPI is configured
+  if (!isVapiConfigured()) {
+    console.log('[VAPI Update] VAPI_API_KEY not configured, skipping update')
+    return { success: false, error: 'VAPI_API_KEY not configured' }
+  }
+
+  try {
+    // Fetch the store to get all needed data
+    const { data: store, error: fetchError } = await supabase
+      .from('stores')
+      .select('store_name, alt_data')
+      .eq('id', storeId)
+      .single()
+
+    if (fetchError || !store) {
+      const error = 'Store not found'
+      console.error('[VAPI Update] Error fetching store:', fetchError)
+      return { success: false, error }
+    }
+
+    const vapiAssistantId = store.alt_data?.vapi_assistant_id
+    const storeName = store.store_name || 'Unknown Store'
+    const languageCode = store.alt_data?.ai_config?.language_code || DEFAULT_LANGUAGE_CODE
+    const voiceId = store.alt_data?.ai_config?.voice_type
+    const greetingMessage = store.alt_data?.ai_config?.greeting_message
+
+    if (!vapiAssistantId) {
+      console.log('[VAPI Update] No VAPI assistant ID found for store, skipping update')
+      return { success: false, error: 'No VAPI assistant configured for this store' }
+    }
+
+    if (!voiceId || !greetingMessage) {
+      console.log('[VAPI Update] Missing voice or greeting config, skipping update')
+      return { success: false, error: 'Missing AI configuration for this store' }
+    }
+
+    // Load system prompt
+    const promptData = await loadSystemPrompt(supabase, languageCode)
+    if (!promptData?.systemPrompt) {
+      console.error(`[VAPI Update] Failed to load system prompt for language: ${languageCode}`)
+      return { success: false, error: 'Failed to load system prompt' }
+    }
+
+    // Load business hours and special hours
+    const { businessHours, isEnabled: hoursEnabled } = await loadStoreBusinessHours(supabase, storeId)
+    const specialHours = await loadStoreSpecialHours(supabase, storeId)
+    const transferPhoneNumber = store.alt_data?.ai_config?.transfer_phone_number || null
+
+    // Build complete system prompt with opening hours
+    // When business hours are disabled, also hide special hours and transfer phone
+    const systemPrompt = buildCompleteSystemPrompt(
+      promptData.systemPrompt,
+      storeName,
+      storeId,
+      hoursEnabled ? businessHours : null,
+      hoursEnabled ? specialHours : null,  // Hide special hours when business hours disabled
+      languageCode,
+      hoursEnabled ? transferPhoneNumber : null  // Hide transfer phone when business hours disabled
+    )
+    console.log(`[VAPI Update] Built system prompt (${systemPrompt.length} chars), hours enabled: ${hoursEnabled}, special hours: ${hoursEnabled ? specialHours.length : 0}`)
+
+    // Update the assistant in VAPI
+    const result = await updateAssistant(vapiAssistantId, voiceId, greetingMessage, systemPrompt)
+
+    if (!result.success) {
+      console.error('[VAPI Update] Failed to update assistant:', result.error)
+      return { success: false, error: result.error }
+    }
+
+    console.log(`[VAPI Update] Successfully updated system prompt for assistant: ${vapiAssistantId}`)
+    return { success: true }
+  } catch (error) {
+    const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+    console.error('[VAPI Update] Update failed:', error)
+    return { success: false, error: errorMessage }
+  }
+}

+ 14 - 4
supabase/functions/api/index.ts

@@ -519,7 +519,7 @@ serve(async (req) => {
     // PUT /api/stores/:id/ai-config - Update AI configuration for a store
     // PUT /api/stores/:id/ai-config - Update AI configuration for a store
     if (path.match(/^stores\/[^\/]+\/ai-config$/) && req.method === 'PUT') {
     if (path.match(/^stores\/[^\/]+\/ai-config$/) && req.method === 'PUT') {
       const storeId = path.split('/')[1]
       const storeId = path.split('/')[1]
-      const { ai_config } = await req.json()
+      const { ai_config, update_system_prompt } = await req.json()
 
 
       if (!ai_config) {
       if (!ai_config) {
         return new Response(
         return new Response(
@@ -564,19 +564,29 @@ serve(async (req) => {
       }
       }
 
 
       // Update VAPI assistant (async, non-blocking)
       // Update VAPI assistant (async, non-blocking)
-      if (ai_config.voice_type && ai_config.greeting_message) {
+      // Use voice from request, or fall back to existing voice in DB
+      const voiceToUse = ai_config.voice_type || store.alt_data?.ai_config?.voice_type
+      const greetingToUse = ai_config.greeting_message || store.alt_data?.ai_config?.greeting_message
+
+      if (voiceToUse && greetingToUse) {
         const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
         const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
         const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
         const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
 
 
-        updateVapiAssistant(supabaseAdmin, storeId, ai_config.voice_type, ai_config.greeting_message)
+        // If update_system_prompt is true, rebuild and update the entire system prompt including work hours
+        const shouldUpdateSystemPrompt = update_system_prompt === true
+        console.log(`[API] Updating VAPI assistant for store ${storeId}, updateSystemPrompt: ${shouldUpdateSystemPrompt}`)
+
+        updateVapiAssistant(supabaseAdmin, storeId, voiceToUse, greetingToUse, shouldUpdateSystemPrompt)
           .then(result => {
           .then(result => {
             if (result.success) {
             if (result.success) {
-              console.log(`[API] VAPI assistant updated for store ${storeId}`)
+              console.log(`[API] VAPI assistant updated for store ${storeId}${shouldUpdateSystemPrompt ? ' (including system prompt with work hours)' : ''}`)
             } else {
             } else {
               console.error(`[API] VAPI assistant update failed for store ${storeId}:`, result.error)
               console.error(`[API] VAPI assistant update failed for store ${storeId}:`, result.error)
             }
             }
           })
           })
           .catch(err => console.error(`[API] VAPI update error for store ${storeId}:`, err))
           .catch(err => console.error(`[API] VAPI update error for store ${storeId}:`, err))
+      } else {
+        console.log(`[API] Skipping VAPI update - missing voice (${voiceToUse}) or greeting (${greetingToUse})`)
       }
       }
 
 
       return new Response(
       return new Response(

+ 30 - 21
supabase/functions/auto-register-shoprenter/index.ts

@@ -414,35 +414,38 @@ serve(wrapHandler('auto-register-shoprenter', async (req) => {
 
 
     console.log(`[AutoRegister] Successfully completed auto-registration for ${email}`)
     console.log(`[AutoRegister] Successfully completed auto-registration for ${email}`)
 
 
-    // Register store with scraper service in background
+    // Setup VAPI assistant FIRST (must await to ensure vapi_assistant_id is stored)
+    let vapiSetupResult = null
+    if (finalPhoneNumberId) {
+      try {
+        const { data: phoneData } = await supabase
+          .from('phone_numbers')
+          .select('phone_number')
+          .eq('id', finalPhoneNumberId)
+          .single()
+
+        if (phoneData?.phone_number) {
+          // Remove spaces for E.164 format
+          const phoneNumber = phoneData.phone_number.replace(/\s/g, '')
+          console.log(`[AutoRegister] Setting up VAPI for store ${storeId} with phone ${phoneNumber}`)
+          vapiSetupResult = await setupVapiForStore(supabase, storeId, pendingInstall.shopname, phoneNumber)
+          console.log(`[AutoRegister] VAPI setup ${vapiSetupResult.success ? 'complete' : 'failed'}: ${vapiSetupResult.error || ''}`)
+        }
+      } catch (err) {
+        console.error(`[AutoRegister] VAPI setup error:`, err)
+      }
+    }
+
+    // Register store with scraper service in background (non-critical)
     registerStoreWithScraper(storeId, `https://${pendingInstall.shopname}.myshoprenter.hu`, supabase)
     registerStoreWithScraper(storeId, `https://${pendingInstall.shopname}.myshoprenter.hu`, supabase)
       .then(() => console.log(`[AutoRegister] Scraper registration completed for store ${storeId}`))
       .then(() => console.log(`[AutoRegister] Scraper registration completed for store ${storeId}`))
       .catch(err => console.error(`[AutoRegister] Scraper registration failed:`, err))
       .catch(err => console.error(`[AutoRegister] Scraper registration failed:`, err))
 
 
-    // Trigger initial data sync in background
+    // Trigger initial data sync in background (non-critical)
     triggerInitialSync(storeId, supabaseUrl, supabaseServiceKey)
     triggerInitialSync(storeId, supabaseUrl, supabaseServiceKey)
       .then(() => console.log(`[AutoRegister] Initial sync completed for store ${storeId}`))
       .then(() => console.log(`[AutoRegister] Initial sync completed for store ${storeId}`))
       .catch(err => console.error(`[AutoRegister] Initial sync failed:`, err))
       .catch(err => console.error(`[AutoRegister] Initial sync failed:`, err))
 
 
-    // Setup VAPI assistant in background (async, non-blocking)
-    if (finalPhoneNumberId) {
-      supabase
-        .from('phone_numbers')
-        .select('phone_number')
-        .eq('id', finalPhoneNumberId)
-        .single()
-        .then(({ data: phoneData }: { data: { phone_number: string } | null }) => {
-          if (phoneData?.phone_number) {
-            // Remove spaces for E.164 format
-            const phoneNumber = phoneData.phone_number.replace(/\s/g, '')
-            setupVapiForStore(supabase, storeId, pendingInstall.shopname, phoneNumber)
-              .then(result => console.log(`[AutoRegister] VAPI setup ${result.success ? 'complete' : 'failed'}: ${result.error || ''}`))
-              .catch(err => console.error(`[AutoRegister] VAPI setup error:`, err))
-          }
-        })
-        .catch((err: Error) => console.error(`[AutoRegister] Failed to fetch phone for VAPI:`, err))
-    }
-
     return new Response(
     return new Response(
       JSON.stringify({
       JSON.stringify({
         success: true,
         success: true,
@@ -456,6 +459,12 @@ serve(wrapHandler('auto-register-shoprenter', async (req) => {
           id: storeId,
           id: storeId,
           shopname: pendingInstall.shopname
           shopname: pendingInstall.shopname
         },
         },
+        vapi: vapiSetupResult ? {
+          success: vapiSetupResult.success,
+          assistant_id: vapiSetupResult.vapiAssistantId,
+          phone_number_id: vapiSetupResult.vapiPhoneNumberId,
+          error: vapiSetupResult.error
+        } : null,
         message: 'Account created and store connected successfully. For future logins, please use password reset.'
         message: 'Account created and store connected successfully. For future logins, please use password reset.'
       }),
       }),
       { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
       { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }

+ 135 - 0
supabase/functions/cleanup-expired-special-hours/index.ts

@@ -0,0 +1,135 @@
+/**
+ * Cleanup Expired Special Hours
+ *
+ * This edge function:
+ * 1. Finds all stores that have special hours expiring today
+ * 2. Deletes the expired special hours from the database
+ * 3. Updates VAPI system prompts for affected stores (to remove expired hours)
+ *
+ * Called by pg_cron daily at midnight
+ */
+
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+import { updateVapiSystemPrompt } from '../_shared/vapi-setup.ts'
+
+serve(async (req) => {
+  // Verify internal call (from pg_cron or service role)
+  const authHeader = req.headers.get('authorization')
+  const internalSecret = Deno.env.get('INTERNAL_SYNC_SECRET')
+  const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
+
+  // Allow either internal secret or service role key
+  const isAuthorized =
+    authHeader === `Bearer ${internalSecret}` ||
+    authHeader === `Bearer ${supabaseServiceKey}`
+
+  if (!isAuthorized) {
+    console.error('[Cleanup] Unauthorized request')
+    return new Response(
+      JSON.stringify({ error: 'Unauthorized' }),
+      { status: 401, headers: { 'Content-Type': 'application/json' } }
+    )
+  }
+
+  const supabaseUrl = Deno.env.get('SUPABASE_URL')!
+  const supabase = createClient(supabaseUrl, supabaseServiceKey!)
+
+  console.log('[Cleanup] Starting expired special hours cleanup')
+
+  try {
+    const today = new Date().toISOString().split('T')[0]
+
+    // Step 1: Find all stores that have special hours expiring today or earlier
+    // We need to update VAPI for these stores BEFORE deleting the records
+    const { data: expiringHours, error: fetchError } = await supabase
+      .from('store_special_hours')
+      .select('store_id')
+      .lt('date', today)
+
+    if (fetchError) {
+      console.error('[Cleanup] Error fetching expiring special hours:', fetchError)
+      return new Response(
+        JSON.stringify({ error: 'Failed to fetch expiring special hours' }),
+        { status: 500, headers: { 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // Get unique store IDs
+    const affectedStoreIds = [...new Set((expiringHours || []).map(h => h.store_id))]
+    console.log(`[Cleanup] Found ${affectedStoreIds.length} stores with expiring special hours`)
+
+    // Step 2: Delete expired special hours
+    const { error: deleteError, count: deletedCount } = await supabase
+      .from('store_special_hours')
+      .delete({ count: 'exact' })
+      .lt('date', today)
+
+    if (deleteError) {
+      console.error('[Cleanup] Error deleting expired special hours:', deleteError)
+      return new Response(
+        JSON.stringify({ error: 'Failed to delete expired special hours' }),
+        { status: 500, headers: { 'Content-Type': 'application/json' } }
+      )
+    }
+
+    console.log(`[Cleanup] Deleted ${deletedCount} expired special hours records`)
+
+    // Step 3: Update VAPI system prompts for affected stores
+    const vapiResults: Array<{ storeId: string; success: boolean; error?: string }> = []
+
+    for (const storeId of affectedStoreIds) {
+      try {
+        console.log(`[Cleanup] Updating VAPI system prompt for store: ${storeId}`)
+        const result = await updateVapiSystemPrompt(supabase, storeId)
+
+        vapiResults.push({
+          storeId,
+          success: result.success,
+          error: result.error
+        })
+
+        if (result.success) {
+          console.log(`[Cleanup] Successfully updated VAPI for store: ${storeId}`)
+        } else {
+          console.error(`[Cleanup] Failed to update VAPI for store ${storeId}: ${result.error}`)
+        }
+      } catch (error) {
+        const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+        console.error(`[Cleanup] Error updating VAPI for store ${storeId}:`, error)
+        vapiResults.push({
+          storeId,
+          success: false,
+          error: errorMessage
+        })
+      }
+    }
+
+    const successCount = vapiResults.filter(r => r.success).length
+    const failCount = vapiResults.filter(r => !r.success).length
+
+    console.log(`[Cleanup] VAPI updates: ${successCount} succeeded, ${failCount} failed`)
+
+    return new Response(
+      JSON.stringify({
+        success: true,
+        message: 'Cleanup completed',
+        deletedRecords: deletedCount,
+        affectedStores: affectedStoreIds.length,
+        vapiUpdates: {
+          succeeded: successCount,
+          failed: failCount,
+          details: vapiResults
+        }
+      }),
+      { status: 200, headers: { 'Content-Type': 'application/json' } }
+    )
+  } catch (error) {
+    const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+    console.error('[Cleanup] Unexpected error:', error)
+    return new Response(
+      JSON.stringify({ error: errorMessage }),
+      { status: 500, headers: { 'Content-Type': 'application/json' } }
+    )
+  }
+})

+ 1 - 1
supabase/functions/vapi-webhook/index.ts

@@ -182,7 +182,7 @@ serve(async (req) => {
       duration: message.durationSeconds || null,
       duration: message.durationSeconds || null,
       costs: message.costs || null,
       costs: message.costs || null,
       cost_total: message.call?.cost || message.cost || null,
       cost_total: message.call?.cost || message.cost || null,
-      caller: caller || null,
+      caller: message.customer?.number || caller || null,
     }
     }
 
 
     console.log('Inserting call log for store:', storeId)
     console.log('Inserting call log for store:', storeId)

+ 66 - 0
supabase/migrations/20251203_update_special_hours_cleanup_cron.sql

@@ -0,0 +1,66 @@
+-- Migration: Update special hours cleanup cron job
+-- Changes the cron job from simple DELETE to calling an edge function
+-- The edge function will:
+-- 1. Delete expired special hours
+-- 2. Update VAPI system prompts for affected stores (removes expired hours from prompt)
+
+-- Create trigger function that calls the edge function
+CREATE OR REPLACE FUNCTION trigger_cleanup_expired_special_hours()
+RETURNS void
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+DECLARE
+  internal_secret text;
+  supabase_url text;
+  response_status int;
+BEGIN
+  -- Get configuration values
+  internal_secret := current_setting('app.internal_sync_secret', true);
+  supabase_url := current_setting('app.supabase_url', true);
+
+  -- Validate configuration
+  IF internal_secret IS NULL OR supabase_url IS NULL THEN
+    RAISE NOTICE '[Special Hours Cleanup] Missing configuration - internal_secret: %, supabase_url: %',
+      internal_secret IS NOT NULL, supabase_url IS NOT NULL;
+    RETURN;
+  END IF;
+
+  -- Call the edge function
+  SELECT status INTO response_status
+  FROM extensions.http((
+    'POST',
+    supabase_url || '/functions/v1/cleanup-expired-special-hours',
+    ARRAY[
+      extensions.http_header('Authorization', 'Bearer ' || internal_secret),
+      extensions.http_header('Content-Type', 'application/json')
+    ],
+    'application/json',
+    '{}'
+  )::extensions.http_request);
+
+  IF response_status >= 200 AND response_status < 300 THEN
+    RAISE NOTICE '[Special Hours Cleanup] Edge function called successfully';
+  ELSE
+    RAISE WARNING '[Special Hours Cleanup] Edge function returned status: %', response_status;
+  END IF;
+
+EXCEPTION WHEN OTHERS THEN
+  RAISE WARNING '[Special Hours Cleanup] Error calling edge function: %', SQLERRM;
+END;
+$$;
+
+-- Update the cron job to call our new trigger function instead of direct DELETE
+-- First, remove the old job
+SELECT cron.unschedule('cleanup-expired-special-hours');
+
+-- Create new job that calls the edge function
+SELECT cron.schedule(
+  'cleanup-expired-special-hours',
+  '0 0 * * *', -- Run daily at midnight
+  $$ SELECT trigger_cleanup_expired_special_hours(); $$
+);
+
+-- Add comment for documentation
+COMMENT ON FUNCTION trigger_cleanup_expired_special_hours() IS
+  'Triggers the cleanup-expired-special-hours edge function. Called by pg_cron daily at midnight. Deletes expired special hours and updates VAPI system prompts.';