{
  "name": "Grocy Receipt Input",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "grocery-receipt",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        60,
        -205
      ],
      "id": "c6f6c942-2fd4-493b-8b64-2f911e9f8952",
      "name": "HTTP Webhook"
    },
    {
      "parameters": {
        "jsCode": "function sanitizeLLMJson(text = \"\") {\n  if (typeof text !== \"string\") return \"\";\n\n  let out = text.trim();\n\n  out = out.replace(/^```[\\s\\S]*?\\n/, \"\").replace(/```[\\s\\S]*$/, \"\"); // trailing fence\n\n  const firstBrace = out.indexOf(\"{\");\n  if (firstBrace > 0) out = out.slice(firstBrace);\n\n  const lastBrace = out.lastIndexOf(\"}\");\n  if (lastBrace !== -1) out = out.slice(0, lastBrace + 1);\n\n  out = out.replace(/[\u201c\u201d]/g, '\"').replace(/[\u2018\u2019]/g, \"'\");\n\n  out = out.replace(/,\\s*([}\\]])/g, \"$1\");\n\n  return out.trim();\n}\n\nconst rawReceipt = $(\"parsed_receipt\").last().json.content;\nconst ai = JSON.parse(sanitizeLLMJson(rawReceipt));\n\nconst list = (k) =>\n  $(k)\n    .all()\n    .map((x) => x.json);\n\nconst products = list(\"grocy_products\");\nconst groups = list(\"grocy_product_groups\");\nconst stores = list(\"grocy_stores\");\nconst locations = list(\"grocy_locations\");\n\nconst productMap = new Map(\n  products.filter((p) => p.name).map((p) => [p.name.toLowerCase(), p]),\n);\n\nconst findId = (arr, label) => {\n  if (!label) return null;\n  const hit = arr.find(\n    (e) => e.name && e.name.toLowerCase() === label.toLowerCase(),\n  );\n  return hit ? hit.id : null;\n};\n\nconst storeId = findId(stores, ai.store_name);\n\nconst out = [];\n\n(ai.items || []).forEach((item) => {\n  if (!item || !item.name) return;\n\n  const groupId = findId(groups, item.group);\n  const locationId = findId(locations, item.location);\n\n  const base = {\n    quantity: item.quantity ?? 1,\n    price: item.unit_price ?? 0,\n    purchase_date: ai.receipt_date,\n    best_before_date: new Date(\n      Date.parse(ai.receipt_date) + (item.shelf_life ?? 0) * 86400000,\n    )\n      .toISOString()\n      .slice(0, 10),\n    product_group_id: groupId,\n    shopping_location_id: storeId,\n    location_id: locationId,\n  };\n\n  const hit = productMap.get(item.name.toLowerCase());\n\n  out.push(\n    hit\n      ? { action: \"add_stock\", product_id: hit.id, ...base }\n      : {\n          action: \"create_product\",\n          name: item.name,\n          shelf_life: item.shelf_life ?? null,\n          ...base,\n        },\n  );\n});\n\nreturn out.map((j) => ({ json: j }));\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        -180
      ],
      "id": "01cf2e0f-2010-4dc4-8eef-a0709ba99dbc",
      "name": "Process Receipt Items"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "create-condition",
              "leftValue": "={{ $json.action }}",
              "rightValue": "create_product",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1160,
        -180
      ],
      "id": "c1eb8cdf-c45d-41f7-a5a1-763a5f756ca8",
      "name": "Check Action Type"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://<grocy_url>/api/objects/products",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"name\":\"{{$json.name}}\",\n  \"product_group_id\": \"{{ $json.product_group_id }}\",\n  \"location_id\": \"{{ $json.location_id }}\",\n  \"qu_id_purchase\":1,\n  \"qu_id_stock\":1,\n  \"min_stock_amount\":0\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1380,
        -355
      ],
      "id": "99f774ef-68e6-4fee-bbc0-b0e7f0114f04",
      "name": "Create New Product",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "https://<grocy_url>/api/objects/products",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        280,
        -580
      ],
      "id": "cb7a9767-4967-4c99-9f4d-ac037bce6f33",
      "name": "grocy_products",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "analyze",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o"
        },
        "text": "=Analyze this grocery receipt and extract the following information for each item:\n1. Product name (generic, no brand words)\n2. Quantity purchased\n3. Unit price\n4. Total line price\n5. Purchase date from receipt\n6. Estimated shelf life in days\nReturn JSON (no markdown):\n{\n  \"receipt_date\":\"YYYY-MM-DD\",\n  \"store_name\":\"(if visible)\",\n  \"items\":[\n    {\n      \"name\":\"Product Name\",\n      \"quantity\":<num>,\n      \"unit_price\":<unit_price>,\n      \"total_price\":<total>,\n      \"shelf_life\":<days>,\n      \"group\":<matched_product_group>,\n      \"location\":<product_storage_location>\n    }\n  ]\n}\nExisting products (match if possible): {{ $('grocy_products').all().map((item) => item.json.name).filter(item => item)}}\nValid values for group: {{$('grocy_product_groups').all().map(i=>i.json.name)}}  \nValid values for location: {{$('grocy_locations').all().map(i=>i.json.name)}}  \nValid store list: {{$('grocy_stores').all().map(i=>i.json.name)}}",
        "inputType": "base64",
        "binaryPropertyName": "file",
        "options": {
          "maxTokens": 1000
        }
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        720,
        -180
      ],
      "id": "4fc97a20-ff9b-4dad-b68f-31d7c5fc3576",
      "name": "parsed_receipt",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "chooseBranch",
        "numberInputs": 5,
        "useDataOfInput": 5
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        500,
        -222
      ],
      "id": "7cab1a14-3a57-4a08-8afe-e909b02af8ef",
      "name": "Merge"
    },
    {
      "parameters": {
        "operation": "resize",
        "dataPropertyName": "file",
        "width": 50,
        "height": 50,
        "resizeOption": "percent",
        "options": {}
      },
      "type": "n8n-nodes-base.editImage",
      "typeVersion": 1,
      "position": [
        280,
        220
      ],
      "id": "26554c43-8a12-4321-b9b1-b900bec4b972",
      "name": "Edit Image"
    },
    {
      "parameters": {
        "url": "https://<grocy_url>/api/objects/locations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        280,
        -380
      ],
      "id": "e61dd740-3e77-406a-949e-87bd2f0aed36",
      "name": "grocy_locations",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "https://<grocy_url>/api/objects/product_groups",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        280,
        -180
      ],
      "id": "4057b59c-c17c-45c0-a73f-826316404011",
      "name": "grocy_product_groups",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "https://<grocy_url>/api/objects/shopping_locations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        280,
        20
      ],
      "id": "87e36e45-c2c8-4bbf-a147-54bddd3be0af",
      "name": "grocy_stores",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        1600,
        -280
      ],
      "id": "0294a39f-7bb8-4d15-a6d3-09652629b5d9",
      "name": "Merge1"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://<grocy_url>/api/stock/products/{{ $json.created_object_id }}/add",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"amount\":{{$json.quantity}},\n  \"price\":{{$json.price}},\n  \"transaction_type\":\"purchase\",\n  \"purchased_date\":\"{{$json.purchase_date}}\",\n  \"best_before_date\":\"{{$json.best_before_date}}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1820,
        -280
      ],
      "id": "b04a72fc-dea9-4591-a5c4-8f206415983a",
      "name": "Add Stock - New Product",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://<grocy_url>/api/stock/products/{{ $json.product_id }}/add",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"amount\":{{$json.quantity}},\n  \"price\":{{$json.price}},\n  \"transaction_type\":\"purchase\",\n  \"purchased_date\":\"{{$json.purchase_date}}\",\n  \"best_before_date\":\"{{$json.best_before_date}}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1820,
        -55
      ],
      "id": "a5597ab9-464e-405e-b9ce-4e6b13f83020",
      "name": "Add Stock - Existing Product",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "/**\n * Grocery-receipt summary (Markdown, no tables)\n * --------------------------------------------\n * \u2013 Handles BOTH paths: new products & stock-only items\n * \u2013 Computes running cost totals\n * \u2013 Bullet-list output that any MD renderer can show\n */\n\n/* helpers ---------------------------------------------------------- */\n\nconst items = $input.all().map(i => i.json);          // everything that just ran\n\nconst products = $(\"grocy_products\").all().map(x => x.json);\nconst productName = id => products.find(p => p.id === id)?.name ?? `#${id}`;\n\nconst fmtDate = d => d ?? \"\u2014\";\nconst money   = v => (v ?? 0).toFixed(2);\n\n/* build the list --------------------------------------------------- */\n\nlet grandTotal = 0;\n\nconst lines = items.map((it, idx) => {\n  const name = it.action === \"create_product\" ? it.name : productName(it.product_id);\n  const cost = (it.price ?? 0) * (it.amount ?? 1);\n  grandTotal += cost;\n\n  return (\n    `- **${idx + 1}. ${name}** \u2014 ` +\n    (it.action === \"create_product\" ? \"created & \" : \"\") +\n    `added \\`${it.amount}\\` @\u20ac${money(it.price)} ` +\n    `(BB: ${fmtDate(it.best_before_date)}) \u2192 \u20ac${money(cost)}`\n  );\n});\n\n/* stitch together -------------------------------------------------- */\n\nconst summary_md = [\n  \"### Grocery Receipt Import\",\n  \"\",\n  ...lines,\n  \"\",\n  `**Total cost:** \u20ac${money(grandTotal)}`\n].join(\"\\n\");\n\n/* return for the rest of the flow ---------------------------------- */\n\nreturn [{\n  json: {\n    timestamp: new Date().toISOString(),\n    total_items : items.length,\n    total_cost  : grandTotal,\n    items,            // raw data\n    summary_md        // pretty report\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2040,
        -180
      ],
      "id": "17c20991-f770-4acc-ae9f-6dd5bcb56e92",
      "name": "Create Summary"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://<ntfy_url>/n8n-workflows",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Markdown",
              "value": "true"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/markdown",
        "body": "={{ $json.summary_md }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2260,
        -180
      ],
      "id": "59e6164a-409b-4f4b-bc33-533abb6e4766",
      "name": "ntfy",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        },
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "HTTP Webhook": {
      "main": [
        [
          {
            "node": "grocy_products",
            "type": "main",
            "index": 0
          },
          {
            "node": "Edit Image",
            "type": "main",
            "index": 0
          },
          {
            "node": "grocy_locations",
            "type": "main",
            "index": 0
          },
          {
            "node": "grocy_product_groups",
            "type": "main",
            "index": 0
          },
          {
            "node": "grocy_stores",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Receipt Items": {
      "main": [
        [
          {
            "node": "Check Action Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Action Type": {
      "main": [
        [
          {
            "node": "Create New Product",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge1",
            "type": "main",
            "index": 1
          }
        ],
        [
          {
            "node": "Add Stock - Existing Product",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create New Product": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "grocy_products": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "parsed_receipt": {
      "main": [
        [
          {
            "node": "Process Receipt Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "parsed_receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Image": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "grocy_locations": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "grocy_product_groups": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "grocy_stores": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "Add Stock - New Product",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Stock - New Product": {
      "main": [
        [
          {
            "node": "Create Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Summary": {
      "main": [
        [
          {
            "node": "ntfy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Stock - Existing Product": {
      "main": [
        [
          {
            "node": "Create Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "1a1aaafe-6b62-480f-9e5c-1bccf5c03b14",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "e06H4PHCZuZtwLKQ",
  "tags": []
}