Invoice Chaser: calculate late fees and choose a reminder stage
Check invoice age, contract-specified simple-interest fees and reminder history using Kitwise Studio's pinned free sample, with repeatable calculations kept separate from Claude and email drafting.
Run this workflow
CI-verified, 5/5 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
A freelancer checking an ordinary cent-denominated invoice under explicitly supplied contract terms and reviewing an age- and reminder-history-based next stage. The public MIT sample is enough; the paid pack is separate and unnecessary for this recipe.
Not for
- Fractional-cent money, extreme numeric values, arbitrary currency precision, partial-payment ledgers, compound interest or different contractual day-count methods
- Treating the default 1.5% as a right to charge a fee, or treating this calculation as legal or accounting advice
- Calling the weekly estimate the exact difference between rounded payable balances
- Claiming the complete paid Invoice Chaser pack, email drafting, delivery or the new Claude skill was verified
- Automatically sending reminders, escalating disputes, waiving fees or contacting clients
The Stack
Tested Against
Kitwise Invoice Chaser free sample@1.0.0October 30 README-only archive@sha256:3c9afa69bfb82f7a0164897534be1031e880f0ad3df3f9d7af6cce99fba5302dPython 3 standard library; 18 upstream unittest methods plus FlowStacks checksClaude.ai sample skill: not executed; model output is outside verificationSide effects & data flow
- Network
- FlowStacks public download only when obtaining the pinned MIT sample; CI uses the same checked-in artifact
- Writes
- A new ./kitwise-invoice-chaser-flowstacks/ sample directory; the MIT notice is also copied into the separately uploadable skill folder
- Credentials
- None for CI or the calculator; your own Claude account only for optional skill use
Data privacy
- No external service during calculations ← The CI example is synthetic; local invoice inputs remain in the Python process (retention: Your local files and terminal history are under your control)
- Anthropic only if you choose to use the optional Claude skill ← Any invoice or client context you voluntarily provide; use synthetic or minimized details (retention: Per your Claude account settings and Anthropic's policies; no such call is made in CI)
Prerequisites
- Python 3, bash and curl for the sample check; no API key, paid service or real invoice is needed
- For your own calculation: ordinary amounts and flat fees in whole cents, an explicit date, due date and actual agreed fee terms; use rate 0 if no fee was agreed
- Optional Claude.ai account with code execution and custom skills enabled; client availability and limits are separate from the offline calculation
- Human review of contractual applicability and every proposed client communication
Steps
- 1
Obtain the exact MIT sample and run its offline checks
Run in a fresh directory. The script uses the reviewed archive bundled with FlowStacks, checks its SHA-256 and exact file inventory before extraction, retains the full MIT notice, and executes the published example and 18 upstream tests. The October 30 ZIP has a different checksum from the October 24 submission because its README and changelog were updated; the calculator remains unchanged. Nothing from the paid pack is required. This step does not install a skill in your account or send an invoice anywhere.
#!/bin/sh # Execute the reviewed MIT sample unchanged. No Claude account or real invoice is used. set -eu OUTPUT_DIR=kitwise-invoice-chaser-flowstacks ARCHIVE=kitwise-invoice-chaser-sample-v1.0.0-3c9afa69.zip if [ -e "$OUTPUT_DIR" ]; then echo "Refusing to overwrite $OUTPUT_DIR; use a fresh directory." >&2 exit 1 fi mkdir "$OUTPUT_DIR" cd "$OUTPUT_DIR" # CI uses the checked-in artifact; readers can download the same public copy. if [ -n "${FLOWSTACKS_KITWISE_ARCHIVE:-}" ]; then cp "$FLOWSTACKS_KITWISE_ARCHIVE" sample.zip elif [ -n "${GITHUB_WORKSPACE:-}" ] && [ -f "$GITHUB_WORKSPACE/public/downloads/$ARCHIVE" ]; then cp "$GITHUB_WORKSPACE/public/downloads/$ARCHIVE" sample.zip else curl --disable --proto '=https' --tlsv1.2 --max-time 30 --max-filesize 131072 -fsS \ "https://flowstacks.xyz/downloads/$ARCHIVE" -o sample.zip fi python3 -I <<'PY' import hashlib import json import pathlib import subprocess import sys import zipfile from decimal import Decimal archive = pathlib.Path('sample.zip').read_bytes() assert hashlib.sha256(archive).hexdigest() == '3c9afa69bfb82f7a0164897534be1031e880f0ad3df3f9d7af6cce99fba5302d', 'sample checksum changed' prefix = 'invoice-chaser-sample-v1.0.0/' expected = { 'CHANGELOG.md', 'LICENSE', 'README.md', 'expected-script-output.json', 'run-script.sh', 'invoice-chaser/SKILL.md', 'invoice-chaser/scripts/late_fee.py', 'invoice-chaser/references/fee-method.md', 'invoice-chaser/references/stages.md', 'tests/test_late_fee.py', } with zipfile.ZipFile('sample.zip') as bundle: entries = bundle.infolist() assert len(entries) == len(expected) assert {item.filename for item in entries} == {prefix + name for name in expected} assert sum(item.file_size for item in entries) < 65536 for item in entries: path = pathlib.PurePosixPath(item.filename) assert not path.is_absolute() and '..' not in path.parts and '\\' not in item.filename assert (item.external_attr >> 16) & 0o170000 != 0o120000, 'symlink not allowed' bundle.extractall('.') root = pathlib.Path(prefix).resolve() license_text = (root / 'LICENSE').read_text() assert 'MIT License' in license_text and 'Copyright (c) 2026 Kitwise Studio' in license_text for name in ['scripts/late_fee.py', 'references/stages.md', 'references/fee-method.md']: assert (root / 'invoice-chaser' / name).is_file() # Keep the copyright/permission notice with a separately uploaded skill folder too. (root / 'invoice-chaser' / 'LICENSE').write_text(license_text) print('Kitwise source pinned OK: sample 1.0.0, SHA-256 3c9afa69, MIT notice retained') runner = subprocess.run(['bash', 'run-script.sh'], cwd=root, capture_output=True, text=True, timeout=20) assert runner.returncode == 0 and 'PASS: output matches' in runner.stdout, runner.stdout + runner.stderr tests = subprocess.run([sys.executable, '-I', 'tests/test_late_fee.py'], cwd=root, capture_output=True, text=True, timeout=30) assert tests.returncode == 0 and 'Ran 18 tests' in tests.stderr, tests.stdout + tests.stderr print('Kitwise upstream checks OK: exact example and 18 regression tests') script = root / 'invoice-chaser/scripts/late_fee.py' def calculate(**overrides): values = {'amount': '2400', 'due': '2026-09-15', 'today': '2026-10-16', 'monthly-rate': '0.015'} values.update({key.replace('_', '-'): str(value) for key, value in overrides.items()}) args = [sys.executable, '-I', str(script)] for key, value in values.items(): args.extend(['--' + key, value]) done = subprocess.run(args, capture_output=True, text=True, timeout=5) result = json.loads(done.stdout, parse_float=Decimal, parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value))) return done.returncode, result code, normal = calculate() assert code == 0 and normal['days_overdue'] == 31 and normal['late_fee'] == Decimal('36.69') assert normal['total_due'] == Decimal('2436.69') and normal['stage'] == 3 assert calculate(cap_pct='0.01')[1]['fee_per_extra_week'] == 0 assert calculate(monthly_rate='0')[1]['late_fee'] == 0 assert calculate(amount='91.25', today='2026-09-16')[1]['late_fee'] == Decimal('0.05') assert calculate(today='2026-09-25', reminders_sent=2)[1]['stage'] == 3 assert calculate(today='2026-09-10', reminders_sent=4)[1]['stage'] == 0 for override, field in [({'amount': 'nan'}, 'amount'), ({'amount': '0'}, 'amount'), ({'grace': '-1'}, 'grace'), ({'monthly_rate': '-0.01'}, 'monthly_rate')]: code, result = calculate(**override) assert code == 2 and result['field'] == field and 'error' in result print('Kitwise independent checks OK: cents, half-up rounding, cap, no fee, reminder history and JSON errors') print('Scope: cent-denominated inputs; weekly growth is an estimate; fractional-cent behavior is not corrected') print('No Claude execution, email drafting or sending, paid pack, or legal entitlement was verified') PY printf 'Sample and MIT license are in %s/invoice-chaser-sample-v1.0.0/\n' "$OUTPUT_DIR" - 2
Calculate using your actual terms and an explicit date
From kitwise-invoice-chaser-flowstacks/invoice-chaser-sample-v1.0.0, run: python3 invoice-chaser/scripts/late_fee.py --amount 2400 --due 2026-09-15 --today 2026-10-16 --monthly-rate 0.015 --reminders-sent 2. Replace every value with your own agreed terms; rate 0 is appropriate if no fee was agreed. Use whole-cent principal and flat fees only. The fixed example gives stage_by_days 3 and stage 3; reminders-sent means the highest stage previously sent, not a count of emails. Weekly growth is an estimate from unrounded fees: this example's rounded balance rises by 8.29 over seven days even though the weekly estimate is 8.28. Do not use fractional-cent or extreme inputs, and stop on errors or unreconciled totals.
- 3
Optionally inspect the new skill in Claude
Package the extracted invoice-chaser folder, including its copied LICENSE, as a ZIP and upload it using Claude's custom-skill controls with code execution enabled. The free sample explains calculation and stage selection only. Ask it to run the script with synthetic inputs first and compare every number yourself. FlowStacks has not executed this new skill in Claude. It contains no email templates; any separate drafting step and all client communications require your own review and are outside the badge.
Eval, 5 fixtures
Last passed: verified todaysource-and-licensecontainstimeout 120s · max $0Expected:
Kitwise source pinned OK: sample 1.0.0, SHA-256 3c9afa69, MIT notice retainedupstream-regressionscontainstimeout 120s · max $0Expected:
Kitwise upstream checks OK: exact example and 18 regression testsindependent-calculationcontainstimeout 120s · max $0Expected:
Kitwise independent checks OK: cents, half-up rounding, cap, no fee, reminder history and JSON errorsscope-keptcontainstimeout 120s · max $0Expected:
No Claude execution, email drafting or sending, paid pack, or legal entitlement was verifiedclean-exitexit_codetimeout 120s · max $0Expected:
0
Results
CI checks the exact archived MIT sample, the published example and all 18 upstream regression tests, then independently checks cent-denominated calculations, half-up rounding, fee caps, no-fee terms, reminder history and structured invalid-input errors. The published example is 31 days overdue, fee 36.69, total 2436.69, stage 3 and estimated weekly growth 8.28. Source code is unchanged: fractional-cent inputs can yield inconsistent displayed totals and very large numbers can fail, so this recipe is limited to ordinary two-decimal amounts. Weekly growth is based on unrounded fees and can differ by a cent from the change in rounded balances. The new sample skill has not been run in Claude; the older paid skill's historical run does not verify this sample.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- Yoya + Claude Code: inspect booking tools before confirming a meeting
- Mastra: stop on incomplete private soccer moneyline outcome sets
- Self-hosting the open-source stack? Prove your backup actually restores before you need it
- Chat with a CSV, but pin a known-answer guardrail so a wrong query cannot pass
- Advisor pattern: cap how often the expensive model gets called, and catch drift
- Route through a gateway with a tested open-weights fallback
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).