{
  "name": "Sunday Grocery Assistant v3",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "grocery-assistant",
        "options": {
          "responseMode": "onReceived"
        }
      },
      "id": "webhook",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [0, 300],
      "webhookId": "grocery-assistant"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://nominatim.openstreetmap.org/reverse?format=json&lat={{ $json.latitude }}&lon={{ $json.longitude }}",
        "options": {
          "timeout": 10000
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "OpenClaw-GroceryAssistant/1.0 (brandon@disorganized.net)"
            }
          ]
        }
      },
      "id": "geocode",
      "name": "Geocode Location",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [220, 300]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://nominatim.openstreetmap.org/search?format=json&q=grocery+store&viewbox={{ $('Webhook').item.json.longitude - 0.05 }},{{ $('Webhook').item.json.latitude + 0.05 }},{{ $('Webhook').item.json.longitude + 0.05 }},{{ $('Webhook').item.json.latitude - 0.05 }}&bounded=1&limit=10",
        "options": {
          "timeout": 15000
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "OpenClaw-GroceryAssistant/1.0 (brandon@disorganized.net)"
            }
          ]
        }
      },
      "id": "find-stores",
      "name": "Find Nearby Stores",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [440, 300]
    },
    {
      "parameters": {
        "jsCode": "// Build the store selection payload for Pantry\nconst webhookData = $('Webhook').item.json;\nconst geocodeData = $('Geocode Location').item.json;\nconst storesData = $('Find Nearby Stores').item.json;\n\n// Parse address from geocode\nconst address = geocodeData.address || {};\nconst city = address.city || address.town || address.village || 'Unknown';\nconst state = address.state || 'Unknown';\n\n// Process found stores or use defaults for the area\nlet nearbyStores = [];\n\nif (Array.isArray(storesData) && storesData.length > 0) {\n  nearbyStores = storesData.map(store => {\n    const lat1 = webhookData.latitude;\n    const lon1 = webhookData.longitude;\n    const lat2 = parseFloat(store.lat);\n    const lon2 = parseFloat(store.lon);\n    \n    // Haversine distance\n    const R = 3959; // miles\n    const dLat = (lat2 - lat1) * Math.PI / 180;\n    const dLon = (lon2 - lon1) * Math.PI / 180;\n    const a = Math.sin(dLat/2) * Math.sin(dLat/2) +\n              Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *\n              Math.sin(dLon/2) * Math.sin(dLon/2);\n    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n    const distance = R * c;\n    \n    // Determine store type\n    let type = 'grocery';\n    const name = store.display_name.toLowerCase();\n    if (name.includes('sprouts')) type = 'natural_grocery';\n    else if (name.includes('whole foods')) type = 'premium_natural';\n    else if (name.includes('trader joe')) type = 'specialty';\n    else if (name.includes('natural grocers')) type = 'natural_grocery';\n    else if (name.includes('walmart')) type = 'discount';\n    else if (name.includes('costco')) type = 'warehouse';\n    \n    return {\n      name: store.display_name.split(',')[0],\n      address: store.display_name,\n      distance_miles: Math.round(distance * 10) / 10,\n      type: type\n    };\n  });\n} else {\n  // Default stores for Albuquerque area if search fails\n  nearbyStores = [\n    { name: \"Sprouts Farmers Market\", address: \"Albuquerque, NM\", distance_miles: 2.0, type: \"natural_grocery\" },\n    { name: \"Smith's\", address: \"Albuquerque, NM\", distance_miles: 1.5, type: \"traditional_grocery\" },\n    { name: \"Whole Foods Market\", address: \"Albuquerque, NM\", distance_miles: 3.0, type: \"premium_natural\" },\n    { name: \"Trader Joe's\", address: \"Albuquerque, NM\", distance_miles: 4.0, type: \"specialty\" },\n    { name: \"Natural Grocers\", address: \"Albuquerque, NM\", distance_miles: 2.5, type: \"natural_grocery\" }\n  ];\n}\n\n// Build payload\nconst payload = {\n  step: \"store_selection\",\n  location: {\n    latitude: webhookData.latitude,\n    longitude: webhookData.longitude,\n    city: city,\n    state: state\n  },\n  nearbyStores: nearbyStores.slice(0, 8),\n  budget: webhookData.budget || 80,\n  preferences: {\n    healthyWeight: 0.6,\n    budgetWeight: 0.4\n  }\n};\n\nreturn { json: payload };"
      },
      "id": "prepare-store-selection",
      "name": "Prepare Store Selection",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [660, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://192.168.7.6:18789/hooks/agent",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 300000
        }
      },
      "id": "pantry-store-selection",
      "name": "Pantry: Store Selection",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [880, 300],
      "credentials": {
        "httpHeaderAuth": {
          "id": "openclaw-webhook",
          "name": "OpenClaw Webhook"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Build the deal research payload\nconst storeResult = $('Pantry: Store Selection').item.json;\nconst webhookData = $('Webhook').item.json;\n\n// Get today's date for the week\nconst today = new Date();\nconst weekOf = today.toISOString().split('T')[0];\n\nconst payload = {\n  step: \"deal_research\",\n  store: storeResult.selectedStore || {\n    name: \"Sprouts Farmers Market\",\n    city: \"Albuquerque\",\n    state: \"NM\"\n  },\n  weekOf: weekOf,\n  budget: webhookData.budget || 80,\n  researchSources: [\n    \"store_website\",\n    \"flipp\",\n    \"instacart\",\n    \"brave_search\",\n    \"manufacturer_coupons\"\n  ],\n  preferences: {\n    dietary: [\"organic-preferred\", \"healthy\"],\n    avoid: [\"highly-processed\", \"artificial-sweeteners\"]\n  }\n};\n\nreturn { json: payload };"
      },
      "id": "prepare-deal-research",
      "name": "Prepare Deal Research",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1100, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://192.168.7.6:18789/hooks/agent",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 900000
        }
      },
      "id": "pantry-deal-research",
      "name": "Pantry: Deal Research",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1320, 300],
      "credentials": {
        "httpHeaderAuth": {
          "id": "openclaw-webhook",
          "name": "OpenClaw Webhook"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Build the recipe matching payload\nconst dealResult = $('Pantry: Deal Research').item.json;\nconst storeResult = $('Pantry: Store Selection').item.json;\nconst webhookData = $('Webhook').item.json;\n\n// Calculate week dates\nconst today = new Date();\nconst endDate = new Date(today);\nendDate.setDate(endDate.getDate() + 6);\n\nconst payload = {\n  step: \"recipe_matching\",\n  store: storeResult.selectedStore?.name || \"Sprouts Farmers Market\",\n  deals: dealResult.deals || [],\n  coupons: dealResult.coupons || [],\n  budget: webhookData.budget || 80,\n  budgetBuffer: 0.10,\n  targetBudget: (webhookData.budget || 80) * 0.90,\n  mealieApiUrl: \"https://mealie.disorganized.net\",\n  weekDates: {\n    start: today.toISOString().split('T')[0],\n    end: endDate.toISOString().split('T')[0]\n  },\n  preferences: {\n    mealsPerDay: 1,\n    servingsPerMeal: 2,\n    allowLeftovers: true,\n    maxPrepTimeMinutes: 60,\n    dietary: [\"organic-preferred\", \"healthy\"]\n  },\n  attempt: 1\n};\n\nreturn { json: payload };"
      },
      "id": "prepare-recipe-matching",
      "name": "Prepare Recipe Matching",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1540, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://192.168.7.6:18789/hooks/agent",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        }
      },
      "id": "pantry-recipe-matching",
      "name": "Pantry: Recipe Matching",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1760, 300],
      "credentials": {
        "httpHeaderAuth": {
          "id": "openclaw-webhook",
          "name": "OpenClaw Webhook"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// QA Budget Check\nconst result = $('Pantry: Recipe Matching').item.json;\nconst webhookData = $('Webhook').item.json;\nconst budget = webhookData.budget || 80;\nconst maxRetries = 5;\nconst currentAttempt = result.attempt || $('Prepare Recipe Matching').item.json?.attempt || 1;\n\nconst estimatedTotal = result.budgetSummary?.estimatedTotal || result.totalCost || 0;\n\nif (estimatedTotal <= budget) {\n  return {\n    json: {\n      status: \"passed\",\n      proceed: true,\n      result: result,\n      message: `Budget check passed: $${estimatedTotal.toFixed(2)} <= $${budget}`\n    }\n  };\n} else if (currentAttempt < maxRetries) {\n  return {\n    json: {\n      status: \"over_budget\",\n      proceed: false,\n      retry: true,\n      attempt: currentAttempt + 1,\n      feedback: {\n        issue: \"over_budget\",\n        overBy: estimatedTotal - budget,\n        suggestion: \"Substitute expensive proteins or remove one meal\",\n        previousTotal: estimatedTotal\n      },\n      previousResult: result\n    }\n  };\n} else {\n  return {\n    json: {\n      status: \"max_retries_reached\",\n      proceed: true,\n      warning: `After ${maxRetries} attempts, still over budget by $${(estimatedTotal - budget).toFixed(2)}. Proceeding with best effort.`,\n      result: result\n    }\n  };\n}"
      },
      "id": "qa-budget-check",
      "name": "QA Budget Check",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1980, 300]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.proceed }}",
              "value2": true
            }
          ]
        }
      },
      "id": "check-proceed",
      "name": "Check Proceed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [2200, 300]
    },
    {
      "parameters": {
        "jsCode": "// Prepare retry payload\nconst qaResult = $('QA Budget Check').item.json;\nconst dealResult = $('Pantry: Deal Research').item.json;\nconst storeResult = $('Pantry: Store Selection').item.json;\nconst webhookData = $('Webhook').item.json;\n\nconst today = new Date();\nconst endDate = new Date(today);\nendDate.setDate(endDate.getDate() + 6);\n\nconst payload = {\n  step: \"recipe_matching_retry\",\n  attempt: qaResult.attempt,\n  previousResult: qaResult.previousResult,\n  feedback: qaResult.feedback,\n  store: storeResult.selectedStore?.name || \"Sprouts Farmers Market\",\n  deals: dealResult.deals || [],\n  coupons: dealResult.coupons || [],\n  budget: webhookData.budget || 80,\n  targetBudget: (webhookData.budget || 80) * 0.90,\n  weekDates: {\n    start: today.toISOString().split('T')[0],\n    end: endDate.toISOString().split('T')[0]\n  }\n};\n\nreturn { json: payload };"
      },
      "id": "prepare-retry",
      "name": "Prepare Retry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2420, 500]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://192.168.7.6:18789/hooks/agent",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 600000
        }
      },
      "id": "pantry-retry",
      "name": "Pantry: Retry",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [2640, 500],
      "credentials": {
        "httpHeaderAuth": {
          "id": "openclaw-webhook",
          "name": "OpenClaw Webhook"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Format final message for Signal\nconst qaResult = $('QA Budget Check').item.json;\nconst result = qaResult.result;\nconst storeResult = $('Pantry: Store Selection').item.json;\nconst warning = qaResult.warning || null;\n\nconst store = storeResult.selectedStore || { name: 'Local Grocery', address: 'N/A' };\n\n// Format meal plan\nlet mealPlanText = '';\nif (result.mealPlan && Array.isArray(result.mealPlan)) {\n  mealPlanText = result.mealPlan.map(m => {\n    const day = m.dayOfWeek || m.date;\n    const meal = m.recipe?.name || m.title || 'TBD';\n    return `• ${day}: ${meal}`;\n  }).join('\\n');\n} else {\n  mealPlanText = '• Meal plan details in Mealie';\n}\n\n// Format budget\nconst subtotal = result.budgetSummary?.subtotal || result.totalCost || 0;\nconst total = result.budgetSummary?.estimatedTotal || subtotal;\nconst savings = result.budgetSummary?.totalSavingsFromDeals || 0;\nconst couponSavings = result.budgetSummary?.totalSavingsFromCoupons || 0;\n\n// Format coupons\nlet couponsText = '';\nif (result.budgetSummary?.couponsApplied?.length > 0) {\n  couponsText = '\\n\\n🏷️ COUPONS TO USE\\n';\n  couponsText += result.budgetSummary.couponsApplied.map(c => \n    `• ${c.brand || 'Store'}: ${c.discount || '$' + c.savings + ' off'}`\n  ).join('\\n');\n}\n\n// Warning if over budget\nlet warningText = '';\nif (warning) {\n  warningText = `\\n\\n⚠️ ${warning}`;\n}\n\nconst message = `🛒 Weekly Grocery Plan Ready!\n\n📍 STORE: ${store.name}\n   ${store.address} (${store.distance_miles || '?'} mi)\n\n📅 MEAL PLAN\n${mealPlanText}\n\n💰 BUDGET\nSubtotal: $${subtotal.toFixed(2)}\nDeal Savings: -$${savings.toFixed(2)}\nCoupon Savings: -$${couponSavings.toFixed(2)}\nTOTAL: $${total.toFixed(2)} ${total <= 80 ? '✅' : '⚠️'}${couponsText}${warningText}\n\n📱 Full list in Mealie: \"${result.shoppingList?.mealieName || 'Weekly Groceries'}\"\\n\\n💡 ${result.strategy || 'Happy shopping!'}`;\n\nreturn { json: { message: message, store: store, result: result } };"
      },
      "id": "format-message",
      "name": "Format Signal Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2420, 100]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://192.168.7.6:18789/hooks/agent",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ step: 'send_signal', message: $json.message, channel: 'signal' }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "send-signal",
      "name": "Send Signal Message",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [2640, 100],
      "credentials": {
        "httpHeaderAuth": {
          "id": "openclaw-webhook",
          "name": "OpenClaw Webhook"
        }
      }
    },
    {
      "parameters": {},
      "id": "end",
      "name": "Done",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [2860, 100]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [{ "node": "Geocode Location", "type": "main", "index": 0 }]
      ]
    },
    "Geocode Location": {
      "main": [
        [{ "node": "Find Nearby Stores", "type": "main", "index": 0 }]
      ]
    },
    "Find Nearby Stores": {
      "main": [
        [{ "node": "Prepare Store Selection", "type": "main", "index": 0 }]
      ]
    },
    "Prepare Store Selection": {
      "main": [
        [{ "node": "Pantry: Store Selection", "type": "main", "index": 0 }]
      ]
    },
    "Pantry: Store Selection": {
      "main": [
        [{ "node": "Prepare Deal Research", "type": "main", "index": 0 }]
      ]
    },
    "Prepare Deal Research": {
      "main": [
        [{ "node": "Pantry: Deal Research", "type": "main", "index": 0 }]
      ]
    },
    "Pantry: Deal Research": {
      "main": [
        [{ "node": "Prepare Recipe Matching", "type": "main", "index": 0 }]
      ]
    },
    "Prepare Recipe Matching": {
      "main": [
        [{ "node": "Pantry: Recipe Matching", "type": "main", "index": 0 }]
      ]
    },
    "Pantry: Recipe Matching": {
      "main": [
        [{ "node": "QA Budget Check", "type": "main", "index": 0 }]
      ]
    },
    "QA Budget Check": {
      "main": [
        [{ "node": "Check Proceed", "type": "main", "index": 0 }]
      ]
    },
    "Check Proceed": {
      "main": [
        [{ "node": "Format Signal Message", "type": "main", "index": 0 }],
        [{ "node": "Prepare Retry", "type": "main", "index": 0 }]
      ]
    },
    "Prepare Retry": {
      "main": [
        [{ "node": "Pantry: Retry", "type": "main", "index": 0 }]
      ]
    },
    "Pantry: Retry": {
      "main": [
        [{ "node": "QA Budget Check", "type": "main", "index": 0 }]
      ]
    },
    "Format Signal Message": {
      "main": [
        [{ "node": "Send Signal Message", "type": "main", "index": 0 }]
      ]
    },
    "Send Signal Message": {
      "main": [
        [{ "node": "Done", "type": "main", "index": 0 }]
      ]
    }
  },
  "settings": {
    "executionTimeout": 7200,
    "saveManualExecutions": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "pinData": {},
  "meta": {
    "instanceId": "openclaw-link"
  }
}
