{
  "name": "AI Image Generator",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "generate-ai-image/create",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "5e80126f-caa8-4e2c-833b-b8605dadf602",
      "name": "AI Image Generator",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -896,
        192
      ],
      "webhookId": "generate-ai-image-create"
    },
    {
      "parameters": {
        "jsCode": "const payload = $json.body && typeof $json.body === 'object' ? $json.body : $json;\nconst headers = $json.headers || {};\nconst requestId = payload.request_id || payload.trace_id || 'ai-image-' + Date.now() + '-' + Math.random().toString(16).slice(2);\nconst startedAt = new Date().toISOString();\n\nfunction firstValue(...values) {\n  for (const value of values) {\n    if (value !== undefined && value !== null && String(value).trim() !== '') return value;\n  }\n  return undefined;\n}\n\nfunction firstDefinedValue(...values) {\n  for (const value of values) {\n    if (value !== undefined && value !== null) return value;\n  }\n  return undefined;\n}\n\nfunction cleanText(value) {\n  return String(value || '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction cleanMultilineText(value) {\n  return String(value || '')\n    .split(/\\r?\\n/)\n    .map((line) => line.replace(/\\s+/g, ' ').trim())\n    .filter(Boolean)\n    .join('\\n');\n}\n\nasync function checkReferenceImageUrl(url) {\n  if (typeof fetch !== 'function') {\n    return { ok: true, url, reason: 'fetch unavailable in n8n code node; passing URL through' };\n  }\n\n  const controller = new AbortController();\n  const timeout = setTimeout(() => controller.abort(), 5000);\n  try {\n    let response = await fetch(url, {\n      method: 'HEAD',\n      redirect: 'follow',\n      signal: controller.signal,\n      headers: { Accept: 'image/*,*/*;q=0.8' }\n    });\n\n    if (!response.ok || !String(response.headers.get('content-type') || '').toLowerCase().startsWith('image/')) {\n      response = await fetch(url, {\n        method: 'GET',\n        redirect: 'follow',\n        signal: controller.signal,\n        headers: { Accept: 'image/*,*/*;q=0.8', Range: 'bytes=0-2047' }\n      });\n    }\n\n    const contentType = String(response.headers.get('content-type') || '').toLowerCase();\n    if (!response.ok) return { ok: false, url, reason: 'HTTP ' + response.status };\n    if (contentType && !contentType.startsWith('image/')) return { ok: false, url, reason: 'Non-image content-type: ' + contentType };\n    return { ok: true, url };\n  } catch (error) {\n    return { ok: false, url, reason: error?.name === 'AbortError' ? 'Download check timed out' : (error?.message || String(error)) };\n  } finally {\n    clearTimeout(timeout);\n  }\n}\n\nconst posterTypes = {\n  '9': {\n    key: 'offer',\n    label: 'restaurant offer poster',\n    direction: 'Make the poster feel promotional, appetizing, and value-focused. Highlight the offer clearly with strong hierarchy, warm food styling, and a clean callout area for price or deal details.'\n  },\n  '10': {\n    key: 'event',\n    label: 'restaurant event poster',\n    direction: 'Make the poster feel like a restaurant event announcement. Use a celebratory, social, date-night or live-event atmosphere with clear event-title hierarchy and space for event details.'\n  },\n  '11': {\n    key: 'menu',\n    label: 'restaurant menu poster',\n    direction: 'Make the poster feel like a curated restaurant menu feature. Use organized menu-style layout, readable dish information, premium food presentation, and balanced spacing.'\n  }\n};\n\nconst token = firstValue(payload.token);\nconst city = firstValue(payload.city, payload.City);\nconst offerId = firstValue(payload.id, payload.offer_id, payload.offerId, payload.offerID);\nconst zavedeniaId = firstValue(payload.zavedenia_id, payload.zavedeniaId, payload.zId, payload.z_id);\nconst type = firstValue(payload.type, payload.image_type, payload.imageType);\nconst typeConfig = posterTypes[String(type || '')];\nconst title = firstValue(payload.title, payload.poster_title, payload.posterTitle);\nconst context = firstValue(payload.context, payload.ambiance, payload.ambience, payload.description, payload.instructions);\nconst content = firstDefinedValue(payload.content, payload.poster_content, payload.posterContent, payload.text);\nconst language = firstValue(payload.language, payload.lang);\nconst logoImageUrl = firstValue(payload.logo_image_url, payload.logoImageUrl, payload.logo);\nconst authorization = firstValue(headers.authorization, headers.Authorization, payload.authorization, payload.aiAuthorization, payload.ai_authorization);\nconst notifyEmail = 'zavedenia@gmail.com,darko@appfactory.bg';\nconst referenceImages = [];\nfor (const value of [payload.referenceImageUrl, payload.reference_image_url, payload.referenceImageUrl2, payload.reference_image_url_2]) {\n  if (value !== undefined && value !== null && String(value).trim() !== '') referenceImages.push(String(value).trim());\n}\nfor (const value of payload.referenceImageUrls || payload.referenceImages || payload.reference_image_urls || []) {\n  if (value !== undefined && value !== null && String(value).trim() !== '') referenceImages.push(String(value).trim());\n}\n\nconst missing = [];\nif (!token) missing.push('token');\nif (!city) missing.push('city');\nif (!offerId) missing.push('offer_id');\nif (!type) missing.push('type');\nif (type && !typeConfig) missing.push('type must be one of 9, 10, 11');\nif (!zavedeniaId) missing.push('zavedenia_id');\nif (!title) missing.push('title');\nif (!context) missing.push('context');\nif (!logoImageUrl) missing.push('logoImageUrl');\nif (!language) missing.push('language');\nif (!authorization) missing.push('Authorization header');\n\nif (missing.length) {\n  return [{\n    json: {\n      kind: 'error',\n      ok: false,\n      statusCode: 400,\n      request_id: requestId,\n      trace_id: requestId,\n      started_at: startedAt,\n      finished_at: new Date().toISOString(),\n      notify_email: notifyEmail,\n      city: city || null,\n      offer_id: offerId ? String(offerId) : null,\n      zavedenia_id: zavedeniaId || null,\n      error_stage: 'validation',\n      failed_node: 'Validate Initial Request',\n      error: 'Missing or invalid required input',\n      request_payload: payload,\n      details: { missing, payload }\n    }\n  }];\n}\n\nconst referenceImageChecks = await Promise.all([...new Set(referenceImages)].map(checkReferenceImageUrl));\nconst usableReferenceImageUrls = referenceImageChecks.filter((item) => item.ok).map((item) => item.url);\nconst skippedReferenceImages = referenceImageChecks.filter((item) => !item.ok);\n\nconst safeTitle = cleanText(title);\nconst safeContext = cleanText(context);\nconst safeContent = cleanMultilineText(content);\nconst safeLanguage = cleanText(language);\nconst systemPrompt = [\n  'You are a senior restaurant marketing art director and poster designer.',\n  'Create a high-quality vertical poster for a mobile restaurant app.',\n  'The design must be ultra realistic, appetizing, polished, readable, and commercially useful.',\n  'Render the food, restaurant interior, lighting, materials, table styling, and atmosphere as believable real-world photography rather than illustration or CGI-looking artwork.',\n  'Use natural shadows, realistic reflections, authentic textures, believable depth of field, and cinematic lighting.',\n  'Use food, restaurant interior details, table settings, typography, and brand elements as the primary visual language.',\n  'Do not recreate unrelated famous or trademarked logos; use only the provided restaurant logo as the intentional brand mark.',\n  'Use the restaurant logo as branding, but do not let it dominate the composition.',\n  'Keep all visible text exactly in the requested language and avoid inventing unavailable prices, dates, or menu items.',\n  'If reference images contain specific bottles, labels, packaging, or glassware, preserve those object details faithfully and do not redesign them.'\n].join(' ');\nconst promptParts = [\n  'Poster type: ' + typeConfig.label + '.',\n  typeConfig.direction,\n  'Poster title: \"' + safeTitle + '\".',\n  'Ambiance/context from the restaurant: ' + safeContext + '.',\n  usableReferenceImageUrls.length ? 'Reference images are provided and must be used as primary visual guidance for the poster. Base the restaurant atmosphere, interior materials, colors, lighting, decor style, food presentation, and overall composition on the reference images whenever they are available. Keep the result commercially polished and poster-like, but make the generated image clearly reflect the supplied references. Do not recreate unrelated famous or trademarked logos from reference images. Use only the provided restaurant logo as the intentional brand mark. Do not modify bottle shape, etiquette or label color, packaging color, glass shape, or other distinctive product details visible in the reference images.' : '',\n  safeContent ? 'Poster text/content to include: ' + safeContent + '.' : '',\n  'Language for poster text: ' + safeLanguage + '.',\n  'Create a 1024x1280 vertical composition with clear hierarchy, strong food-focused visual appeal, tasteful typography, enough contrast for mobile readability, and an ultra realistic photographic look with natural lighting, authentic materials, and believable depth.',\n  'Preserve the identity of important referenced objects exactly. If a bottle, box, candle, plate, or glass appears in the reference image, keep its visible design details unchanged rather than inventing a new version.',\n  'Do not include placeholder text, watermarks, QR codes, fake logos, unrelated famous or trademarked logos, or unrelated brand names.'\n].filter(Boolean).join('\\n');\n\nconst aiRequest = { ...payload };\ndelete aiRequest.system_prompt;\ndelete aiRequest.systemPrompt;\ndelete aiRequest.user_prompt;\ndelete aiRequest.userPrompt;\ndelete aiRequest.mockData;\ndelete aiRequest.mock_data;\nObject.assign(aiRequest, {\n  title: safeTitle,\n  context: safeContext,\n  offer_id: String(offerId),\n  zavedenia_id: String(zavedeniaId),\n  content: safeContent,\n  language: safeLanguage,\n  type: String(type),\n  posterType: typeConfig.key,\n  systemPrompt,\n  prompt: promptParts,\n  imageSize: '1024x1280',\n  imageQuality: 'high',\n  referenceImageUrls: usableReferenceImageUrls,\n  skippedReferenceImages\n});\n\nreturn [{\n  json: {\n    kind: 'accepted',\n    ok: true,\n    statusCode: 202,\n    request_id: requestId,\n    trace_id: requestId,\n    started_at: startedAt,\n    notify_email: notifyEmail,\n    token,\n    city,\n    offer_id: String(offerId),\n    id: String(offerId),\n    type: String(type),\n    poster_type: typeConfig.key,\n    title: safeTitle,\n    context: safeContext,\n    content: safeContent,\n    zavedenia_id: String(zavedeniaId),\n    authorization,\n    request_payload: payload,\n    skipped_reference_images: skippedReferenceImages,\n    ai_request: aiRequest,\n    accepted_response: {\n      status: 'processing',\n      request_id: requestId,\n      message: 'AI image generation has started. This can take some time. You will be notified when it is finished.'\n    }\n  }\n}];"
      },
      "id": "e44edb0e-ba62-4fb1-ae87-f07b8533f85d",
      "name": "Validate Initial Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -672,
        192
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ec6e4c6a-65cf-418d-8ebe-a912317a3a10",
              "leftValue": "={{ $json.kind }}",
              "rightValue": "error",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "ce20b03e-2fd8-40f5-8a58-f4dd6dec3423",
      "name": "Is Request Invalid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -448,
        192
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseCode": "={{ $json.statusCode || 400 }}"
        }
      },
      "id": "80be41a0-0bba-4747-8bf1-8e3b7cc3a8cf",
      "name": "Respond Validation Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json.accepted_response }}",
        "options": {
          "responseCode": 202
        }
      },
      "id": "7d66342a-ccca-426e-9181-19eb8889f3dc",
      "name": "Respond Accepted",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -224,
        256
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $getWorkflowStaticData('global');\nconst now = Date.now();\nconst spacingMs = 90000;\nconst nextAllowedAt = Number(data.next_ai_image_generate_at || 0);\nconst waitMs = Math.max(0, nextAllowedAt - now);\n\nif (waitMs > 0) {\n  await new Promise((resolve) => setTimeout(resolve, waitMs));\n}\n\nconst startedAt = Date.now();\ndata.next_ai_image_generate_at = startedAt + spacingMs;\n\nreturn items.map((item) => ({\n  json: {\n    ...item.json,\n    ai_image_throttle_wait_ms: waitMs,\n    ai_image_next_allowed_at: new Date(data.next_ai_image_generate_at).toISOString()\n  },\n  binary: item.binary\n}));"
      },
      "id": "a7c6ddc3-872f-40b2-bd53-20e81efc42b1",
      "name": "Throttle AI Image Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -224,
        448
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://ai.zavedenia.com/generate-ai-image/create",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "={{ $json.authorization }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.ai_request) }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 1200000
        }
      },
      "id": "35b995d0-16b9-4c93-b54f-b736f4e0e10d",
      "name": "Generate AI Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        0,
        352
      ],
      "retryOnFail": true,
      "maxTries": 5,
      "waitBetweenTries": 60000,
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Validate Initial Request').first().json;\nconst response = $json || {};\nconst finishedAt = new Date().toISOString();\nconst durationMs = Date.now() - Date.parse(meta.started_at);\n\nfunction errorItem(stage, failedNode, message, details = {}) {\n  return [{\n    json: {\n      kind: 'error',\n      ok: false,\n      request_id: meta.request_id,\n      trace_id: meta.trace_id,\n      started_at: meta.started_at,\n      finished_at: finishedAt,\n      duration_ms: durationMs,\n      duration_seconds: Number((durationMs / 1000).toFixed(2)),\n      notify_email: meta.notify_email,\n      city: meta.city,\n      offer_id: meta.offer_id,\n      zavedenia_id: meta.zavedenia_id || null,\n      error_stage: stage,\n      failed_node: failedNode,\n      error: message,\n      request_payload: meta.request_payload || null,\n      ai_request: meta.ai_request || null,\n      details: { ...details, request_payload: meta.request_payload || null, ai_request: meta.ai_request || null }\n    }\n  }];\n}\n\nif (response.error) {\n  return errorItem('generate_image', 'Generate AI Image', 'AI image generation request failed', { response_error: response.error, response });\n}\n\nconst imageBase64 = response.image_base64 || response.imageBase64 || response.image || response.file_base64 || response.data?.[0]?.b64_json;\n\nif (!imageBase64) {\n  return errorItem('generate_image', 'Generate AI Image', 'Image generation response did not contain image_base64', { response });\n}\n\nconst dataUrlMatch = String(imageBase64).match(/^data:(image\\/[a-zA-Z0-9.+-]+);base64,(.+)$/);\nconst cleanBase64 = dataUrlMatch?.[2] || String(imageBase64);\nconst imageBuffer = Buffer.from(cleanBase64, 'base64');\n\nif (!imageBuffer.length) {\n  return errorItem('prepare_image_file', 'Prepare Offer Image File', 'Generated image base64 decoded to an empty file');\n}\n\nfunction detectMime(buffer) {\n  if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return 'image/jpeg';\n  if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return 'image/png';\n  if (buffer.slice(0, 4).toString('ascii') === 'RIFF' && buffer.slice(8, 12).toString('ascii') === 'WEBP') return 'image/webp';\n  return null;\n}\n\nconst mimeType = detectMime(imageBuffer) || dataUrlMatch?.[1] || response.mime_type || response.mimeType || 'image/jpeg';\nconst extension = mimeType.includes('png') ? 'png' : mimeType.includes('webp') ? 'webp' : 'jpg';\nconst fileName = `offer_${meta.offer_id}.${extension}`;\n\nreturn [{\n  json: {\n    kind: 'image_ready',\n    ok: true,\n    request_id: meta.request_id,\n    trace_id: meta.trace_id,\n    started_at: meta.started_at,\n    notify_email: meta.notify_email,\n    token: meta.token,\n    city: meta.city,\n    offer_id: meta.offer_id,\n    id: meta.id || meta.offer_id,\n    type: meta.type,\n    poster_type: meta.poster_type || null,\n    title: meta.title || null,\n    context: meta.context || null,\n    zavedenia_id: meta.zavedenia_id || null,\n    request_payload: meta.request_payload || null,\n    ai_request: meta.ai_request || null,\n    generated_model: response.model || null,\n    generated_response_id: response.response_id || null,\n    generated_price_usd: response.price_usd ?? null,\n    image_file_name: fileName,\n    image_mime_type: mimeType,\n    image_size_bytes: imageBuffer.length\n  },\n  binary: {\n    file: await this.helpers.prepareBinaryData(imageBuffer, fileName, mimeType)\n  }\n}];"
      },
      "id": "ad188a33-0199-4bc6-8cdb-d7258ace978c",
      "name": "Prepare Offer Image File",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        224,
        352
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://zavedenia.com/apps/orderapprestaurant/api/ai_image_insert.php",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "city",
              "value": "={{ $json.city }}"
            },
            {
              "name": "token",
              "value": "={{ $json.token }}"
            },
            {
              "name": "type",
              "value": "={{ $json.type }}"
            },
            {
              "name": "id",
              "value": "={{ $json.id || $json.offer_id }}"
            },
            {
              "name": "zavedenia_id",
              "value": "={{ $json.zavedenia_id }}"
            },
            {
              "parameterType": "formBinaryData",
              "name": "image",
              "inputDataFieldName": "file"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 120000
        }
      },
      "id": "c2a311f3-53a1-4d80-8a1e-39382ae5c31a",
      "name": "Upload Offer Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        672,
        432
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const uploadResponse = $json || {};\nconst meta = $('Prepare Offer Image File').first().json;\nconst finishedAt = new Date().toISOString();\nconst durationMs = Date.now() - Date.parse(meta.started_at);\n\nfunction pickUrl(response) {\n  return response.image_url || response.ai_image_url || response.file_url || response.url || response.data?.image_url || response.data?.ai_image_url || response.data?.url || null;\n}\n\nif (uploadResponse.error || uploadResponse.success === false || uploadResponse.ok === false || uploadResponse.status === 'error') {\n  return [{\n    json: {\n      kind: 'error',\n      ok: false,\n      request_id: meta.request_id,\n      trace_id: meta.trace_id,\n      started_at: meta.started_at,\n      finished_at: finishedAt,\n      duration_ms: durationMs,\n      duration_seconds: Number((durationMs / 1000).toFixed(2)),\n      notify_email: meta.notify_email,\n      city: meta.city,\n      offer_id: meta.offer_id,\n      zavedenia_id: meta.zavedenia_id || null,\n      error_stage: 'insert_ai_image',\n      failed_node: 'Upload Offer Image',\n      error: 'AI image insert failed',\n      request_payload: meta.request_payload || null,\n      ai_request: meta.ai_request || null,\n      upload_payload: { city: meta.city, token: meta.token, type: meta.type, id: meta.id || meta.offer_id, zavedenia_id: meta.zavedenia_id || null },\n      details: { upload_response: uploadResponse, request_payload: meta.request_payload || null, ai_request: meta.ai_request || null }\n    }\n  }];\n}\n\nreturn [{\n  json: {\n    kind: 'upload_success',\n    ok: true,\n    request_id: meta.request_id,\n    trace_id: meta.trace_id,\n    started_at: meta.started_at,\n    notify_email: meta.notify_email,\n    offer_id: meta.offer_id,\n    id: meta.id || meta.offer_id,\n    type: meta.type,\n    poster_type: meta.poster_type || null,\n    title: meta.title || null,\n    context: meta.context || null,\n    zavedenia_id: meta.zavedenia_id || null,\n    city: meta.city,\n    request_payload: meta.request_payload || null,\n    ai_request: meta.ai_request || null,\n    upload_response: uploadResponse,\n    image_url: pickUrl(uploadResponse)\n  }\n}];"
      },
      "id": "7478bc9b-c087-4c67-a9e6-dde42be9c335",
      "name": "Check Upload Success",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        896,
        432
      ]
    },
    {
      "parameters": {
        "fromEmail": "zavedenia@gmail.com",
        "toEmail": "={{ $json.notify_email || \"zavedenia@gmail.com,darko@appfactory.bg\" }}",
        "subject": "={{ 'AI image generation started for ' + ($json.poster_type || 'poster') + ' ' + $json.offer_id }}",
        "html": "=<p>AI image generation request received.</p><p><strong>ID:</strong> {{ $json.offer_id }}<br><strong>Type:</strong> {{ $json.type }} / {{ $json.poster_type || 'unknown' }}<br><strong>Title:</strong> {{ $json.title || 'unknown' }}<br><strong>Context:</strong> {{ $json.context || 'unknown' }}<br><strong>Zavedenia ID:</strong> {{ $json.zavedenia_id || 'unknown' }}<br><strong>City:</strong> {{ $json.city }}<br><strong>Trace ID:</strong> {{ $json.trace_id }}<br><strong>Started at:</strong> {{ $json.started_at }}</p>",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "5f9ad932-88f4-432e-9f6e-e964c6708d58",
      "name": "Email Generation Started",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        0,
        160
      ],
      "webhookId": "c88c7f8b-f970-4c71-80d3-f11262d44b1a",
      "credentials": {
        "smtp": {
          "id": "FHnrd8vBCAukd5VU",
          "name": "SMTP account"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "7aa84377-dc1b-4a4b-95df-efe3f9729c59",
              "leftValue": "={{ $json.kind }}",
              "rightValue": "error",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a2e17076-ca0a-4b91-bd2a-03d39d446e31",
      "name": "Is Image Generation Error?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        448,
        352
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "9502a417-0eab-44e7-b38d-771c9ebf2ad5",
              "leftValue": "={{ $json.kind }}",
              "rightValue": "error",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "f60d2e58-3392-4756-9818-ec09f49d1e55",
      "name": "Is Upload Error?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1120,
        432
      ]
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Check Upload Success').first().json;\nconst finishedAt = new Date().toISOString();\nconst durationMs = Date.now() - Date.parse(meta.started_at);\n\nreturn [{\n  json: {\n    kind: 'success',\n    ok: true,\n    request_id: meta.request_id,\n    trace_id: meta.trace_id,\n    started_at: meta.started_at,\n    finished_at: finishedAt,\n    duration_ms: durationMs,\n    duration_seconds: Number((durationMs / 1000).toFixed(2)),\n    notify_email: meta.notify_email,\n    city: meta.city,\n    offer_id: meta.offer_id,\n    id: meta.id || meta.offer_id,\n    type: meta.type,\n    poster_type: meta.poster_type || null,\n    title: meta.title || null,\n    context: meta.context || null,\n    zavedenia_id: meta.zavedenia_id || null,\n    request_payload: meta.request_payload || null,\n    ai_request: meta.ai_request || null,\n    image_url: meta.image_url || null,\n    upload_response: meta.upload_response\n  }\n}];"
      },
      "id": "87789af4-d700-454e-b821-2c17b985f5ec",
      "name": "Build Finish Status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1344,
        480
      ]
    },
    {
      "parameters": {
        "fromEmail": "zavedenia@gmail.com",
        "toEmail": "={{ $json.notify_email || \"zavedenia@gmail.com,darko@appfactory.bg\" }}",
        "subject": "={{ 'AI image generation failed at ' + ($json.failed_node || $json.error_stage || 'unknown node') + ' for ' + ($json.poster_type || 'poster') + ' ' + ($json.offer_id || 'unknown') }}",
        "html": "=<p>AI image generation flow failed.</p><p><strong>Failed node:</strong> {{ $json.failed_node || 'unknown' }}<br><strong>Stage:</strong> {{ $json.error_stage || 'unknown' }}<br><strong>Error:</strong> {{ $json.error || 'unknown' }}<br><strong>Offer ID:</strong> {{ $json.offer_id || 'unknown' }}<br><strong>Zavedenia ID:</strong> {{ $json.zavedenia_id || 'unknown' }}<br><strong>City:</strong> {{ $json.city || 'unknown' }}<br><strong>Trace ID:</strong> {{ $json.trace_id || $json.request_id || 'unknown' }}<br><strong>Started at:</strong> {{ $json.started_at || 'unknown' }}<br><strong>Finished at:</strong> {{ $json.finished_at || 'unknown' }}<br><strong>Duration:</strong> {{ $json.duration_seconds || 0 }} seconds</p><p><strong>Original request payload:</strong></p><pre>{{ JSON.stringify($json.request_payload || {}, null, 2) }}</pre><p><strong>Error details:</strong></p><pre>{{ JSON.stringify($json.details || {}, null, 2) }}</pre>",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "0531464b-6115-4ee7-92fa-a9a2bfae832a",
      "name": "Email Generation Error",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1360,
        0
      ],
      "webhookId": "058458c9-8b08-4c03-8c1f-2afa793677b4",
      "credentials": {
        "smtp": {
          "id": "FHnrd8vBCAukd5VU",
          "name": "SMTP account"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "fromEmail": "zavedenia@gmail.com",
        "toEmail": "={{ $json.notify_email || \"zavedenia@gmail.com,darko@appfactory.bg\" }}",
        "subject": "={{ 'AI image generation completed for ' + ($json.poster_type || 'poster') + ' ' + $json.offer_id + ' in ' + $json.duration_seconds + 's' }}",
        "html": "=<p>AI image generation flow completed successfully.</p><p><strong>ID:</strong> {{ $json.offer_id }}<br><strong>Type:</strong> {{ $json.type }} / {{ $json.poster_type || 'unknown' }}<br><strong>Title:</strong> {{ $json.title || 'unknown' }}<br><strong>Zavedenia ID:</strong> {{ $json.zavedenia_id || 'unknown' }}<br><strong>City:</strong> {{ $json.city }}<br><strong>Trace ID:</strong> {{ $json.trace_id }}<br><strong>Started at:</strong> {{ $json.started_at }}<br><strong>Finished at:</strong> {{ $json.finished_at }}<br><strong>Duration:</strong> {{ $json.duration_seconds }} seconds<br><strong>Image URL:</strong> {{ $json.image_url || 'not returned by upload API' }}</p><p><strong>Upload response:</strong></p><pre>{{ JSON.stringify($json.upload_response || {}, null, 2) }}</pre>",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "620ed8b6-12e0-4ea1-ac26-0c4a464507fd",
      "name": "Email Generation Success",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1568,
        480
      ],
      "webhookId": "510b1a39-0619-4e33-a7d2-e1c736f3aad5",
      "credentials": {
        "smtp": {
          "id": "FHnrd8vBCAukd5VU",
          "name": "SMTP account"
        }
      },
      "continueOnFail": true
    }
  ],
  "pinData": {
    "AI Image Generator Webhook": [
      {
        "json": {
          "headers": {
            "content-type": "application/json",
            "authorization": "Bearer YOUR_AI_AUTH_TOKEN"
          },
          "params": {},
          "query": {},
          "body": {
            "city": "sofia",
            "token": "55f5d0dbb4ebacf85e919f3bed8ea4f5",
            "type": "9",
            "offer_id": "65157",
            "zavedenia_id": 3054,
            "title": "Специјална понуда",
            "context": "пицария, дървена обстановка, градина",
            "logoImageUrl": "https://theshoepeople.weebly.com/uploads/1/0/0/5/10059130/5476832_orig.jpg",
            "referenceImageUrl": "https://zavedenia.com/zimages/sofia/big/3054/3054-127403114224195205.jpg",
            "referenceImageUrl2": "https://zavedenia.com/zimages/sofia/big/3054/3054-1719505305.jpg",
            "language": "mk",
            "content": "Title:Српски палачинки\nНајубава пита од боровница\nво Планинското бачило Скитуљко\nДомашен вкус кој се памети"
          }
        }
      }
    ]
  },
  "connections": {
    "AI Image Generator": {
      "main": [
        [
          {
            "node": "Validate Initial Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Initial Request": {
      "main": [
        [
          {
            "node": "Is Request Invalid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Request Invalid?": {
      "main": [
        [
          {
            "node": "Respond Validation Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Accepted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond Validation Error": {
      "main": [
        [
          {
            "node": "Email Generation Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond Accepted": {
      "main": [
        [
          {
            "node": "Email Generation Started",
            "type": "main",
            "index": 0
          },
          {
            "node": "Throttle AI Image Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI Image": {
      "main": [
        [
          {
            "node": "Prepare Offer Image File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Offer Image File": {
      "main": [
        [
          {
            "node": "Is Image Generation Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Image Generation Error?": {
      "main": [
        [
          {
            "node": "Email Generation Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upload Offer Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Offer Image": {
      "main": [
        [
          {
            "node": "Check Upload Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Upload Success": {
      "main": [
        [
          {
            "node": "Is Upload Error?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Upload Error?": {
      "main": [
        [
          {
            "node": "Email Generation Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Finish Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Finish Status": {
      "main": [
        [
          {
            "node": "Email Generation Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Throttle AI Image Request": {
      "main": [
        [
          {
            "node": "Generate AI Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "6643d871-ebf0-4056-843e-ef48ee052f73",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "a69a2ae11d22a0ab13dc5c02e8089d4035c7a914714358414fedccbd61128bac"
  },
  "nodeGroups": [],
  "id": "GPYxguCKyf5O9356",
  "tags": []
}
