{
  "name": "freee の入金遅れに、お支払いのお願いメールを下書き（金額はすべてコードが確かめる）",
  "nodes": [
    {
      "id": "c9000000-0000-4000-8000-000000000001",
      "name": "平日の朝9時",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000002",
      "name": "今すぐ動かす",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        200
      ],
      "parameters": {}
    },
    {
      "id": "c9000000-0000-4000-8000-000000000003",
      "name": "設定",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        100
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": false,
        "assignments": {
          "assignments": [
            {
              "id": "c01",
              "name": "freee_company_id",
              "value": "PASTE_YOUR_FREEE_COMPANY_ID",
              "type": "string"
            },
            {
              "id": "c02",
              "name": "staff_email",
              "value": "staff@example.com",
              "type": "string"
            },
            {
              "id": "c03",
              "name": "sender_signature",
              "value": "株式会社〇〇 経理担当",
              "type": "string"
            },
            {
              "id": "c04",
              "name": "min_days_overdue",
              "value": 1,
              "type": "number"
            },
            {
              "id": "c05",
              "name": "max_customers",
              "value": 10,
              "type": "number"
            },
            {
              "id": "c06",
              "name": "timezone",
              "value": "Asia/Tokyo",
              "type": "string"
            },
            {
              "id": "c07",
              "name": "today_override",
              "value": "",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000004",
      "name": "未決済の取引を読む",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        100
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/api/1/deals?company_id={{ $json.freee_company_id }}&type=income&status=unsettled&limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000005",
      "name": "freee の取引先を読む",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        100
      ],
      "parameters": {
        "url": "=https://api.freee.co.jp/api/1/partners?company_id={{ $('設定').first().json.freee_company_id }}&limit=3000",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000006",
      "name": "期日を過ぎた取引を探す",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        100
      ],
      "parameters": {
        "jsCode": "// Past the due date with an amount still due, grouped by customer. Customers without an e-mail get no draft.\nconst c = $('設定').first().json;\nconst deals = $('未決済の取引を読む').first().json;\nconst ps = $json;\nconst summary = { problem: '', drafts: [], no_email: [], no_partner: [], count: 0 };\nif (deals.statusCode !== 200) summary.problem = 'freee の取引（売上）を読めませんでした（HTTP ' + deals.statusCode + '）。アプリの取引の権限を確かめてください。';\nelse if (ps.statusCode !== 200) summary.problem = 'freee の取引先を読めませんでした（HTTP ' + ps.statusCode + '）。';\nconst today = c.today_override ? DateTime.fromISO(c.today_override, { zone: c.timezone }) : DateTime.now().setZone(c.timezone);\nconst cut = today.minus({ days: Number(c.min_days_overdue) - 1 }).toFormat('yyyy-MM-dd');\nconst byId = new Map(((ps.body && ps.body.partners) || []).map((p) => [p.id, p]));\nconst groups = new Map();\nif (!summary.problem) for (const d of deals.body.deals || []) {\n  if (!d.due_date || d.due_date >= cut || !(d.due_amount > 0)) continue;\n  const key = d.partner_id || 'none';\n  if (!groups.has(key)) groups.set(key, []);\n  groups.get(key).push({ number: d.ref_number || null, deal_id: d.id, issue_date: d.issue_date, due_date: d.due_date, amount: d.amount, due_amount: d.due_amount,\n    days_overdue: Math.round(today.startOf('day').diff(DateTime.fromISO(d.due_date, { zone: c.timezone }), 'days').days) });\n}\nconst customers = [];\nfor (const [key, items] of groups) {\n  summary.count += items.length;\n  const p = byId.get(key);\n  const total = items.reduce((s, x) => s + x.due_amount, 0);\n  if (!p) { summary.no_partner.push({ items, total }); continue; }\n  const entry = { partner_id: p.id, name: p.long_name || p.name, title: p.default_title && p.default_title !== '(空白)' ? p.default_title : '御中', contact: p.contact_name || '', email: String(p.email || '').trim(), items, total };\n  if (!entry.email) summary.no_email.push(entry); else customers.push(entry);\n}\ncustomers.sort((a, b) => b.total - a.total);\nconst take = customers.slice(0, Number(c.max_customers));\nif (customers.length > take.length) summary.problem = (customers.length - take.length) + ' 件のお客様は、次の回に回しました（max_customers）。';\nsummary.drafts = take.map((x) => x.name);\nsummary.today = today.toFormat('yyyy-MM-dd');\nconst yen = (v) => Number(v).toLocaleString('ja-JP') + '円';\nconst jaDate = (s) => { const d = DateTime.fromISO(s); return d.year + '年' + d.month + '月' + d.day + '日'; };\nconst out = take.map((x) => ({ json: { ok: true, summary, customer: x,\n  facts: x.items.map((i) => '- 請求書番号: ' + (i.number || '（番号なし）') + ' / 請求額: ' + yen(i.amount) + ' / お支払い期日: ' + jaDate(i.due_date) + ' / 未入金の額: ' + yen(i.due_amount) + (i.due_amount < i.amount ? '（一部ご入金済み: ' + yen(i.amount - i.due_amount) + '）' : '')).join('\\n')\n    + (x.items.length > 1 ? '\\n- 未入金の合計: ' + yen(x.total) : '') } }));\nreturn out.length ? out : [{ json: { ok: false, summary } }];"
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000007",
      "name": "書く下書きがある？",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000008",
      "name": "Claude Haiku 4.5",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [
        1320,
        320
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-haiku-4-5-20251001",
          "cachedResultName": "Claude Haiku 4.5"
        },
        "options": {}
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000009",
      "name": "お願いのメールを書く（AI）",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "promptType": "define",
        "text": "={{ '宛名: ' + $json.customer.name + ' ' + $json.customer.title + ($json.customer.contact ? '（ご担当: ' + $json.customer.contact + '様）' : '') + '\\n差出人の署名: ' + $('設定').first().json.sender_signature + '\\n\\n未入金の請求（この内容だけを使う）:\\n' + $json.facts }}",
        "messages": {
          "messageValues": [
            {
              "type": "SystemMessagePromptTemplate",
              "message": "You write a polite payment reminder e-mail in Japanese from a small company to a customer.\nUse only the facts given: the invoice numbers, the remaining amounts, the due dates. Write every invoice number, every remaining amount (\"未入金の額\") and every due date exactly as given. If an invoice was partly paid, say how much remains and thank them for the partial payment. Do not add any other amount, date, fee, interest or penalty. Do not threaten, do not mention legal action, collection agencies or late fees.\nSay that if the payment was already made and the messages crossed, they can ignore this mail. Ask them to tell you the planned payment date. End with the sender's signature exactly as given.\nReturn only JSON: {\"subject\": \"...\", \"body\": \"...\"}"
            }
          ]
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000010",
      "name": "下書きを確かめる",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "jsCode": "// Every number in the draft must come from freee. A draft that fails is not saved.\nconst src = $('期日を過ぎた取引を探す').all();\nconst nf = (s) => String(s ?? '').normalize('NFKC');\nconst BANNED = /延滞金|遅延損害金|損害金|利息|法的|訴訟|弁護士|裁判|債権回収|督促状を送付|差し押さえ/;\nreturn $input.all().map((item, i) => {\n  const x = src[i].json.customer;\n  let a;\n  try { a = JSON.parse(String(item.json.text ?? '').match(/\\{[\\s\\S]*\\}/)[0]); } catch (e) { return { json: { ok: false, customer: x, why: ['AI の答えが JSON ではなかった'] } }; }\n  const body = nf(a.body), subject = nf(a.subject);\n  const why = [];\n  const flat = body.replace(/,/g, '');\n  const yenIn = (v) => flat.includes(String(v) + '円') || flat.includes('¥' + v) || flat.includes('￥' + v);\n  for (const it of x.items) {\n    if (it.number && !body.includes(nf(it.number))) why.push('請求書番号 ' + it.number + ' がない');\n    if (!yenIn(it.due_amount)) why.push((it.number || '請求書') + ' の未入金の額 ' + it.due_amount + '円がない');\n    const d = DateTime.fromISO(it.due_date);\n    if (![it.due_date, d.year + '年' + d.month + '月' + d.day + '日', d.month + '月' + d.day + '日', d.toFormat('yyyy/MM/dd'), d.year + '/' + d.month + '/' + d.day].some((f) => body.includes(f))) why.push('期日 ' + it.due_date + ' がない');\n  }\n  // Every yen amount in the draft must be one of these\n  const allowed = new Set([x.total, ...x.items.flatMap((it) => [it.due_amount, it.amount, it.amount - it.due_amount])].map(Number));\n  const found = [...flat.matchAll(/(?:[¥￥]\\s?(\\d+)|(\\d+)\\s?円)/g)].map((m) => Number(m[1] || m[2]));\n  const extra = found.filter((v) => !allowed.has(v));\n  if (extra.length) why.push('freee にない金額：' + [...new Set(extra)].join(', '));\n  const bad = (body + subject).match(BANNED);\n  if (bad) why.push('「' + bad[0] + '」が入っている（脅し・手数料・法的な言葉は使わない）');\n  if (!a.subject || !a.body) why.push('件名か本文がない');\n  return { json: { ok: !why.length, customer: x, subject: a.subject, body: a.body, why } };\n});"
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000011",
      "name": "確認を通った？",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        -100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000012",
      "name": "Gmail に下書き保存",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        -200
      ],
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {
          "sendTo": "={{ $json.customer.email }}"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "c9000000-0000-4000-8000-000000000013",
      "name": "お知らせを書く",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        200
      ],
      "parameters": {
        "jsCode": "// One mail to the staff: drafts saved, drafts stopped and why, customers with no e-mail, sales with no customer.\nconst yen = (v) => Number(v).toLocaleString('ja-JP') + '円';\nconst first = $('期日を過ぎた取引を探す').first().json;\nconst s = first.summary;\nconst checked = $('下書きを確かめる').isExecuted ? $('下書きを確かめる').all().map((i) => i.json) : [];\nconst line = (it) => '  ' + (it.number || '（番号なし）') + '：未入金 ' + yen(it.due_amount) + '、期日 ' + it.due_date + ' から ' + it.days_overdue + '日' + (it.due_amount < it.amount ? '（一部入金済み、請求額 ' + yen(it.amount) + '）' : '');\nconst L = [];\nif (s.problem) L.push('!! ' + s.problem, '');\nconst okd = checked.filter((x) => x.ok), bad = checked.filter((x) => !x.ok);\nif (okd.length) { L.push('Gmail にお願いのメールを下書き保存しました（送っていません。確かめて、ご自身で送ってください）：'); okd.forEach((x) => { L.push('- ' + x.customer.name + ' <' + x.customer.email + '> 合計 ' + yen(x.customer.total)); x.customer.items.forEach((it) => L.push(line(it))); }); L.push(''); }\nif (bad.length) { L.push('下書きを保存しなかったもの（確認で止めました。手で書いてください）：'); bad.forEach((x) => { L.push('- ' + x.customer.name + '：' + x.why.join('、')); }); L.push(''); }\nif (s.no_email.length) { L.push('freee にメールアドレスがないお客様（下書きなし）：'); s.no_email.forEach((x) => { L.push('- ' + x.name + ' 合計 ' + yen(x.total)); x.items.forEach((it) => L.push(line(it))); }); L.push(''); }\nif (s.no_partner.length) { L.push('freee で取引先が入っていない取引：'); s.no_partner.forEach((x) => x.items.forEach((it) => L.push(line(it)))); L.push(''); }\nconst n = s.count;\nconst title = '[n8n 入金遅れ] ' + (n ? '期日を過ぎた取引 ' + n + '件、下書き ' + okd.length + '件' : '期日を過ぎた取引はありません') + '（' + s.today + '）';\nif (!n && !s.problem) L.push('期日を過ぎて、未入金の残っている取引はありません。');\nL.push('freee会計から読んだもの：未決済の収入の取引（最初の100件）。');\nreturn [{ json: { title, message: L.join('\\n'), to: $('設定').first().json.staff_email } }];"
      },
      "executeOnce": true
    },
    {
      "id": "c9000000-0000-4000-8000-000000000014",
      "name": "担当者に知らせる",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2200,
        200
      ],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.to }}",
        "subject": "={{ $json.title }}",
        "emailType": "text",
        "message": "={{ $json.message }}",
        "options": {
          "appendAttribution": false
        }
      }
    },
    {
      "id": "c9000000-0000-4000-8000-000000000015",
      "name": "下書きの失敗を知らせる",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        -100
      ],
      "parameters": {
        "jsCode": "const err = $json.error && ($json.error.message || $json.error);\nreturn [{ json: { title: '[n8n 入金遅れ] 下書きを保存できませんでした', message: 'Gmail が下書きの保存を受け付けませんでした' + (err ? '：' + String(err).slice(0, 300) : '') + '。あるはずの下書きは、お知らせのメールに載っています。Gmail を確かめてください。', to: $('設定').first().json.staff_email } }];"
      },
      "executeOnce": true
    },
    {
      "parameters": {
        "content": "## freee の入金遅れに、お支払いのお願いメールを下書き\n平日の朝、**freee会計の未決済の売上**を読み、**期日を過ぎて未入金の残っている取引**をお客様ごとにまとめます。メールアドレスのあるお客様には、**AI（Claude）がていねいなお願いのメール**を書きます。**コードが中の数字をすべて freee と照らします**：請求書番号、未入金の額（請求額ではなく）、期日、それ以外の金額がないこと。脅し・延滞金・法的な言葉は止めます。通ったものは **Gmail の下書きに保存するだけで、何も送りません。** 担当者には1通のお知らせ：保存した下書き、止めた下書きとその理由、メールアドレスのないお客様。\n\n### 準備\n1. freee Developers のアプリ：取引（参照）と取引先（参照）の権限。請求書番号をメールに入れたいときは、取引の「管理番号」に入れておく\n2. n8n の認証情報：freee は「OAuth2 API」（認可の URL https://accounts.secure.freee.co.jp/public_api/authorize 、トークンの URL .../public_api/token 、Authentication は Body）。Gmail と Anthropic も\n3. **設定**（事業所 ID、担当者のメールアドレス、署名）を埋める。まず **今すぐ動かす** で試す。過去の日付で試すときは today_override に入れる",
        "height": 1000,
        "width": 600,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -320
      ],
      "id": "c9000000-0000-4000-9000-000000000015",
      "name": "Overview"
    },
    {
      "parameters": {
        "content": "## 1. freee を読む\n未決済の収入の取引（最初の100件）と、取引先（名前・敬称・メールアドレスのため）。",
        "height": 520,
        "width": 840,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -20,
        -120
      ],
      "id": "c9000000-0000-4000-9000-000000000016",
      "name": "Section 1"
    },
    {
      "parameters": {
        "content": "## 2. 期日を過ぎたものを、お客様ごとに\n期日を過ぎ（min_days_overdue）、未入金が残っているもの。メールアドレスのないお客様は、下書きを作らずお知らせに載せる。",
        "height": 520,
        "width": 400,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        860,
        -120
      ],
      "id": "c9000000-0000-4000-9000-000000000017",
      "name": "Section 2"
    },
    {
      "parameters": {
        "content": "## 3. AI が書き、コードが確かめる\nAI に渡すのは事実だけ。番号・未入金の額・期日が抜けている、ほかの金額が入っている、延滞金や法的などの言葉がある下書きは、コードが止める。",
        "height": 820,
        "width": 640,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1280,
        -320
      ],
      "id": "c9000000-0000-4000-9000-000000000018",
      "name": "Section 3"
    },
    {
      "parameters": {
        "content": "## 4. Gmail の下書きとお知らせ\n下書きは、人が読んで送るために保存するだけ。担当者に、見つけたことをまとめて1通で知らせる。",
        "height": 820,
        "width": 520,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1940,
        -320
      ],
      "id": "c9000000-0000-4000-9000-000000000019",
      "name": "Section 4"
    }
  ],
  "connections": {
    "平日の朝9時": {
      "main": [
        [
          {
            "node": "設定",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "今すぐ動かす": {
      "main": [
        [
          {
            "node": "設定",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "設定": {
      "main": [
        [
          {
            "node": "未決済の取引を読む",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "未決済の取引を読む": {
      "main": [
        [
          {
            "node": "freee の取引先を読む",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "freee の取引先を読む": {
      "main": [
        [
          {
            "node": "期日を過ぎた取引を探す",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "期日を過ぎた取引を探す": {
      "main": [
        [
          {
            "node": "書く下書きがある？",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "書く下書きがある？": {
      "main": [
        [
          {
            "node": "お願いのメールを書く（AI）",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "お知らせを書く",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Haiku 4.5": {
      "ai_languageModel": [
        [
          {
            "node": "お願いのメールを書く（AI）",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "お願いのメールを書く（AI）": {
      "main": [
        [
          {
            "node": "下書きを確かめる",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "下書きを確かめる": {
      "main": [
        [
          {
            "node": "確認を通った？",
            "type": "main",
            "index": 0
          },
          {
            "node": "お知らせを書く",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "確認を通った？": {
      "main": [
        [
          {
            "node": "Gmail に下書き保存",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "お知らせを書く": {
      "main": [
        [
          {
            "node": "担当者に知らせる",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail に下書き保存": {
      "main": [
        [],
        [
          {
            "node": "下書きの失敗を知らせる",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "下書きの失敗を知らせる": {
      "main": [
        [
          {
            "node": "担当者に知らせる",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}