{
 "name": "Printavo \u2192 MajorDTF DTF print-file automation",
 "nodes": [
  {
   "parameters": {
    "rule": {
     "interval": [
      {
       "field": "seconds"
      }
     ]
    }
   },
   "id": "2c4b6ec2-71b0-451b-b46e-c44f9d133bdd",
   "name": "Schedule - Poll Printavo Ready",
   "type": "n8n-nodes-base.scheduleTrigger",
   "typeVersion": 1.3,
   "position": [
    -1040,
    128
   ]
  },
  {
   "parameters": {
    "jsCode": "const readyStatusId = 'REPLACE_WITH_YOUR_READY_STATUS_ID';\nconst first = 1;\nconst query = `query MajorDtfReadyInvoices($first: Int!, $statusIds: [ID!]) {\n  invoices(first: $first, statusIds: $statusIds, sortOn: VISUAL_ID, sortDescending: true) {\n    nodes {\n      id\n      visualId\n      nickname\n      url\n      status { id name type }\n      timestamps { updatedAt }\n    }\n    pageInfo { hasNextPage endCursor }\n    totalNodes\n  }\n}`;\nreturn [{ json: { status: 'ok', stage: 'prepare_poll', body: { query, variables: { first, statusIds: [readyStatusId] } } } }];"
   },
   "id": "eb0162fb-71e8-48e2-8ebc-6ce3d50637bf",
   "name": "Prepare Printavo Poll Query",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    -800,
    128
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://www.printavo.com/api/v2",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Content-Type",
       "value": "application/json"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{$json.body}}",
    "options": {}
   },
   "id": "70ca71b2-b089-47b3-a28e-9f81421b8657",
   "name": "Printavo - Poll Ready Invoices",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    -560,
    128
   ]
  },
  {
   "parameters": {
    "jsCode": "const payload = $input.first()?.json || {};\nif (payload.errors?.length) {\n  return [{ json: { status: 'error', stage: 'printavo_poll', error: `Printavo GraphQL errors: ${JSON.stringify(payload.errors).slice(0, 1000)}` } }];\n}\nconst nodes = payload.data?.invoices?.nodes || [];\nif (!nodes.length) {\n  return [{ json: { status: 'noop', stage: 'printavo_poll', message: 'No invoices currently in Ready for DTF Processing', totalNodes: payload.data?.invoices?.totalNodes || 0 } }];\n}\nconst invoice = nodes[0];\nconst query = `query MajorDtfInvoice($id: ID!) {\n  invoice(id: $id) {\n    id\n    visualId\n    visualPoNumber\n    nickname\n    totalQuantity\n    url\n    tags\n    status { id name type }\n    contact {\n      id\n      fullName\n      email\n      customer {\n        id\n        companyName\n        primaryContact { fullName email }\n      }\n    }\n    lineItemGroups(first: 10) {\n      nodes {\n        id\n        position\n        lineItems(first: 10) {\n          nodes {\n            id\n            description\n            itemNumber\n            color\n            items\n            sizes { size count }\n            mockups(first: 2) {\n              nodes { id fullImageUrl thumbnailUrl mimeType }\n            }\n          }\n        }\n        imprints(first: 10) {\n          nodes {\n            id\n            details\n            typeOfWork { id name }\n            pricingMatrixColumn { columnName }\n            mockups(first: 3) {\n              nodes { id fullImageUrl thumbnailUrl mimeType }\n            }\n          }\n        }\n      }\n    }\n  }\n}`;\nreturn [{ json: { status: 'ok', trigger: 'printavo_poll', requestedInvoiceId: String(invoice.id), poll: { readyInvoice: invoice, statusId: invoice.status?.id, statusName: invoice.status?.name }, body: { query, variables: { id: String(invoice.id) } } } }];"
   },
   "id": "6ddffcd4-22e7-4d11-a931-fb605d06659b",
   "name": "Select Ready Invoice",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    -272,
    192
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "strict",
      "version": 2
     },
     "conditions": [
      {
       "id": "poll-status-error",
       "leftValue": "={{$json.status}}",
       "rightValue": "error",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "23d0f4d4-fa61-4e86-8fc1-93ea0bf512bd",
   "name": "IF Poll Error",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    -80,
    192
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "strict",
      "version": 2
     },
     "conditions": [
      {
       "id": "poll-status-ok",
       "leftValue": "={{$json.status}}",
       "rightValue": "ok",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "2feddbe4-6844-42d4-b3b6-d16dad831af1",
   "name": "IF Poll Has Invoice",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    160,
    208
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://www.printavo.com/api/v2",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Content-Type",
       "value": "application/json"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{$json.body}}",
    "options": {}
   },
   "id": "e85cfe67-45f0-45a0-8e83-85ad079a0c12",
   "name": "Printavo - Fetch Invoice Detail",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    448,
    192
   ]
  },
  {
   "parameters": {
    "jsCode": "const payload = $input.first()?.json || {};\nconst selector = $('Select Ready Invoice').first().json;\nconst invoiceId = selector.requestedInvoiceId || 'unknown';\nif (payload.errors?.length) {\n  return [{ json: { status: 'error', stage: 'printavo_fetch', requestedInvoiceId: invoiceId, error: `Printavo GraphQL errors: ${JSON.stringify(payload.errors).slice(0, 1000)}`, poll: selector.poll } }];\n}\nif (!payload.data?.invoice) {\n  return [{ json: { status: 'error', stage: 'printavo_fetch', requestedInvoiceId: invoiceId, error: 'Printavo invoice detail query returned no invoice', poll: selector.poll } }];\n}\nreturn [{ json: { status: 'ok', trigger: selector.trigger || 'printavo_poll', requestedInvoiceId: invoiceId, poll: selector.poll, printavo: payload } }];"
   },
   "id": "f8ebeba4-ebc4-42c0-90f4-449bb5a1a03d",
   "name": "Wrap Printavo Invoice Detail",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    640,
    224
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "strict",
      "version": 2
     },
     "conditions": [
      {
       "id": "status-is-error",
       "leftValue": "={{$json.status}}",
       "rightValue": "error",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "6c9196ca-5bd4-45a5-a89f-17dc2b1a9f34",
   "name": "IF Fetch Error",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    -560,
    -16
   ]
  },
  {
   "parameters": {
    "fromEmail": "=you@yourshop.com",
    "toEmail": "=you@yourshop.com",
    "subject": "={{'MajorDTF Printavo automation failed: ' + ($json.stage || 'unknown')}}",
    "emailFormat": "text",
    "text": "={{'Stage: ' + ($json.stage || 'unknown') + '\\nInvoice: ' + ($json.requestedInvoiceId || $json.invoiceId || 'unknown') + '\\nError: ' + ($json.error || 'unknown') + '\\n\\nPayload:\\n' + JSON.stringify($json, null, 2)}}",
    "options": {
     "appendAttribution": false
    }
   },
   "id": "88eb7e6f-886a-4e32-bcd9-74efe2ede3d6",
   "name": "Send Failure Email",
   "type": "n8n-nodes-base.emailSend",
   "typeVersion": 2.1,
   "position": [
    -32,
    -208
   ],
   "disabled": true
  },
  {
   "parameters": {
    "jsCode": "const source = $input.first().json;\nconst payload = source.printavo;\nconst invoice = payload?.data?.invoice;\nconst DEFAULT_PRINT_WIDTH_IN = 11;\n\nfunction connNodes(conn) {\n  return conn?.nodes || (conn?.edges || []).map(edge => edge.node).filter(Boolean) || [];\n}\n\nfunction cleanSegment(value) {\n  return String(value || '')\n    .toLowerCase()\n    .replace(/&/g, 'and')\n    .replace(/[^a-z0-9]+/g, '-')\n    .replace(/^-+|-+$/g, '')\n    .slice(0, 60);\n}\n\nfunction widthToken(widthIn) {\n  const n = Number(widthIn);\n  if (Number.isInteger(n)) return String(n);\n  return String(n).replace(/\\.0+$/, '').replace(/\\.5$/, 'p5').replace('.', 'p');\n}\n\nfunction filepickerHandle(url) {\n  const matches = String(url || '').matchAll(/cdn\\.filepicker\\.io\\/([^/?#]+)/g);\n  const handles = Array.from(matches, m => m[1]).filter(Boolean);\n  return handles.length ? handles[handles.length - 1] : '';\n}\n\nfunction mockupKeys(mockup) {\n  const keys = new Set();\n  if (mockup?.id) keys.add(`id:${mockup.id}`);\n  const fullHandle = filepickerHandle(mockup?.fullImageUrl);\n  const thumbHandle = filepickerHandle(mockup?.thumbnailUrl);\n  if (fullHandle) keys.add(`handle:${fullHandle}`);\n  if (thumbHandle) keys.add(`handle:${thumbHandle}`);\n  return keys;\n}\n\nfunction parseWidth(text) {\n  const raw = String(text || '').replace(/[??]/g, '\"');\n  const normalized = raw.replace(/p5w\\b/ig, '.5w');\n  const patterns = [\n    /(?:^|\\b)(\\d+(?:\\.\\d+)?)\\s*(?:w|wide|in\\s*wide|inch(?:es)?\\s*wide)\\b/i,\n    /\\bwidth\\s*[:=]?\\s*(\\d+(?:\\.\\d+)?)/i,\n    /\\b(\\d+(?:\\.\\d+)?)\\s*(?:in|inch|inches)\\s*(?:wide|w)\\b/i,\n    /\\b(?:front|back|ls|rs)-(\\d+(?:\\.\\d+)?)w\\b/i,\n  ];\n  for (const re of patterns) {\n    const m = normalized.match(re);\n    if (m) return Number(m[1]);\n  }\n  return null;\n}\n\nfunction parseLocation(imprint, group) {\n  const search = `${imprint?.details || ''} ${imprint?.typeOfWork?.name || ''}`;\n  const placement = String(search || '').toLowerCase().match(/\\b(front|back|ls|rs)-\\d+(?:p5|\\.5|(?:\\.\\d+)?)?w\\b/);\n  if (placement) return placement[1];\n\n  const details = String(imprint?.details || '');\n  const withoutSize = details\n    .replace(/\\b(?:front|back|ls|rs)-\\d+(?:p5|\\.5|(?:\\.\\d+)?)?w\\b/ig, '')\n    .replace(/\\b\\d+(?:\\.\\d+)?\\s*(?:w|wide|in\\s*wide|inch(?:es)?\\s*wide)\\b/ig, '')\n    .replace(/\\bwidth\\s*[:=]?\\s*\\d+(?:\\.\\d+)?/ig, '')\n    .split(/[;|,\\n]/)[0];\n  const detailLocation = cleanSegment(withoutSize);\n  if (detailLocation) return detailLocation;\n\n  const typeName = cleanSegment(imprint?.typeOfWork?.name);\n  if (typeName && !['dtf', 'screen-printing', 'print'].includes(typeName)) return typeName;\n\n  return `imprint-${Number(group?.position ?? 0) + 1}`;\n}\n\n// Printavo's pricing-matrix column name (e.g. \"front-4w\", \"front-2p5w\") is the\n// authoritative print location + width when present -- it's a structured field,\n// not free text. Only fall back to parsing imprint.details when it's missing/unrecognized.\nfunction parseMatrixColumn(columnName) {\n  const m = String(columnName || '').trim().match(/^(front|back|ls|rs)-(\\d+(?:p5)?)w$/i);\n  if (!m) return null;\n  const token = m[2].toLowerCase();\n  const widthIn = token.endsWith('p5') ? Number(token.slice(0, -2)) + 0.5 : Number(token);\n  if (!widthIn) return null;\n  return { location: m[1].toLowerCase(), widthIn };\n}\n\nfunction qtyForGroup(group) {\n  const lineItems = connNodes(group.lineItems);\n  let total = 0;\n  for (const li of lineItems) {\n    if (Number.isFinite(Number(li.items))) total += Number(li.items);\n    else if (Array.isArray(li.sizes)) total += li.sizes.reduce((sum, s) => sum + (Number(s.count) || 0), 0);\n  }\n  return total || null;\n}\n\nconst errors = [];\nconst warnings = [];\nif (!invoice) errors.push('Printavo response did not include data.invoice');\n\nconst visualId = invoice?.visualId ? String(invoice.visualId) : '';\nif (!visualId) errors.push('Invoice missing visualId');\n\nconst contact = invoice?.contact || {};\nconst customer = contact.customer || {};\nconst clientName = customer.companyName || contact.fullName || customer.primaryContact?.fullName || contact.email || (customer.id ? `customer-${customer.id}` : 'unknown-client');\nif (!clientName || clientName === 'unknown-client') errors.push('Invoice missing client/customer name');\n\nconst folderName = `${visualId || source.requestedInvoiceId} - ${String(clientName).replace(/[\\\\/:*?\"<>|]+/g, '').trim()}`;\nconst jobs = [];\nconst skippedDuplicateMockups = [];\nconst fileCounts = new Map();\nconst groups = connNodes(invoice?.lineItemGroups);\n\nconst lineItemMockupKeys = new Set();\nfor (const group of groups) {\n  for (const lineItem of connNodes(group.lineItems)) {\n    for (const mockup of connNodes(lineItem.mockups)) {\n      for (const key of mockupKeys(mockup)) lineItemMockupKeys.add(key);\n    }\n  }\n}\nif (lineItemMockupKeys.size) warnings.push(`Detected ${lineItemMockupKeys.size} line-item mockup file key(s) for duplicate filtering`);\n\nfunction isLineItemDuplicate(mockup) {\n  for (const key of mockupKeys(mockup)) {\n    if (lineItemMockupKeys.has(key)) return true;\n  }\n  return false;\n}\n\nfor (const group of groups) {\n  const quantity = qtyForGroup(group);\n  if (!quantity) {\n    errors.push(`Line item group ${group.id} has no usable quantity`);\n    continue;\n  }\n\n  const imprints = connNodes(group.imprints);\n  if (!imprints.length) {\n    warnings.push(`Line item group ${group.id} has no imprints`);\n    continue;\n  }\n\n  for (const imprint of imprints) {\n    const details = imprint.details || '';\n    const matrixParsed = parseMatrixColumn(imprint.pricingMatrixColumn?.columnName);\n\n    let widthIn, widthSource, location;\n    if (matrixParsed) {\n      widthIn = matrixParsed.widthIn;\n      widthSource = 'matrix';\n      location = matrixParsed.location;\n    } else {\n      const parsedWidthIn = parseWidth(`${details} ${imprint.typeOfWork?.name || ''}`);\n      widthIn = parsedWidthIn || DEFAULT_PRINT_WIDTH_IN;\n      widthSource = parsedWidthIn ? 'printavo' : 'default';\n      if (!parsedWidthIn) warnings.push(`Imprint ${imprint.id} missing parseable width in matrix column or details; using default ${DEFAULT_PRINT_WIDTH_IN}in`);\n\n      location = parseLocation(imprint, group);\n      if (!location) {\n        errors.push(`Imprint ${imprint.id} missing parseable location/typeOfWork`);\n        continue;\n      }\n    }\n\n    const allMockups = connNodes(imprint.mockups).filter(m => m.fullImageUrl);\n    if (!allMockups.length) {\n      errors.push(`Imprint ${imprint.id} has no mockup fullImageUrl`);\n      continue;\n    }\n\n    const mockups = [];\n    for (const mockup of allMockups) {\n      if (lineItemMockupKeys.size && isLineItemDuplicate(mockup)) {\n        skippedDuplicateMockups.push({ imprintId: imprint.id, mockupId: mockup.id, handle: filepickerHandle(mockup.fullImageUrl), reason: 'matches_line_item_mockup' });\n        continue;\n      }\n      mockups.push(mockup);\n    }\n    if (!mockups.length) {\n      warnings.push(`Imprint ${imprint.id} only had duplicate line-item mockups; skipped all ${allMockups.length} imprint file(s)`);\n      continue;\n    }\n\n    for (const mockup of mockups) {\n      const base = `${visualId}-${location}-${widthToken(widthIn)}w-${quantity}`;\n      const next = (fileCounts.get(base) || 0) + 1;\n      fileCounts.set(base, next);\n      const suffix = next > 1 ? `-${next}` : '';\n      const fileBase = `${base}${suffix}`;\n      jobs.push({\n        invoiceId: invoice.id,\n        visualId,\n        clientName,\n        folderName,\n        groupId: group.id,\n        imprintId: imprint.id,\n        mockupId: mockup.id,\n        imageUrl: mockup.fullImageUrl,\n        mimeType: mockup.mimeType || '',\n        imprintDetails: details,\n        location,\n        widthIn,\n        widthSource,\n        quantity,\n        fileBase,\n        pngName: `${fileBase}.png`,\n        jhdrName: `${fileBase}.jhdr`\n      });\n    }\n  }\n}\n\nif (skippedDuplicateMockups.length) warnings.push(`Skipped ${skippedDuplicateMockups.length} imprint mockup file(s) because they also appear in line-item mockups`);\nif (!jobs.length) errors.push('No production imprint mockup jobs found after duplicate line-item mockup filtering');\n\nif (errors.length) {\n  return [{ json: { status: 'error', stage: 'normalize_printavo', requestedInvoiceId: source.requestedInvoiceId, invoiceId: invoice?.id, visualId, folderName, error: errors.join('; '), warnings, skippedDuplicateMockups, rawInvoiceUrl: invoice?.url } }];\n}\n\nreturn [{ json: { status: 'ok', stage: 'normalize_printavo', requestedInvoiceId: source.requestedInvoiceId, invoiceId: invoice.id, visualId, clientName, folderName, jobCount: jobs.length, warnings, skippedDuplicateMockups, jobs } }];"
   },
   "id": "27b8de3c-f60e-4879-ace3-18d5d7b2cf2e",
   "name": "Normalize Printavo Jobs",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    -320,
    -16
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "strict",
      "version": 2
     },
     "conditions": [
      {
       "id": "normalize-status-error",
       "leftValue": "={{$json.status}}",
       "rightValue": "error",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "95c1140a-4d69-4513-96e1-72c755c2ef2a",
   "name": "IF Normalize Error",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    -48,
    -16
   ]
  },
  {
   "parameters": {
    "resource": "folder",
    "name": "={{$json.folderName}}",
    "driveId": {
     "__rl": true,
     "value": "My Drive",
     "mode": "list",
     "cachedResultName": "My Drive",
     "cachedResultUrl": "https://drive.google.com/drive/my-drive"
    },
    "folderId": {
     "__rl": true,
     "value": "=REPLACE_WITH_YOUR_DRIVE_PARENT_FOLDER_ID",
     "mode": "id"
    },
    "options": {
     "simplifyOutput": true
    }
   },
   "id": "02b235da-24dc-426f-adc2-7b08bb060afa",
   "name": "Create Drive Folder",
   "type": "n8n-nodes-base.googleDrive",
   "typeVersion": 3,
   "position": [
    208,
    0
   ],
   "onError": "continueErrorOutput"
  },
  {
   "parameters": {
    "jsCode": "const normalize = $('Normalize Printavo Jobs').first().json;\nconst folder = $('Create Drive Folder').first().json;\nconst folderId = folder.id || folder.fileId || folder.webViewLink || '';\nif (!folderId) {\n  return [{ json: { status: 'error', stage: 'drive_folder', requestedInvoiceId: normalize.requestedInvoiceId, visualId: normalize.visualId, error: `Google Drive folder was created but no folder id was returned: ${JSON.stringify(folder).slice(0, 700)}` } }];\n}\nreturn normalize.jobs.map(job => ({ json: { ...job, status: 'ok', folderId } }));"
   },
   "id": "997c8afa-2b81-41ca-95cf-edf3d61835ee",
   "name": "Split Jobs With Folder",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    400,
    0
   ]
  },
  {
   "parameters": {
    "jsCode": "return $input.all().map(item => {\n  const job = item.json;\n  return {\n    json: {\n      ...job,\n      prepRequest: {\n        image_url: job.imageUrl,\n        print_width_in: Number(job.widthIn),\n        fit: 'width',\n        dpi: 300\n      }\n    }\n  };\n});"
   },
   "id": "626f2459-0268-4822-a36c-6b24a9f067cf",
   "name": "Build MajorDTF Prep Requests",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    608,
    0
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://worker.majordtf.com/v1/prep",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpBearerAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Content-Type",
       "value": "application/json"
      },
      {
       "name": "Idempotency-Key",
       "value": "={{$json.fileBase}}"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{$json.prepRequest}}",
    "options": {
     "response": {
      "response": {
       "neverError": true,
       "responseFormat": "json"
      }
     },
     "timeout": 600000
    }
   },
   "id": "9d10f704-b103-4b02-8c4c-7efafe80aed7",
   "name": "MajorDTF - Prep Artwork",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    880,
    0
   ]
  },
  {
   "parameters": {
    "jsCode": "const out = [];\ntry {\n  const downloadedItems = $input.all();\n  const requestItems = $('Build MajorDTF Prep Requests').all();\n  const prepItems = $('MajorDTF - Prep Artwork').all();\n\n  for (let i = 0; i < downloadedItems.length; i++) {\n    const job = requestItems[i]?.json || {};\n    const prep = prepItems[i]?.json || downloadedItems[i]?.json || {};\n    const binary = downloadedItems[i]?.binary?.data;\n\n    if (prep.status === 'error' || prep.error || !prep.file_url) {\n      throw new Error(`MajorDTF prep failed for ${job.fileBase || `item ${i + 1}`}: ${JSON.stringify(prep).slice(0, 1000)}`);\n    }\n    if (!binary?.data) {\n      throw new Error(`Could not download MajorDTF PNG for ${job.fileBase || `item ${i + 1}`}: missing binary data`);\n    }\n\n    const printHeightIn = prep.print_in?.h || (prep.height ? Number(prep.height) / 300 : null);\n    if (!printHeightIn) throw new Error(`MajorDTF response missing height/print_in.h for ${job.fileBase}`);\n\n    const widthPts = (Number(job.widthIn) * 72).toFixed(6);\n    const heightPts = (Number(printHeightIn) * 72).toFixed(6);\n    const jhdr = `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\\n<JHDR>\\n  <PrintMode name=\"Default\" />\\n  <Media name=\"\" />\\n  <Template name=\"\" slot=\"\" />\\n  <Size sizetype=\"0\" width=\"${widthPts}\" height=\"${heightPts}\" />\\n  <Copies>${Number(job.quantity)}</Copies>\\n</JHDR>\\n`;\n\n    const common = {\n      ...job,\n      status: 'ok',\n      majorDtf: {\n        width: prep.width,\n        height: prep.height,\n        print_in: prep.print_in,\n        method: prep.method,\n        quality: prep.quality,\n        credits_charged: prep.credits_charged,\n        balance: prep.balance\n      }\n    };\n\n    out.push({\n      json: { ...common, outputKind: 'png', fileName: job.pngName },\n      binary: {\n        data: {\n          ...binary,\n          mimeType: 'application/octet-stream', // image/* Content-Type triggers filepicker's 1600px downsampler on Printavo attach\n          fileName: job.pngName\n        }\n      }\n    });\n    out.push({\n      json: { ...common, outputKind: 'jhdr', fileName: job.jhdrName, jhdr },\n      binary: {\n        data: {\n          data: Buffer.from(jhdr, 'utf8').toString('base64'),\n          mimeType: 'application/xml',\n          fileName: job.jhdrName\n        }\n      }\n    });\n  }\n\n  return out;\n} catch (err) {\n  const first = $('Build MajorDTF Prep Requests').first()?.json || $input.first()?.json || {};\n  return [{ json: { status: 'error', stage: 'major_dtf_or_jhdr', requestedInvoiceId: first.invoiceId, visualId: first.visualId, error: err.message } }];\n}"
   },
   "id": "9d6bcd8c-bffa-4b07-8154-09b0bc8b18ed",
   "name": "Process MajorDTF and Build Files",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1072,
    240
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "strict",
      "version": 2
     },
     "conditions": [
      {
       "id": "process-status-error",
       "leftValue": "={{$json.status}}",
       "rightValue": "error",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "c9f681f4-e930-4daa-ae31-1c8790e72bca",
   "name": "IF Process Error",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    1248,
    240
   ]
  },
  {
   "parameters": {
    "name": "={{$json.fileName}}",
    "driveId": {
     "__rl": true,
     "mode": "list",
     "value": "My Drive"
    },
    "folderId": {
     "__rl": true,
     "mode": "id",
     "value": "={{$json.folderId}}",
     "cachedResultName": "Invoice Folder"
    },
    "options": {
     "simplifyOutput": true
    }
   },
   "id": "83a77c9b-17d1-409e-aeec-6167050862c9",
   "name": "Upload PNG and JHDR to Drive",
   "type": "n8n-nodes-base.googleDrive",
   "typeVersion": 3,
   "position": [
    1456,
    256
   ]
  },
  {
   "parameters": {
    "jsCode": "const result = $('Check Printavo Processed Status Update').first()?.json || $input.first()?.json || {};\nconst staticData = $getWorkflowStaticData('global');\nstaticData.processedDtfInvoices = staticData.processedDtfInvoices || {};\nconst invoiceKey = String(result.invoiceId || result.visualId || '');\nif (invoiceKey) {\n  staticData.processedDtfInvoices[invoiceKey] = {\n    processedAt: new Date().toISOString(),\n    visualId: result.visualId,\n    uploadedCount: result.uploadedCount,\n    printavoStatusUpdated: !!result.printavoStatusUpdated,\n    printavoStatus: result.printavoStatus\n  };\n}\nreturn [{ json: { ok: true, ...result } }];"
   },
   "id": "42ada459-a99e-46e9-bc58-ae112429ea34",
   "name": "Build Success Response",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2640,
    256
   ]
  },
  {
   "parameters": {
    "content": "## Setup \u2014 do these before activating\n\n1. **MajorDTF API key** \u2014 mint one at https://www.majordtf.com/account \u2192 API keys (format `mdtf_live_...`). Create an n8n *Bearer Auth* credential named `MajorDTF Worker API` and assign it to the `MajorDTF - Prep Artwork` node.\n2. **Printavo API** \u2014 create an n8n *Custom Auth* credential named `Printavo v2 API` that sends your Printavo `email` + `token` headers. Assign it to all `Printavo - *` HTTP nodes.\n3. **Status IDs** \u2014 replace `REPLACE_WITH_YOUR_READY_STATUS_ID`, `REPLACE_WITH_YOUR_PROCESSED_STATUS_ID`, `REPLACE_WITH_YOUR_ERROR_STATUS_ID` in the Code nodes with your own Printavo status IDs. Create statuses like 'Ready for DTF Processing', 'DTF Files Processed', 'DTF Error' if you don't have them.\n4. **Google Drive** \u2014 assign your Drive OAuth credential to the three Drive nodes and replace `REPLACE_WITH_YOUR_DRIVE_PARENT_FOLDER_ID` with the folder that should hold per-invoice output folders.\n5. **Failure email** \u2014 assign an SMTP credential to `Send Failure Email` and set from/to (placeholder: you@yourshop.com).\n6. **Postgres lock (optional)** \u2014 the `* Invoice Processing Lock` nodes prevent double-processing when executions overlap. Point them at any Postgres DB, or delete them and reconnect around them if you run serially.\n7. **Test first** \u2014 run manually with one invoice in the Ready status before activating the schedule.\n\nEach prep call costs 1 MajorDTF credit (billed on success only). Output: 300 DPI transparent PNG with print-ready alpha.",
    "height": 360,
    "width": 520
   },
   "id": "d13d09be-5cb7-410b-8eaa-0d4d6cf10ca5",
   "name": "Credential Setup Notes",
   "type": "n8n-nodes-base.stickyNote",
   "typeVersion": 1,
   "position": [
    -1056,
    -368
   ]
  },
  {
   "parameters": {
    "url": "={{$json.file_url}}",
    "options": {
     "response": {
      "response": {
       "neverError": true,
       "responseFormat": "file"
      }
     },
     "timeout": 300000
    }
   },
   "id": "3d4041da-af93-4f8d-9b50-0f14a3c5ccae",
   "name": "Download MajorDTF PNG",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    880,
    240
   ]
  },
  {
   "parameters": {
    "jsCode": "const item = $input.first()?.json || {};\nconst stage = item.stage || 'unknown';\nconst invoice = item.requestedInvoiceId || item.invoiceId || item.visualId || 'unknown';\nconst message = item.error || item.message || JSON.stringify(item).slice(0, 1000);\nthrow new Error(`MajorDTF Printavo automation failed at ${stage} for invoice ${invoice}: ${message}`);"
   },
   "id": "1cf6cc4d-1215-434a-9fb6-b07f08fac811",
   "name": "Fail Workflow With Original Error",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    -256,
    -208
   ]
  },
  {
   "parameters": {
    "jsCode": "const uploads = $input.all().map(i => i.json);\nconst normalize = $('Normalize Printavo Jobs').first()?.json || {};\nconst invoiceId = normalize.invoiceId || normalize.requestedInvoiceId;\nif (!invoiceId) throw new Error('Cannot mark Printavo processed: missing invoice id');\nconst mutation = `mutation MarkDtfFilesProcessed($parentId: ID!, $statusId: ID!) {\n  statusUpdate(parentId: $parentId, statusId: $statusId) {\n    __typename\n    ... on Invoice { id visualId status { id name type } }\n  }\n}`;\nreturn [{ json: {\n  status: 'ok',\n  stage: 'build_printavo_status_update',\n  invoiceId: String(invoiceId),\n  visualId: normalize.visualId,\n  uploadedCount: uploads.length,\n  files: uploads.map(u => ({ id: u.id, name: u.name, mimeType: u.mimeType, webViewLink: u.webViewLink })),\n  targetStatusId: 'REPLACE_WITH_YOUR_PROCESSED_STATUS_ID',\n  targetStatusName: 'DTF Files Processed',\n  body: { query: mutation, variables: { parentId: String(invoiceId), statusId: 'REPLACE_WITH_YOUR_PROCESSED_STATUS_ID' } }\n} }];"
   },
   "id": "8a156ef3-23bb-4aba-8dd5-71c8ba148438",
   "name": "Build Printavo Processed Status Request",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1680,
    256
   ]
  },
  {
   "parameters": {
    "method": "POST",
    "url": "https://www.printavo.com/api/v2",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Content-Type",
       "value": "application/json"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{$json.body}}",
    "options": {
     "response": {
      "response": {
       "neverError": true,
       "responseFormat": "json"
      }
     },
     "timeout": 30000
    }
   },
   "id": "9cad7690-3e17-4445-811d-bd0abbd21cb9",
   "name": "Printavo - Mark DTF Files Processed",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    1920,
    256
   ]
  },
  {
   "parameters": {
    "jsCode": "const response = $input.first()?.json || {};\nconst request = $('Build Printavo Processed Status Request').first()?.json || {};\nif (response.errors?.length) {\n  throw new Error(`Printavo status update failed for invoice ${request.invoiceId}: ${JSON.stringify(response.errors).slice(0, 1000)}`);\n}\nconst updated = response.data?.statusUpdate;\nconst status = updated?.status;\nif (status?.id && String(status.id) !== 'REPLACE_WITH_YOUR_PROCESSED_STATUS_ID') {\n  throw new Error(`Printavo status update returned unexpected status for invoice ${request.invoiceId}: ${status.id} ${status.name || ''}`);\n}\nreturn [{ json: {\n  ok: true,\n  invoiceId: request.invoiceId,\n  visualId: request.visualId,\n  uploadedCount: request.uploadedCount,\n  files: request.files,\n  printavoStatusUpdated: true,\n  printavoStatus: status || { id: 'REPLACE_WITH_YOUR_PROCESSED_STATUS_ID', name: 'DTF Files Processed' },\n  printavoStatusResponseType: updated?.__typename || null\n} }];"
   },
   "id": "32272348-f495-47d3-aa38-f0572784e8a8",
   "name": "Check Printavo Processed Status Update",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2160,
    256
   ]
  },
  {
   "parameters": {
    "jsCode": "const item = $input.first().json;\nconst invoiceId = String(item.requestedInvoiceId || item.invoiceId || item.poll?.readyInvoice?.id || '');\nif (!invoiceId) {\n  return [{ json: { ...item, status: 'error', stage: 'invoice_lock', error: 'Cannot acquire lock: missing invoice id' } }];\n}\nconst visualId = String(item.visualId || item.poll?.readyInvoice?.visualId || '');\nconst esc = (value) => String(value || '').replace(/'/g, \"''\");\nconst lockKey = `printavo-dtf:${invoiceId}`;\nconst executionId = String($execution.id || 'manual');\nconst workflowId = 'printavo-v2-majordtf-dtf-package';\nconst lockSql = `with cleanup as (\n  delete from majordtf_workflow_locks where expires_at < now()\n), inserted as (\n  insert into majordtf_workflow_locks (lock_key, workflow_id, invoice_id, visual_id, execution_id, expires_at)\n  values ('${esc(lockKey)}', '${esc(workflowId)}', '${esc(invoiceId)}', '${esc(visualId)}', '${esc(executionId)}', now() + interval '15 minutes')\n  on conflict (lock_key) do nothing\n  returning true as acquired\n)\nselect coalesce((select acquired from inserted), false) as lock_acquired;`;\nreturn [{ json: { ...item, invoiceId, visualId, lockKey, lockSql } }];"
   },
   "id": "093b9e95-9ba0-4f15-b695-186d8485a8d2",
   "name": "Build Invoice Processing Lock Query",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    368,
    416
   ]
  },
  {
   "parameters": {
    "operation": "executeQuery",
    "query": "{{$json.lockSql}}",
    "options": {}
   },
   "id": "f581c398-297c-4f94-81e7-b5120022e83e",
   "name": "Acquire Invoice Processing Lock",
   "type": "n8n-nodes-base.postgres",
   "typeVersion": 2.6,
   "position": [
    560,
    464
   ]
  },
  {
   "parameters": {
    "jsCode": "const source = $('Build Invoice Processing Lock Query').first()?.json || {};\nconst lock = $input.first()?.json || {};\nconst acquired = lock.lock_acquired === true || lock.lock_acquired === 'true' || lock.lock_acquired === 1;\nif (!acquired) {\n  return [{ json: { ...source, status: 'noop', stage: 'invoice_lock', lockAcquired: false, message: `Invoice ${source.invoiceId || source.requestedInvoiceId} is already being processed by another workflow run` } }];\n}\nreturn [{ json: { ...source, status: 'ok', lockAcquired: true } }];"
   },
   "id": "adcbc671-a59a-4e43-879a-d9e8b5e52dd0",
   "name": "Check Invoice Processing Lock",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    720,
    480
   ]
  },
  {
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "strict",
      "version": 2
     },
     "conditions": [
      {
       "id": "lock-acquired",
       "leftValue": "={{$json.lockAcquired}}",
       "rightValue": true,
       "operator": {
        "type": "boolean",
        "operation": "true",
        "singleValue": true
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   },
   "id": "d3a9c983-8ed7-42a0-b7c1-32b999e6d3ab",
   "name": "IF Lock Acquired",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    1120,
    496
   ]
  },
  {
   "parameters": {
    "operation": "executeQuery",
    "query": "{{ `delete from majordtf_workflow_locks where lock_key = 'printavo-dtf:${String($json.invoiceId || '').replace(/'/g, )}';` }}",
    "options": {}
   },
   "id": "4f03f4a5-e744-4d7c-9b1b-b19a2f73d58a",
   "name": "Release Invoice Processing Lock",
   "type": "n8n-nodes-base.postgres",
   "typeVersion": 2.6,
   "position": [
    2400,
    256
   ]
  },
  {
   "id": "dtf-filter-png-uploads",
   "name": "Filter PNG Uploads",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1696,
    476
   ],
   "parameters": {
    "jsCode": "// Only PNGs go to Printavo production files (JHDR would be MIME-sniffed and renamed; stays Drive-only)\nreturn $input.all().filter(i => String(i.json.name || '').toLowerCase().endsWith('.png'));"
   }
  },
  {
   "id": "dtf-share-png-public",
   "name": "Drive - Share PNG Public",
   "type": "n8n-nodes-base.googleDrive",
   "typeVersion": 3,
   "position": [
    1936,
    476
   ],
   "parameters": {
    "operation": "share",
    "fileId": {
     "__rl": true,
     "mode": "id",
     "value": "={{$json.id}}"
    },
    "permissionsUi": {
     "permissionsValues": {
      "role": "reader",
      "type": "anyone",
      "allowFileDiscovery": false
     }
    },
    "options": {}
   }
  },
  {
   "id": "dtf-build-prodfile-reqs",
   "name": "Build Production File Requests",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2176,
    476
   ],
   "parameters": {
    "jsCode": "const normalize = $('Normalize Printavo Jobs').first()?.json || {};\nconst invoiceId = String(normalize.invoiceId || normalize.requestedInvoiceId || '');\nif (!invoiceId) throw new Error('Cannot attach production files: missing invoice id');\nconst mutation = `mutation AttachDtfProductionFile($parentId: ID!, $url: String!) {\n  productionFileCreate(parentId: $parentId, publicFileUrl: $url) { id fileUrl }\n}`;\nconst pngs = $('Filter PNG Uploads').all();\nreturn $input.all().map((item, idx) => {\n  const f = pngs[idx]?.json || {};\n  if (!f.id) throw new Error(`Missing Drive file id for attach (item ${idx})`);\n  return { json: {\n    status: 'ok', stage: 'build_production_file_request',\n    invoiceId, fileName: f.name, driveFileId: f.id,\n    body: { query: mutation, variables: { parentId: invoiceId, url: `https://drive.google.com/uc?export=download&id=${f.id}` } }\n  } };\n});"
   }
  },
  {
   "id": "dtf-attach-prodfile",
   "name": "Printavo - Attach Production File",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    2416,
    476
   ],
   "parameters": {
    "method": "POST",
    "url": "https://www.printavo.com/api/v2",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Content-Type",
       "value": "application/json"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{$json.body}}",
    "options": {
     "response": {
      "response": {
       "neverError": true,
       "responseFormat": "json"
      }
     },
     "timeout": 120000
    }
   }
  },
  {
   "id": "dtf-check-prodfile",
   "name": "Check Production File Attach",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    2656,
    476
   ],
   "parameters": {
    "jsCode": "const reqs = $('Build Production File Requests').all();\nconst failed = [];\n$input.all().forEach((r, idx) => {\n  const body = r.json || {};\n  const name = reqs[idx]?.json?.fileName || `item ${idx}`;\n  if (body.errors?.length || !body.data?.productionFileCreate?.id) {\n    failed.push(`${name}: ${JSON.stringify(body.errors || body).slice(0, 300)}`);\n  }\n});\nif (failed.length) throw new Error(`productionFileCreate failed for ${failed.length} file(s): ${failed.join(' | ')}`);\nreturn [{ json: { status: 'ok', stage: 'production_files_attached', attachedCount: $input.all().length } }];"
   }
  },
  {
   "id": "dtf-build-error-status",
   "name": "Build DTF Error Status Request",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    -300,
    244
   ],
   "parameters": {
    "jsCode": "const item = $input.first()?.json || {};\nlet invoiceId = String(item.requestedInvoiceId || item.invoiceId || '');\nif (!invoiceId) { try { invoiceId = String($('Normalize Printavo Jobs').first()?.json?.invoiceId || ''); } catch (e) {} }\nif (!invoiceId) { try { invoiceId = String($('Select Ready Invoice').first()?.json?.requestedInvoiceId || ''); } catch (e) {} }\nconst mutation = `mutation SetDtfErrorStatus($parentId: ID!, $statusId: ID!) {\n  statusUpdate(parentId: $parentId, statusId: $statusId) { __typename ... on Invoice { id visualId status { id name } } }\n}`;\nreturn [{ json: { ...item, invoiceId, targetStatusId: 'REPLACE_WITH_YOUR_ERROR_STATUS_ID', targetStatusName: 'DTF Processing - ERROR',\n  body: { query: mutation, variables: { parentId: invoiceId, statusId: 'REPLACE_WITH_YOUR_ERROR_STATUS_ID' } } } }];"
   }
  },
  {
   "id": "dtf-set-error-status",
   "name": "Printavo - Set DTF Error Status",
   "type": "n8n-nodes-base.httpRequest",
   "typeVersion": 4.2,
   "position": [
    -60,
    244
   ],
   "parameters": {
    "method": "POST",
    "url": "https://www.printavo.com/api/v2",
    "authentication": "genericCredentialType",
    "genericAuthType": "httpCustomAuth",
    "sendHeaders": true,
    "headerParameters": {
     "parameters": [
      {
       "name": "Content-Type",
       "value": "application/json"
      }
     ]
    },
    "sendBody": true,
    "specifyBody": "json",
    "jsonBody": "={{$json.body}}",
    "options": {
     "response": {
      "response": {
       "neverError": true,
       "responseFormat": "json"
      }
     },
     "timeout": 30000
    }
   }
  },
  {
   "id": "dtf-check-error-status",
   "name": "Check DTF Error Status Update",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    180,
    244
   ],
   "parameters": {
    "jsCode": "// Best-effort: never throw here, or the lock below never gets released.\nconst response = $input.first()?.json || {};\nconst request = $('Build DTF Error Status Request').first()?.json || {};\nconst updated = response.data?.statusUpdate;\nconst statusUpdateFailed = !!(response.errors?.length || !updated?.status?.id);\nreturn [{ json: {\n  invoiceId: request.invoiceId, requestedInvoiceId: request.requestedInvoiceId, visualId: request.visualId,\n  stage: request.stage, error: request.error,\n  statusUpdateFailed, statusUpdateDetail: statusUpdateFailed ? JSON.stringify(response).slice(0, 500) : null\n} }];"
   }
  },
  {
   "id": "dtf-release-lock-error",
   "name": "Release Invoice Processing Lock (Error)",
   "type": "n8n-nodes-base.postgres",
   "typeVersion": 2.6,
   "position": [
    420,
    244
   ],
   "parameters": {
    "operation": "executeQuery",
    "query": "{{ `delete from majordtf_workflow_locks where lock_key = 'printavo-dtf:${String($json.invoiceId || '').replace(/'/g, )}';` }}",
    "options": {}
   }
  },
  {
   "id": "dtf-check-prep-response",
   "name": "Check Prep Response",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    1100,
    140
   ],
   "parameters": {
    "jsCode": "// Guard before Download: a bad/missing file_url here would otherwise hard-crash\n// Download MajorDTF PNG (a real n8n node exception, not a caught status:'error' item) --\n// which skips the error chain entirely and leaves the lock held for the full 15min TTL.\nconst items = $input.all().map(i => i.json);\nconst req = $('Build MajorDTF Prep Requests').first()?.json || {};\nconst bad = items.find(it => it.status === 'error' || it.error || !it.file_url);\nif (bad) {\n  return [{ json: { status: 'error', stage: 'major_dtf_prep', requestedInvoiceId: req.invoiceId, visualId: req.visualId, error: JSON.stringify(bad).slice(0, 1000) } }];\n}\nreturn items.map(it => ({ json: it }));"
   }
  },
  {
   "id": "dtf-if-prep-error",
   "name": "IF Prep Error",
   "type": "n8n-nodes-base.if",
   "typeVersion": 2.2,
   "position": [
    1340,
    140
   ],
   "parameters": {
    "conditions": {
     "options": {
      "caseSensitive": true,
      "leftValue": "",
      "typeValidation": "strict",
      "version": 2
     },
     "conditions": [
      {
       "id": "prep-status-error",
       "leftValue": "={{$json.status}}",
       "rightValue": "error",
       "operator": {
        "type": "string",
        "operation": "equals"
       }
      }
     ],
     "combinator": "and"
    },
    "options": {}
   }
  },
  {
   "id": "dtf-tag-drive-folder-error",
   "name": "Tag Drive Folder Error",
   "type": "n8n-nodes-base.code",
   "typeVersion": 2,
   "position": [
    428,
    200
   ],
   "parameters": {
    "jsCode": "const err = $input.first()?.json || {};\nreturn [{ json: { status: 'error', stage: 'drive_folder_create', error: err.error || JSON.stringify(err).slice(0, 500) } }];"
   }
  }
 ],
 "connections": {
  "IF Fetch Error": {
   "main": [
    [
     {
      "node": "Build DTF Error Status Request",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Normalize Printavo Jobs",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Send Failure Email": {
   "main": [
    []
   ]
  },
  "Normalize Printavo Jobs": {
   "main": [
    [
     {
      "node": "IF Normalize Error",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "IF Normalize Error": {
   "main": [
    [
     {
      "node": "Build DTF Error Status Request",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Create Drive Folder",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Create Drive Folder": {
   "main": [
    [
     {
      "node": "Split Jobs With Folder",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Tag Drive Folder Error",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Split Jobs With Folder": {
   "main": [
    [
     {
      "node": "Build MajorDTF Prep Requests",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Process MajorDTF and Build Files": {
   "main": [
    [
     {
      "node": "IF Process Error",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "IF Process Error": {
   "main": [
    [
     {
      "node": "Build DTF Error Status Request",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Upload PNG and JHDR to Drive",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Upload PNG and JHDR to Drive": {
   "main": [
    [
     {
      "node": "Build Printavo Processed Status Request",
      "type": "main",
      "index": 0
     },
     {
      "node": "Filter PNG Uploads",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Build Success Response": {
   "main": [
    []
   ]
  },
  "Schedule - Poll Printavo Ready": {
   "main": [
    [
     {
      "node": "Prepare Printavo Poll Query",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Prepare Printavo Poll Query": {
   "main": [
    [
     {
      "node": "Printavo - Poll Ready Invoices",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Printavo - Poll Ready Invoices": {
   "main": [
    [
     {
      "node": "Select Ready Invoice",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Select Ready Invoice": {
   "main": [
    [
     {
      "node": "IF Poll Error",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "IF Poll Error": {
   "main": [
    [
     {
      "node": "Fail Workflow With Original Error",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "IF Poll Has Invoice",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "IF Poll Has Invoice": {
   "main": [
    [
     {
      "node": "Build Invoice Processing Lock Query",
      "type": "main",
      "index": 0
     }
    ],
    []
   ]
  },
  "Printavo - Fetch Invoice Detail": {
   "main": [
    [
     {
      "node": "Wrap Printavo Invoice Detail",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Wrap Printavo Invoice Detail": {
   "main": [
    [
     {
      "node": "IF Fetch Error",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Build MajorDTF Prep Requests": {
   "main": [
    [
     {
      "node": "MajorDTF - Prep Artwork",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "MajorDTF - Prep Artwork": {
   "main": [
    [
     {
      "node": "Check Prep Response",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Download MajorDTF PNG": {
   "main": [
    [
     {
      "node": "Process MajorDTF and Build Files",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Build Printavo Processed Status Request": {
   "main": [
    [
     {
      "node": "Printavo - Mark DTF Files Processed",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Printavo - Mark DTF Files Processed": {
   "main": [
    [
     {
      "node": "Check Printavo Processed Status Update",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Check Printavo Processed Status Update": {
   "main": [
    [
     {
      "node": "Release Invoice Processing Lock",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Build Invoice Processing Lock Query": {
   "main": [
    [
     {
      "node": "Acquire Invoice Processing Lock",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Acquire Invoice Processing Lock": {
   "main": [
    [
     {
      "node": "Check Invoice Processing Lock",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Check Invoice Processing Lock": {
   "main": [
    [
     {
      "node": "IF Lock Acquired",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "IF Lock Acquired": {
   "main": [
    [
     {
      "node": "Printavo - Fetch Invoice Detail",
      "type": "main",
      "index": 0
     }
    ],
    []
   ]
  },
  "Release Invoice Processing Lock": {
   "main": [
    [
     {
      "node": "Build Success Response",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Filter PNG Uploads": {
   "main": [
    [
     {
      "node": "Drive - Share PNG Public",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Drive - Share PNG Public": {
   "main": [
    [
     {
      "node": "Build Production File Requests",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Build Production File Requests": {
   "main": [
    [
     {
      "node": "Printavo - Attach Production File",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Printavo - Attach Production File": {
   "main": [
    [
     {
      "node": "Check Production File Attach",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Build DTF Error Status Request": {
   "main": [
    [
     {
      "node": "Printavo - Set DTF Error Status",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Printavo - Set DTF Error Status": {
   "main": [
    [
     {
      "node": "Check DTF Error Status Update",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Check DTF Error Status Update": {
   "main": [
    [
     {
      "node": "Release Invoice Processing Lock (Error)",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Release Invoice Processing Lock (Error)": {
   "main": [
    [
     {
      "node": "Fail Workflow With Original Error",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Check Prep Response": {
   "main": [
    [
     {
      "node": "IF Prep Error",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "IF Prep Error": {
   "main": [
    [
     {
      "node": "Build DTF Error Status Request",
      "type": "main",
      "index": 0
     }
    ],
    [
     {
      "node": "Download MajorDTF PNG",
      "type": "main",
      "index": 0
     }
    ]
   ]
  },
  "Tag Drive Folder Error": {
   "main": [
    [
     {
      "node": "Build DTF Error Status Request",
      "type": "main",
      "index": 0
     }
    ]
   ]
  }
 },
 "settings": {
  "executionOrder": "v1"
 },
 "pinData": {}
}