4c672c370ecd8eea80c9bf15b2fa9aa22476c233652637ef4499daa691569a82:{"__metadata":{"version":0.1},"root":{"steps":[{"step_id":"01M6FAZG1GV4DP6Y2CA4DZC0QY","step_position":[0,0],"config_field_values":[],"task_id":"shopify::admin::order_created","task_version":"0.1","task_type":"TRIGGER","description":null,"note":null,"name":null},{"step_id":"01M6FB0N75M1VJQFE59GYXTXJ1","step_position":[0,200],"config_field_values":[{"config_field_id":"input","value":"{\n order {\n createdAt\n lineItems {\n quantity\n variant {\n id\n }\n product {\n id\n tags\n }\n }\n customer {\n purchaseCounts {\n value\n }\n }\n }\n shop {\n timezoneOffsetMinutes\n purchaseLimits {\n value\n }\n }\n}\n"},{"config_field_id":"script","value":"const SIGN = 1;\n\nconst parse = (value) => {\n try {\n return JSON.parse(value || '') || {};\n } catch {\n return {};\n }\n};\n\n// The store's date of a time like 2026-11-03T00:22:02Z, from the store's offset in\n// minutes (New York: -300). Plain arithmetic: Run code blocks clock functions, Date too\nconst MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\nconst isLeapYear = (year) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\nconst daysInMonth = (year, month) =>\n month === 2 && isLeapYear(year) ? 29 : MONTH_DAYS[month - 1];\nconst localDate = (time, offsetMinutes) => {\n const text = String(time || '');\n const parts =\n /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::[\\d.]+)?(Z|([+-])(\\d{2}):?(\\d{2}))?/.exec(\n text,\n );\n if (!parts) {\n return text.slice(0, 10);\n }\n let [year, month, day] = [parts[1], parts[2], parts[3]].map(Number);\n // Minutes into the day in UTC, then in the store's time zone\n const zone = parts[7]\n ? (parts[7] === '-' ? -1 : 1) * (Number(parts[8]) * 60 + Number(parts[9]))\n : 0;\n let minutes =\n Number(parts[4]) * 60 + Number(parts[5]) - zone + (Number(offsetMinutes) || 0);\n for (; minutes < 0; minutes += 1440) {\n day -= 1;\n if (day < 1) {\n month -= 1;\n if (month < 1) {\n month = 12;\n year -= 1;\n }\n day = daysInMonth(year, month);\n }\n }\n for (; minutes >= 1440; minutes -= 1440) {\n day += 1;\n if (day > daysInMonth(year, month)) {\n day = 1;\n month += 1;\n if (month > 12) {\n month = 1;\n year += 1;\n }\n }\n }\n const pad = (value) => String(value).padStart(2, '0');\n return `${year}-${pad(month)}-${pad(day)}`;\n};\n\n// gid://shopify/ProductVariant/123 to 123\nconst idNumber = (gid) =>\n Number(\n String(gid || '')\n .split('/')\n .pop(),\n ) || 0;\n\nconst matches = (limit, line) => {\n const tags = (line.tags || []).map((tag) => String(tag).trim().toLowerCase());\n const hasTag = (list) =>\n (list || []).some((tag) => tags.includes(String(tag).trim().toLowerCase()));\n const picked =\n limit.selectionType === 'ALL' ||\n (limit.selectionType === 'VARIANTS' &&\n (limit.variantIds || []).includes(line.variant)) ||\n (limit.selectionType === 'PRODUCT_TAGS' && hasTag(limit.productTags));\n const leftOut =\n (limit.excludeType === 'VARIANTS' &&\n (limit.excludeVariantIds || []).includes(line.variant)) ||\n (limit.excludeType === 'PRODUCT_TAGS' && hasTag(limit.excludeProductTags));\n return picked && !leftOut;\n};\n\nconst countsKey = (limit, line) => {\n if (limit.countBy === 'PRODUCT') {\n return `${limit.key}:p${line.product}`;\n }\n if (limit.countBy === 'VARIANT') {\n return `${limit.key}:v${line.variant}`;\n }\n return limit.key;\n};\n\nfunction countPurchases(input, sign = SIGN) {\n const order = input?.order || {};\n const stored = parse(order.customer?.purchaseCounts?.value);\n const counts = stored.c && typeof stored.c === 'object' ? stored.c : {};\n const storedDate = String(stored.d || '');\n const orderDate = localDate(order.createdAt, input?.shop?.timezoneOffsetMinutes);\n const limits = (parse(input?.shop?.purchaseLimits?.value).limits || []).filter(\n (limit) => limit && limit.key && limit.period && limit.period !== 'ORDER',\n );\n\n // Whether a date is in the counts' year, month and day\n const samePeriods = (orderDay) =>\n [4, 7, 10].map(\n (length) =>\n !!storedDate && storedDate.slice(0, length) === orderDay.slice(0, length),\n );\n\n let date = storedDate;\n // The periods the order's items count in: all time, year, month, day\n let inPeriod = [true, true, true, true];\n if (sign > 0 && (!storedDate || orderDate >= storedDate)) {\n // A new order: the periods it's in start over where they changed\n const same = samePeriods(orderDate);\n Object.keys(counts).forEach((key) => {\n [1, 2, 3].forEach((index) => {\n if (!same[index - 1] && Array.isArray(counts[key])) {\n counts[key][index] = 0;\n }\n });\n });\n date = orderDate;\n } else {\n // A canceled order, or an older one counted late: only the periods still current\n inPeriod = [true, ...samePeriods(orderDate)];\n }\n\n (order.lineItems || []).forEach((item) => {\n const line = {\n variant: idNumber(item?.variant?.id),\n product: idNumber(item?.product?.id),\n tags: item?.product?.tags || [],\n };\n const quantity = Number(item?.quantity) || 0;\n if (!line.variant || quantity <= 0) {\n return;\n }\n limits\n .filter((limit) => matches(limit, line))\n .forEach((limit) => {\n const key = countsKey(limit, line);\n const count = (counts[key] = Array.isArray(counts[key])\n ? counts[key]\n : [0, 0, 0, 0]);\n inPeriod.forEach((counted, index) => {\n if (counted) {\n count[index] = Math.max(0, (Number(count[index]) || 0) + sign * quantity);\n }\n });\n });\n });\n\n // Keys with nothing left go, to stay within the 10,000 bytes Shopify passes on\n Object.keys(counts).forEach((key) => {\n if (!Array.isArray(counts[key]) || counts[key].every((value) => !value)) {\n delete counts[key];\n }\n });\n return { counts: JSON.stringify({ d: date || orderDate, c: counts }) };\n}\n\nexport default function main(input) {\n return countPurchases(input, SIGN);\n}\n"},{"config_field_id":"output_schema","value":"type Output {\n counts: String!\n}\n"}],"task_id":"shopify::flow::run_code","task_version":"0.1","task_type":"ACTION","description":null,"note":null,"name":"Run code"},{"step_id":"01M6FBCGPW4C4SFYY9AT3RFVST","step_position":[0,300],"config_field_values":[{"config_field_id":"customer_id","value":"{\"value\":\"\",\"default_value\":\"order.customer.id\"}"},{"config_field_id":"metafield","value":"{\"namespace\":\"checkout_buddy\",\"key\":\"purchase_counts\",\"type\":\"json\"}"},{"config_field_id":"value","value":"{{runCode.counts}}"}],"task_id":"shopify::admin::add_customer_metafield","task_version":"1.0","task_type":"ACTION","description":null,"note":null,"name":null}],"links":[{"from_step_id":"01M6FAZG1GV4DP6Y2CA4DZC0QY","from_port_id":"output","to_step_id":"01M6FB0N75M1VJQFE59GYXTXJ1","to_port_id":"input"},{"from_step_id":"01M6FB0N75M1VJQFE59GYXTXJ1","from_port_id":"output","to_step_id":"01M6FBCGPW4C4SFYY9AT3RFVST","to_port_id":"input"}],"patched_fields":[{"id":"7ae6fd2e-2cf6-449d-8680-ae5c9e81fb0c","handle":"purchaseCounts","field":"metafield","patched_type":"Customer","arguments":"{\"key\":\"purchase_counts\",\"namespace\":\"checkout_buddy\"}","merchant_configured":true},{"id":"dd567cf5-bc9b-4861-9759-dee95baaa806","handle":"purchaseLimits","field":"metafield","patched_type":"Shop","arguments":"{\"key\":\"purchase_limits\",\"namespace\":\"checkout_buddy\"}","merchant_configured":true}],"variables":[],"note":null,"vertical_layout_enabled":true,"workflow_name":"Purchase limits: count orders"}}