Rate limiting:
- Shop POST endpoints get per-IP throttles with distinct prefixes so login,
register, password-email, and password-reset have separate buckets:
login/register/pw-reset = 5/min, pw-email = 3/min
- OcrInvoiceService gates per-tenant via RateLimiter (30/hour) so a runaway
uploader can't burn Claude Vision spend
Health monitor (poor-man's monitoring):
- HealthCheckCommand probes DB (SELECT 1), cache write/read, public storage
write/read, and most-recent backup age. On any failure, pushes a Telegram
alert via HEALTH_ALERT_BOT_TOKEN/HEALTH_ALERT_CHAT_ID. Dedups identical
failures within a 30-min window via cache.
- Scheduled every 10 min. Pair with external uptime monitoring (UptimeRobot,
Better Stack hitting /up) for total-outage coverage.
- .env.example documents the two new env vars.
VAPID secret hygiene:
- credentials.md no longer stores the VAPID_PRIVATE_KEY; the source of truth
is the Coolify env on the autocrm app. Doc points to where to read it
(UI or API). Mitigates accidental git leak.
Tests (4 new):
- shop login throttles after 5 attempts (6th = 429); register throttle is
independent of login (separate prefix); health command runs clean; dedup
cache path exercised
Full suite: 138 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upload an invoice photo → Claude extracts {supplier_name, date, currency,
items, total} as JSON → auto-create a draft Purchase + PurchaseItems →
redirect to edit so the user reviews before confirming/receiving.
OcrInvoiceService:
- Validates supported MIME (jpg/png/webp/gif)
- Reads tenant Claude key (settings.ai.claude_key) — friendly error if missing
- Calls /v1/messages with image content block + structured-output system prompt
- Tolerant parser: strips ```json fences, falls back to first {…} block
- normalize(): computes per-item total when absent, fills overall total
- All return shapes: {ok:bool, data?, error?, raw?, tokens?}
Filament:
- "Import factură (OCR)" header action on Purchases list
- Image file upload → service → matches Supplier by case-insensitive name
(notes the unmapped name if no match) → creates draft Purchase + items →
redirects to the Edit page
Tests (6 new):
- clean JSON parses; markdown fences stripped; malformed → graceful error;
missing key → friendly message + no HTTP; unsupported MIME rejected;
item total computed when missing
Full suite: 123 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>