Reconciles a HomeCare "run" (today's date + category_secondary, the same filter
HomeCareRunContext already drives on inv/index) via a paper backup channel:
- Export (
GET /homecarerunsheet,POST /homecarerunsheet/export) — snapshots the run's invoices into aHomeCareRunSheet+ oneHomeCareRunSheetItemper invoice, sorted in route order byDwelling::house_number_numeric, and writes a printable CSV to disk (columns: seq, invoice id, client, house number, assigned worker, plus blank columns for the field to fill in by hand, and aDoNotSendReasonlegend). - Printed and hand-annotated in the field — the worker who actually did the job, whether it was completed, and a coded do-not-send reason if not.
- Scan (
POST /homecarerunsheet/upload/{id}) — the annotated sheet is photographed/scanned back in and stored undersrc/Invoice/Uploads/HomeCare_run_sheets/; Claude Haiku 4.5 reads the handwriting via a structured-output vision call (a closed-set schema enumerating the real invoice ids, active worker ids, andDoNotSendReasoncodes on that sheet) and writes the detected worker/completion/reason onto eachHomeCareRunSheetItem. - Review (
GET /homecarerunsheet/review/{id}) — a staging screen shows only the rows the vision read actually changed from what was printed (HomeCareRunSheetItem::hasDetectedChange()), so the office reviews adjustments, not the whole run. - Apply (
POST /homecarerunsheet/apply/{id}) — accepted adjustments are written back ontoInv(worker reassignment viasetWorker(),do_not_send— set only, never cleared, by design), and the whole run is batch-flipped to "sent" via the newInvMarkSentService, which delegates the actual status transition to the existingSettingRepository::invoiceMarkSent()and additionally writes anInvSentLogentry per invoice.
Settings → HomeCare has a "Run Sheet Vision Scan" card holding the Anthropic API key (a
password-type field, with a "Get API Key ↗" link to
https://console.anthropic.com/settings/keys), and "Run Sheets" is in the Invoice nav
dropdown alongside "Workers". The Recent Run Sheets index shows category and item-count per
batch.
HomeCareRunSheet's lifecycle is Exported → Scanned → PendingReview → Applied
(HomeCareRunSheetStatus); a batch that fails partway through a scan stays at Scanned and
can be retried from the Review screen rather than being stuck.
Export and Review are live-verified end-to-end against real invoices and a real Dwelling
row on local MySQL/WAMP: correct HomeCareRunSheet/HomeCareRunSheetItem rows, correct CSV
on disk with house_no correctly resolved through client.dwelling_id, and the Review
screen rendering correctly at every status. The vision call itself is confirmed to reach and
authenticate against the Anthropic API with the configured key — the only thing currently
blocking a full end-to-end run is account credit, not a code issue. A full live Apply run
(a batch actually reaching PendingReview with real detected changes) hasn't happened yet
for the same reason.
All 7 classes in the feature have Testo unit test coverage — 40 tests, added in this session:
| Test file | Covers |
|---|---|
Tests/Testo/Infrastructure/Persistence/HomeCareRunSheet/HomeCareRunSheetTest.php |
Entity lifecycle — markScanned/markPendingReview/markApplied, reqId() guards |
.../HomeCareRunSheetItem/HomeCareRunSheetItemTest.php |
hasDetectedChange() — the shared predicate the staging query and Apply step both key off, gated on detected_completed rather than detected_worker_id so an illegible-handwriting-but-marked-incomplete row still counts as changed |
Tests/Testo/Invoice/HomeCareRunSheet/HomeCareRunSheetApplyServiceTest.php |
Refuses a non-PendingReview batch; worker reassignment; do_not_send set-never-clear rule; a rejected row left untouched but still included in the mark-sent pass; invoice-not-found skip |
.../HomeCareRunSheetVisionServiceTest.php |
The two guard clauses before the real Anthropic\Client call — empty items, missing API key |
.../HomeCareRunSheetExportServiceTest.php |
Idempotent re-fetch of an already-open batch; route-order sort with no-dwelling invoices sorting last; full CSV content verified byte-for-byte off real temp-dir file I/O |
.../HomeCareRunSheetScanServiceTest.php |
Scan file written to disk with the right extension per MediaType (jpg/png/gif/webp); Scanned → PendingReview transition order |
Tests/Testo/Invoice/Inv/InvMarkSentServiceTest.php |
Draft/blocked/not-found guards, InvSentLog write, per-invoice independence |
Conventions: real new Inv()/new Worker()/new HomeCareRunSheet*() value objects rather
than mocking entities, Mockery-mocking only repository/service collaborators, and real
temp-dir file I/O for the two file-writing services (HomeCareRunSheetExportServiceTest,
HomeCareRunSheetScanServiceTest — mirrors UploadServiceTest's deleteUpload convention).
vendor/bin/psalm --no-cache on all 7 new files: 0 errors. Full project Testo suite:
1048/1048 passing.
Existing app patterns reused as-is: HomeCareRunContext, inv/setworker's
worker-reassignment pattern, the DoNotSendReason enum, Inv::do_not_send/
blocksSending(), Dwelling::house_number_numeric, the fputcsv/php://temp CSV
convention, the Form() raw-HTML wrapper, and
CategorySecondaryRepository::optionsDataCategorySecondaries(). The scanned sheet is stored
as a bare filename string against the batch (CompanyPrivate::logo_filename convention)
rather than through the Upload entity, since Upload is client-scoped and doesn't fit a
document spanning one run's many clients.
New dependency: anthropic-ai/sdk:^0.44.0. Model: Claude Haiku 4.5, chosen for a bounded
closed-set extraction task with structured JSON-schema output.
| File | Change |
|---|---|
config/common/di/homecare.php |
Repository interface bindings |
resources/views/invoice/setting/views/partial_settings_homecare.php |
Vision API key field |
resources/views/invoice/setting/views/partial_settings_two_factor_authentication.php |
2FA compulsory (see below) |
resources/views/invoice/homecarerunsheet/index.php, review.php |
Review screen states |
resources/views/layout/invoice.php |
"Run Sheets" nav entry |
src/Invoice/Setting/Trait/SettingFileFolderTrait.php, SettingStaticPathsTrait.php, SettingTooltipTrait.php |
Vision key setting plumbing/tooltip |
src/Invoice/Trait/InvoiceInstallTrait.php |
Default settings for fresh installs |
src/Invoice/Inv/InvMarkSentService.php |
New — the "mark as sent" half of the Apply step |
resources/messages/en/app.php |
New translation keys |
7 new Tests/Testo/** files |
See table above |
Settings → TFA's enable_tfa checkbox is now permanently checked and disabled (decorative
only), and always submits 1 — two-factor authentication is compulsory for every account
rather than optional.
Full Testo Unit suite: 1048/1048 passing. Full-project vendor/bin/psalm --no-cache: 0
errors on every new/changed file. Export, Review, and the vision-call auth path are
live-verified against real MySQL/WAMP; the vision extraction itself and a full live Apply
run are pending Anthropic account credit.