{
  "name": "Calendar Events to Invoice",
  "nodes": [
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "value": "",
          "mode": "id"
        },
        "returnAll": true,
        "timeMin": "={{ $('Config').first().json.dateRanges.previousMonthStart }}",
        "timeMax": "={{ $('Config').first().json.dateRanges.previousMonthEnd }}",
        "options": {
          "fields": "=items(id, summary, start, end, eventType, colorId)",
          "orderBy": "startTime",
          "recurringEventHandling": "expand"
        }
      },
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        224,
        0
      ],
      "id": "3a989d17-cf4a-4da9-ae33-36f2101f879f",
      "name": "Get many events",
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "e5b38923-6b05-4985-9934-638d9a3e2c38",
              "leftValue": "={{ $json.colorId }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        448,
        0
      ],
      "id": "5aa5d698-b43f-40f7-b87c-bbe3ad64b5d6",
      "name": "Filter"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "02cc967e-aa04-415c-8098-4d1020c6db29",
              "name": "durationHours",
              "value": "={{ DateTime.fromISO($json.end.dateTime).diff(DateTime.fromISO($json.start.dateTime), 'hours').hours }}",
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        672,
        0
      ],
      "id": "fe859a13-1ef0-46b1-ae7d-42ac49fcdd0b",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "fieldsToSummarize": {
          "values": [
            {
              "aggregation": "sum",
              "field": "durationHours"
            }
          ]
        },
        "fieldsToSplitBy": "colorId",
        "options": {
          "outputFormat": "singleItem"
        }
      },
      "type": "n8n-nodes-base.summarize",
      "typeVersion": 1.1,
      "position": [
        880,
        0
      ],
      "id": "90c3de2c-447d-4830-b5cd-66eefeccd0ff",
      "name": "Summarize"
    },
    {
      "parameters": {
        "jsCode": "// Configuration Hub - Put this early in your workflow\nconst hubId = \"123123\";  // HubSpot account identifier\nconst baseUrl = `https://app.hubspot.com/contacts/${hubId}/record`;\n\n// HubSpot product data mapped to Google Calendar event colors\nconst productData = {\n  \"Product 1\": {\n    colorId: \"11\", // Google Calendar event colorId\n    id: \"26854895751\", // HubSpot productId\n    name: \"Full name of Product 1\" // HubSpot product name\n  },\n  \"Product 2\": {\n    colorId: \"3\", \n    id: \"26837140533\",\n    name: \"Full name of Product 2\"\n  },\n  \"Product 3\": {\n    colorId: \"2\",\n    id: \"26854895753\",\n    name: \"Full name of Product 3\"\n  }\n};\n\n// Contact data mapped by company name\nconst contactData = {\n  \"Company A\": {\n    contactId: \"123456789\", // HubSpot contactId\n    contactName: \"Roger Wilco\",\n    companyId: \"987654321\" // HubSpot companyId\n  }\n};\n\nconst config = {\n  // Auto-generate color mappings from productData\n  colorMappings: Object.fromEntries(\n    Object.entries(productData).map(([key, data]) => [data.colorId, key])\n  ),\n  \n  // Build products with dynamic URLs\n  products: Object.fromEntries(\n    Object.entries(productData).map(([key, data]) => [\n      key, \n      {\n        id: data.id,\n        name: data.name,\n        url: `${baseUrl}/0-7/${data.id}/properties`\n      }\n    ])\n  ),\n  \n  // Build contacts mapped by company name with URLs\n  contacts: Object.fromEntries(\n    Object.entries(contactData).map(([companyName, data]) => [\n      companyName,\n      {\n        contactId: data.contactId,\n        contactName: data.contactName,\n        contactUrl: `${baseUrl}/0-1/${data.contactId}`,\n        companyId: data.companyId,\n        companyUrl: `${baseUrl}/0-2/${data.companyId}`\n      }\n    ])\n  ),\n  \n  // HubSpot Association Types\n  // full ref: https://developers.hubspot.com/docs/guides/api/crm/associations/associations-v4#association-type-id-values\n  associationTypes: {\n    invoiceToContact: 177,\n    invoiceToCompany: 179,\n    lineItemToInvoice: 410\n  },\n  \n  // Date ranges for retrieving calendar events\n  dateRanges: {\n    previousMonthStart: DateTime.now().minus({ months: 1 }).startOf('month').toISO(),\n    previousMonthEnd: DateTime.now().startOf('month').toISO()\n  },\n  \n  // Other constants\n  constants: {\n    currency: \"USD\"\n  }\n};\n\nreturn [{ json: config }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "id": "c7e01ee5-6742-4637-9da7-8ab5517a20f5",
      "name": "Config"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/invoices",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"properties\": {\n    \"hs_currency\": \"{{ $('Config').item.json.constants.currency }}\"\n  },\n  \"associations\": [\n    {\n      \"types\": [\n        {\n          \"associationCategory\": \"HUBSPOT_DEFINED\",\n          \"associationTypeId\": {{ $('Config').item.json.associationTypes.invoiceToContact }}\n        }\n      ],\n      \"to\": {\n        \"id\": {{ $('Config').item.json.contacts['Company A'].contactId }}\n      }\n    },\n    {\n      \"types\": [\n        {\n          \"associationCategory\": \"HUBSPOT_DEFINED\",\n          \"associationTypeId\": {{ $('Config').item.json.associationTypes.invoiceToCompany }}\n        }\n      ],\n      \"to\": {\n        \"id\": {{ $('Config').item.json.contacts['Company A'].companyId }}\n      }\n    }\n  ]\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1088,
        0
      ],
      "id": "4b25e833-bac5-4b3a-98e2-353dd63be7bc",
      "name": "Create Invoice",
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/line_items/batch/create",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1440,
        0
      ],
      "id": "7c20d33d-2d25-45b0-b3a1-ad9c891c993a",
      "name": "Add Line Items",
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -224,
        0
      ],
      "id": "a25a7ef3-a1d1-4699-9217-8d81b6744d2d",
      "name": "Manual"
    },
    {
      "parameters": {
        "jsCode": "const summaryData = $('Summarize').first().json;\nconst config = $('Config').first().json;\nconst invoiceId = $('Create Invoice').first().json.id;\n\nconst lineItemsPayload = {\n  \"inputs\": Object.entries(summaryData).map(([colorId, data]) => ({\n    \"properties\": {\n      \"hs_product_id\": config.products[config.colorMappings[colorId]].id,\n      \"quantity\": data.sum_durationHours.toString()\n    },\n    \"associations\": [\n      {\n        \"types\": [\n          {\n            \"associationCategory\": \"HUBSPOT_DEFINED\",\n            \"associationTypeId\": config.associationTypes.lineItemToInvoice\n          }\n        ],\n        \"to\": {\n          \"id\": invoiceId\n        }\n      }\n    ]\n  }))\n};\n\nreturn [{ json: lineItemsPayload }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1264,
        0
      ],
      "id": "4384c5ee-7ca6-4874-8b42-ffb6407a7b34",
      "name": "Prepare Line Items"
    },
    {
      "parameters": {
        "content": "Replace with Schedule Trigger that runs every month to automate workflow",
        "height": 272,
        "width": 192
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        -112
      ],
      "typeVersion": 1,
      "id": "4b3d29fe-2540-4e40-8eb4-dcb275487fc1",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "Update with your information. Later nodes pull values generated by this node.",
        "height": 272,
        "width": 192
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -112
      ],
      "typeVersion": 1,
      "id": "a36a0d8d-5de3-4790-aa81-7d40371fa0ff",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "Exclude events without a colorId (or else events that are not billable).",
        "height": 272,
        "width": 192
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        -112
      ],
      "typeVersion": 1,
      "id": "c8e33692-9f66-468c-9aea-4bab33165c19",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "Calculate event durations in hours.",
        "height": 272,
        "width": 192
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        -112
      ],
      "typeVersion": 1,
      "id": "f1b98baa-9b0c-49f1-bb11-b61a2dcde68d",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "content": "Calculate total duration in hours per colorId (event type) ",
        "height": 272,
        "width": 192
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        832,
        -112
      ],
      "typeVersion": 1,
      "id": "d6a79ea6-93af-4a82-b8b7-4714d3dc1e93",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "content": "Create invoice and update with line items. Must create a HubSpot private app with appropriate scopes and use the private app access token for authentication [Reference](https://developers.hubspot.com/docs/guides/apps/private-apps/overview)",
        "height": 272,
        "width": 544
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        -112
      ],
      "typeVersion": 1,
      "id": "68edb0ac-2ec6-47e9-8f1f-f58dfd51448f",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "content": "Retrieve all calendar events from the previous month, ensuring colorId is included in response.",
        "height": 272,
        "width": 192
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        -112
      ],
      "typeVersion": 1,
      "id": "a7f5a766-152d-4779-824d-d0964b2e7f59",
      "name": "Sticky Note6"
    }
  ],
  "connections": {
    "Get many events": {
      "main": [
        [
          {
            "node": "Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Summarize",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize": {
      "main": [
        [
          {
            "node": "Create Invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Get many events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Invoice": {
      "main": [
        [
          {
            "node": "Prepare Line Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Line Items": {
      "main": [
        [
          {
            "node": "Add Line Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a85b5e88-4aaa-46d4-8e50-0170d8f8fc1e",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "eB3InUHi9a15lNwd",
  "tags": []
}