{
  "name": "Wine invoice to pitch cards",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "wine-invoice",
        "responseMode": "responseNode",
        "options": {
          "binaryPropertyName": "invoice"
        }
      },
      "id": "a1f2c3d4-0001-4a7b-9c01-cellarsheet01",
      "name": "Invoice upload webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-460, 0],
      "webhookId": "wine-invoice"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Turn the uploaded binary (image or PDF) into a base64 data URL for the\n// GPT-4o Vision call, and carry venue_id from the multipart form field.\nconst item = items[0];\nconst bin = item.binary?.invoice ?? Object.values(item.binary ?? {})[0];\nif (!bin) throw new Error('No file on the webhook. Send multipart field \"invoice\".');\nconst mime = bin.mimeType || 'image/jpeg';\nconst buf = await this.helpers.getBinaryDataBuffer(0, 'invoice');\nreturn [{\n  json: {\n    venueId: item.json.body?.venue_id ?? item.json.venue_id ?? null,\n    fileName: bin.fileName ?? 'invoice',\n    fileKind: mime === 'application/pdf' ? 'pdf' : 'image',\n    dataUrl: `data:${mime};base64,${buf.toString('base64')}`\n  }\n}];"
      },
      "id": "a1f2c3d4-0002-4a7b-9c01-cellarsheet02",
      "name": "Build data URL",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-220, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  model: 'gpt-4o',\n  max_tokens: 4000,\n  response_format: {\n    type: 'json_schema',\n    json_schema: {\n      name: 'invoice_parse',\n      strict: true,\n      schema: {\n        type: 'object', additionalProperties: false,\n        required: ['distributor','invoiceNumber','invoiceDate','items'],\n        properties: {\n          distributor: { type: 'string' },\n          invoiceNumber: { type: 'string' },\n          invoiceDate: { type: 'string' },\n          items: { type: 'array', items: {\n            type: 'object', additionalProperties: false,\n            required: ['name','vintage','varietal','region','style','qty','unitCost','taste','palate','pairings','pitch'],\n            properties: {\n              name: { type: 'string' }, vintage: { type: 'string' },\n              varietal: { type: 'string' }, region: { type: 'string' },\n              style: { type: 'string', enum: ['red','white','sparkling','rose','dessert'] },\n              qty: { type: 'integer' }, unitCost: { type: 'number' },\n              taste: { type: 'string' },\n              palate: { type: 'object', additionalProperties: false,\n                required: ['body','acidity','tannin','sweetness'],\n                properties: { body: {type:'integer'}, acidity: {type:'integer'}, tannin: {type:'integer'}, sweetness: {type:'integer'} } },\n              pairings: { type: 'array', items: { type: 'string' } },\n              pitch: { type: 'string' }\n            }\n          } }\n        }\n      }\n    }\n  },\n  messages: [\n    { role: 'system', content: 'You are the wine director for a neighborhood restaurant. You read wine distributor invoices and turn each line item into a cheat-sheet entry a busy server can use table-side. Never use em dashes.' },\n    { role: 'user', content: [\n      { type: 'image_url', image_url: { url: $json.dataUrl } },\n      { type: 'text', text: 'Read this wine distributor invoice. Extract distributor, invoiceNumber, invoiceDate, and every WINE line item (skip freight, deposits, glassware, credits, taxes). Per wine: name (producer plus cuvee, no SKUs), vintage or NV, varietal, region, style (red|white|sparkling|rose|dessert), qty in bottles (12 per case if only cases are printed), unitCost per bottle in dollars, taste (5-9 words, concrete flavors), palate scores 1-5 (body, acidity, tannin, sweetness), exactly 3 dish pairings, and pitch: ONE sentence, max 28 words, that a server could say verbatim at the table. Copy numbers exactly as printed.' }\n    ] }\n  ]\n}) }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "a1f2c3d4-0003-4a7b-9c01-cellarsheet03",
      "name": "GPT-4o Vision parse",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [20, 0]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Price the list (house rule: bottle = cost x 2.8, glass = bottle / 4) and\n// shape one invoice row plus one item per wine for the Supabase inserts.\nconst upload = $('Build data URL').first().json;\nconst parsed = JSON.parse($json.choices[0].message.content);\nconst bottle = c => Math.max(1, Math.round(c * 2.8));\nconst glass = c => Math.max(1, Math.round((bottle(c) / 4) * 2) / 2);\nconst wines = parsed.items.map(it => ({\n  venue_id: upload.venueId,\n  name: it.name, vintage: it.vintage, varietal: it.varietal, region: it.region,\n  style: it.style, qty: it.qty, unit_cost: it.unitCost,\n  bottle_price: bottle(it.unitCost),\n  glass_price: it.unitCost <= 28 ? glass(it.unitCost) : null,\n  btg: it.unitCost <= 28,\n  taste: it.taste, palate: it.palate, pairings: it.pairings, pitch: it.pitch,\n  published: false\n}));\nreturn [{ json: {\n  invoice: {\n    venue_id: upload.venueId,\n    distributor: parsed.distributor,\n    invoice_number: parsed.invoiceNumber,\n    invoice_date: parsed.invoiceDate || null,\n    file_path: `${upload.venueId}/${upload.fileName}`,\n    file_kind: upload.fileKind,\n    status: 'review',\n    total_cost: wines.reduce((s, w) => s + w.unit_cost * w.qty, 0)\n  },\n  wines\n} }];"
      },
      "id": "a1f2c3d4-0004-4a7b-9c01-cellarsheet04",
      "name": "Price and shape rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [260, 0]
    },
    {
      "parameters": {
        "operation": "create",
        "tableId": "invoices",
        "dataToSend": "autoMapInputData"
      },
      "id": "a1f2c3d4-0005-4a7b-9c01-cellarsheet05",
      "name": "Create invoice row",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [500, 0],
      "notes": "Uses the service-role key (bypasses RLS by design). Map fields from $json.invoice."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// One n8n item per wine, stamped with the invoice id we just created.\nconst invoiceId = $json.id;\nconst wines = $('Price and shape rows').first().json.wines;\nreturn wines.map(w => ({ json: { ...w, invoice_id: invoiceId } }));"
      },
      "id": "a1f2c3d4-0006-4a7b-9c01-cellarsheet06",
      "name": "Split wines",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [740, 0]
    },
    {
      "parameters": {
        "operation": "create",
        "tableId": "wines",
        "dataToSend": "autoMapInputData"
      },
      "id": "a1f2c3d4-0007-4a7b-9c01-cellarsheet07",
      "name": "Create wine rows",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [980, 0]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: true, wines: $items().length, status: 'review' }) }}"
      },
      "id": "a1f2c3d4-0008-4a7b-9c01-cellarsheet08",
      "name": "Respond to upload",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [1220, 0]
    }
  ],
  "connections": {
    "Invoice upload webhook": {
      "main": [[{ "node": "Build data URL", "type": "main", "index": 0 }]]
    },
    "Build data URL": {
      "main": [[{ "node": "GPT-4o Vision parse", "type": "main", "index": 0 }]]
    },
    "GPT-4o Vision parse": {
      "main": [[{ "node": "Price and shape rows", "type": "main", "index": 0 }]]
    },
    "Price and shape rows": {
      "main": [[{ "node": "Create invoice row", "type": "main", "index": 0 }]]
    },
    "Create invoice row": {
      "main": [[{ "node": "Split wines", "type": "main", "index": 0 }]]
    },
    "Split wines": {
      "main": [[{ "node": "Create wine rows", "type": "main", "index": 0 }]]
    },
    "Create wine rows": {
      "main": [[{ "node": "Respond to upload", "type": "main", "index": 0 }]]
    }
  },
  "settings": { "executionOrder": "v1" },
  "pinData": {}
}
