{
  "name": "Trion | Quote REVIEW starter (prepared example only)",
  "nodes": [
    {
      "parameters": {},
      "id": "74877cf6-ea42-42fa-bc2f-9c210b4d8801",
      "name": "Run prepared example",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        260,
        300
      ],
      "notes": "Manual execution only. This starter does not read an inbox or send messages.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"sampleOnly\": true,\n  \"email\": {\n    \"messageId\": \"SAMPLE-MSG-1042\",\n    \"from\": \"quotes@pioneer-office.example\",\n    \"subject\": \"Re: PR-1042 | Desk equipment quotation\",\n    \"receivedAt\": \"2026-10-09T09:18:00Z\",\n    \"body\": \"Prepared sample: 24 monitor arms at USD 58 each; 6 USB-C docking stations at USD 132 each; shipping USD 72; tax excluded. Delivery by 2026-10-16. Payment 100% before dispatch. Warranty not stated.\"\n  },\n  \"purchaseRequest\": {\n    \"id\": \"SAMPLE-PR-1042\",\n    \"requester\": \"Sam Rivera\",\n    \"currency\": \"USD\",\n    \"budgetBeforeTax\": 2400,\n    \"neededBy\": \"2026-10-16\",\n    \"requestedPaymentTerms\": \"Net 30\",\n    \"minimumWarrantyMonths\": 12,\n    \"approvalOwner\": \"Jordan Lee\",\n    \"items\": [\n      {\n        \"sku\": \"ARM-VESA\",\n        \"description\": \"Adjustable monitor arm\",\n        \"quantity\": 24\n      },\n      {\n        \"sku\": \"DOCK-USBC\",\n        \"description\": \"USB-C docking station\",\n        \"quantity\": 6\n      }\n    ]\n  },\n  \"quotation\": {\n    \"id\": \"SAMPLE-Q-2781\",\n    \"preparedStructuredSample\": true,\n    \"supplier\": \"Pioneer Office Systems\",\n    \"currency\": \"USD\",\n    \"validUntil\": \"2026-10-14\",\n    \"deliveryDate\": \"2026-10-16\",\n    \"paymentTerms\": \"100% before dispatch\",\n    \"warrantyMonths\": null,\n    \"shipping\": 72,\n    \"tax\": null,\n    \"taxIncluded\": false,\n    \"items\": [\n      {\n        \"sku\": \"ARM-VESA\",\n        \"description\": \"Adjustable monitor arm\",\n        \"quantity\": 24,\n        \"unitPrice\": 58\n      },\n      {\n        \"sku\": \"DOCK-USBC\",\n        \"description\": \"USB-C docking station\",\n        \"quantity\": 6,\n        \"unitPrice\": 132\n      }\n    ]\n  }\n}",
        "options": {}
      },
      "id": "74877cf6-ea42-42fa-bc2f-9c210b4d8802",
      "name": "Prepared email and request",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        520,
        300
      ],
      "notes": "Fictional email plus an already structured quotation. No extraction takes place. Replace with validated input only after building your real integration.",
      "notesInFlow": true
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// This starter consumes prepared structured data. It does NOT extract email or PDF content.\nconst roundMoney = (value) => Math.round((value + Number.EPSILON) * 100) / 100;\nconst validMoney = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;\nconst requireText = (value, name) => {\n  if (typeof value !== 'string' || !value.trim()) throw new Error('Missing or invalid ' + name);\n  return value.trim();\n};\nconst validDate = (value) => typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(value) && !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;\n\nreturn $input.all().map((item, index) => {\n  const data = item.json;\n  const request = data.purchaseRequest;\n  const quote = data.quotation;\n  if (!request || !quote) throw new Error('Expected purchaseRequest and quotation objects');\n  const requestId = requireText(request.id, 'purchaseRequest.id');\n  const quotationId = requireText(quote.id, 'quotation.id');\n  const supplier = requireText(quote.supplier, 'quotation.supplier');\n  const currency = requireText(quote.currency, 'quotation.currency');\n  const requestCurrency = requireText(request.currency, 'purchaseRequest.currency');\n  if (!/^[A-Z]{3}$/.test(currency) || !/^[A-Z]{3}$/.test(requestCurrency)) throw new Error('Currencies must be three-letter uppercase codes');\n  if (!validMoney(request.budgetBeforeTax)) throw new Error('budgetBeforeTax must be a finite non-negative number');\n  if (!Array.isArray(quote.items) || !quote.items.length) throw new Error('quotation.items must contain at least one line');\n  if (!Array.isArray(request.items) || !request.items.length) throw new Error('purchaseRequest.items must contain at least one line');\n  const requestedItems = new Map();\n  request.items.forEach((line, lineIndex) => {\n    const sku = requireText(line.sku, 'requested item SKU at line ' + (lineIndex + 1));\n    if (requestedItems.has(sku)) throw new Error('Duplicate requested SKU: ' + sku);\n    if (!Number.isInteger(line.quantity) || line.quantity <= 0) throw new Error('Requested quantities must be positive integers');\n    requestedItems.set(sku, line);\n  });\n  const seen = new Set();\n  const lines = quote.items.map((line, lineIndex) => {\n    const sku = requireText(line.sku, 'quoted item SKU at line ' + (lineIndex + 1));\n    if (seen.has(sku)) throw new Error('Duplicate quoted SKU: ' + sku);\n    seen.add(sku);\n    if (!Number.isInteger(line.quantity) || line.quantity <= 0) throw new Error('Quoted quantities must be positive integers');\n    if (!validMoney(line.unitPrice)) throw new Error('Unit prices must be finite non-negative numbers');\n    const description = requireText(line.description, 'quoted item description');\n    const lineTotal = roundMoney(line.quantity * line.unitPrice);\n    if (!Number.isFinite(lineTotal)) throw new Error('Line total exceeds a valid numeric range');\n    return { sku, description, quantity: line.quantity, unitPrice: line.unitPrice, lineTotal };\n  });\n  const reasons = [];\n  const addReason = (code, detail) => reasons.push({ code, detail });\n  lines.forEach((line) => {\n    const requested = requestedItems.get(line.sku);\n    if (!requested) addReason('UNREQUESTED_ITEM', line.sku + ' is not in the purchase request');\n    else if (line.quantity !== requested.quantity) addReason('QUANTITY_MISMATCH', line.sku + ': requested ' + requested.quantity + ', quoted ' + line.quantity);\n  });\n  requestedItems.forEach((_, sku) => { if (!seen.has(sku)) addReason('MISSING_ITEM', sku + ' is absent from the quotation'); });\n  const subtotal = roundMoney(lines.reduce((sum, line) => sum + line.lineTotal, 0));\n  if (!Number.isFinite(subtotal)) throw new Error('Subtotal exceeds a valid numeric range');\n  const shippingKnown = quote.shipping !== null && quote.shipping !== undefined;\n  if (shippingKnown && !validMoney(quote.shipping)) throw new Error('shipping must be a finite non-negative number or null');\n  const taxKnown = quote.tax !== null && quote.tax !== undefined;\n  if (taxKnown && !validMoney(quote.tax)) throw new Error('tax must be a finite non-negative number or null');\n  const totalBeforeTax = shippingKnown ? roundMoney(subtotal + quote.shipping) : null;\n  if (totalBeforeTax !== null && !Number.isFinite(totalBeforeTax)) throw new Error('Total exceeds a valid numeric range');\n  const comparable = currency === requestCurrency;\n  const headroom = comparable && totalBeforeTax !== null ? roundMoney(request.budgetBeforeTax - totalBeforeTax) : null;\n  if (!comparable) addReason('CURRENCY_MISMATCH', 'A verified currency conversion is required before budget comparison');\n  if (!shippingKnown) addReason('SHIPPING_UNKNOWN', 'Shipping is unknown, so a complete total before tax cannot be reported');\n  if (headroom !== null && headroom < 0) addReason('OVER_BUDGET', 'Known total before tax exceeds the request budget');\n  if (!taxKnown) addReason('TAX_UNKNOWN', 'Tax treatment and the final invoice total need confirmation');\n  if (quote.taxIncluded !== false) addReason('TAX_BASIS_REVIEW', 'This starter expects unit prices before tax; review the tax basis');\n  if (!quote.paymentTerms || quote.paymentTerms !== request.requestedPaymentTerms) addReason('PAYMENT_TERMS', 'Quoted payment terms differ from the request or are missing');\n  const warrantyKnown = Number.isInteger(quote.warrantyMonths) && quote.warrantyMonths >= 0;\n  if (!Number.isInteger(request.minimumWarrantyMonths) || request.minimumWarrantyMonths < 0) throw new Error('minimumWarrantyMonths must be a non-negative integer');\n  if (!warrantyKnown || quote.warrantyMonths < request.minimumWarrantyMonths) addReason('WARRANTY', 'Warranty is missing, invalid or shorter than requested');\n  if (!validDate(quote.deliveryDate) || !validDate(request.neededBy)) addReason('DELIVERY_DATE', 'Delivery or needed-by date is missing or invalid');\n  else if (quote.deliveryDate > request.neededBy) addReason('LATE_DELIVERY', 'Quoted delivery is later than the needed-by date');\n  if (!validDate(quote.validUntil)) addReason('QUOTE_VALIDITY', 'Quotation validity needs confirmation');\n  else if (quote.validUntil < new Date().toISOString().slice(0, 10)) addReason('QUOTE_EXPIRED', 'Quote validity date has passed; obtain a current quotation');\n  return {\n    json: {\n      recordType: 'QUOTE_REVIEW',\n      status: 'REVIEW',\n      sampleOnly: data.sampleOnly === true,\n      requestId, quotationId, supplier, currency,\n      sourceMessageId: data.email?.messageId || null,\n      structuredInputIsPreparedSample: quote.preparedStructuredSample === true,\n      createdAt: new Date().toISOString(),\n      lines,\n      cost: { subtotal, shipping: shippingKnown ? quote.shipping : null, totalBeforeTax, tax: taxKnown ? quote.tax : null, budgetBeforeTax: request.budgetBeforeTax, budgetCurrency: requestCurrency, budgetHeadroomBeforeTax: headroom },\n      terms: { payment: quote.paymentTerms || null, warrantyMonths: warrantyKnown ? quote.warrantyMonths : null, deliveryDate: quote.deliveryDate || null, validUntil: quote.validUntil || null },\n      reviewReasons: reasons,\n      approvalOwner: request.approvalOwner || null,\n      nextAction: 'A person must validate source evidence, resolve gaps and record the approval decision.',\n      limitations: ['No email or attachment extraction', 'No credentials or live integrations', 'No automatic approval', 'No messages, orders or payments are sent']\n    },\n    pairedItem: { item: index }\n  };\n});"
      },
      "id": "74877cf6-ea42-42fa-bc2f-9c210b4d8803",
      "name": "Build REVIEW record",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        800,
        300
      ],
      "notes": "Always returns REVIEW. Calculates known costs and flags gaps. It does not approve an order, contact a supplier or make a payment.",
      "notesInFlow": true
    }
  ],
  "connections": {
    "Run prepared example": {
      "main": [
        [
          {
            "node": "Prepared email and request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepared email and request": {
      "main": [
        [
          {
            "node": "Build REVIEW record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "tags": []
}
