Vendor Lifecycle: Coordination → Job Done → Paid

Internal brief · 2026-07-30 · Camellia Apartments (org_jpco, property 1773625953462) · read-only mining, zero writes · no tenant names anywhere in this document

1. The arc

Coordination is the beginning. Job done is the middle. Payment is the end. One job, one thread, no re-typing. That is the whole product thesis for vendors: a PM should never have to re-key a job into a second system because it moved from "we booked it" to "they finished it" to "we owe them $1,038.61".

Beginning — coordination
Vendor calling
Tonight's work. Production-ready: outbound dial, company-first recognition, schedule write-back. Shipped.
Middle — job done
Completion signal
Today: 32 signals found for 2026, of which only 4 are structured work-order rows. This doc — detection only.
End — paid
Invoice & amount
$39,583.61 of real 2026 spend sits in the mailbox and in zero PropFlow records. This doc — detection only.
Scope of this document. The beginning of the arc is built. This brief covers the middle and the end, and it is deliberately detection-only: read the signal, store it, show it. No accounting writes, no AppFolio push, no PM notifications, no payment. Everything below is either measured from production or costed from code that already exists.

What we found, in one paragraph

Camellia's mailbox already carries the entire back half of the lifecycle in structured, parseable form — 443 vendor emails, 220 PDF attachments, 44 unique invoices, $39,583.61. The classifier already reads those emails; it just throws them away. The PDF parser that would extract the amounts already runs in the production inbound-email Lambda. The UI stepper that would display the lifecycle is already mounted on the work-order page. Almost none of this is a build — it is wiring, plus one policy decision about which vendor senders we trust.

2. What Camellia's 2026 mail actually contains

Window: 2026-01-01 → 2026-07-29. Sources: production DynamoDB (read-only) plus the live Camellia mailbox via the app's production Microsoft Graph integration (Mail.Read only). Every dollar figure below was extracted literally from downloaded PDF text — none inferred, none estimated.

2026 invoice spend
$39,583.61
44 unique invoices
Vendor emails
443
of 2,468 inbox messages
PDFs parsed
220
181 text layer · 39 OCR · 0 failed
Schedule signals
101
confirmation of a visit
Job-done signals
32
21 email · 7 SMS · 4 WO rows
Attributed to a turnover
$264.42
0.7% of spend

Categories in the mail stream

CategoryCountWhat it is / real example
Vendor emails identified443The categorized universe for 2026
Schedule confirmations101"we'll be out Tuesday" — Miracle Method, Western States Fire Protection, Conservice prebill scheduling
Job-done signals32Chem-Aqua monthly Field Service Report (site ID 429108, narrative in the body); American Garage Door line items reading COMPLETE - 6/22/26 - AO/JA; 7 vendor SMS; 4 typed into a work order
Invoice emails5147 with an amount literally extracted; 44 unique after de-duplicating vendor re-sends
Quotes / estimates228 priced, $7,229.36 — not committed spend. Royal Textile is quote-heavy: 10 quotes vs 2 invoices
Aggregate statements14Anchor's statements_bill.pdf, TK's Statement of Account — excluded to avoid double counting
High-volume, zero-billingComm-Fit (20), EcoFlo Supply (22), Ally Waste (21), EnergyScapes (17), Surface Experts (14) — cold outreach only

Spend by vendor — 2026 YTD, unique invoices only

VendorInvoicesTotalCharacter
MTech Mechanical (plumbing)4$10,820.87Project + per-call plumbing; best attribution in the portfolio
Apartments.com (CoStar) via Billtrust7$9,653.00Flat $1,379.00/mo advertising
TK Elevator4$5,721.05Monthly maintenance contract
The Home Depot5$3,148.25On-site tech material purchases
Anchor Pest Control & Wildlife15$2,961.00$333.00/mo recurring + $12–$215 callouts
American Garage Door5$2,598.61Per service call
Conservice (Denver Water pass-through)1$2,323.44Utility, property-level
Royal Textile Products1$1,370.39Install after quote
Alpine Glass2$987.00Glass repair
Total44$39,583.61

Spend by month

MonthInvoicesTotal
2026-014$2,249.00
2026-023$2,097.00
2026-038$6,414.54
2026-048$7,111.95
2026-056$5,072.64
2026-067$11,882.03
2026-07 (to 07-29)8$4,756.45
Total44$39,583.61

* July is partial (through 07-29). June is inflated by a single property-level job: MTech invoice 158334-1, $8,387.00 — "Replace Meter Pit Valves".

Proof: 12 real invoices parsed end to end

Fields extracted mechanically from downloaded PDF text — no manual entry, no guessing. Amount evidence strings are stored per row (e.g. PDF JORDAN PERLMUTTER & CO 162425.pdf: Total 1,616.00).

VendorDateInvoice #AmountPO / job refUnit
MTech Mechanical2026-04-02162425$1,616.00Customer PO 591322
MTech Mechanical2026-05-22164430$553.45Customer PO 262207
MTech Mechanical2026-06-16165501$264.42422
American Garage Door2026-01-0893390$440.00
American Garage Door2026-06-2395245$1,038.61
TK Elevator2026-05-213009463147$1,378.80PO # (blank on doc)
Apartments.com / CoStar2026-06-02124138040$1,379.00
Anchor Pest Control2026-04-0279616$128.00222
Royal Textile Products2026-05-20251742$1,370.39PO Number 590117
The Home Depot2026-04-02H1505-716699$949.36PO / Job Name 596
Alpine Glass2026-03-209974$602.00
Denver Water (via Conservice)2026-04-16$2,323.44

Attribution: how much of the spend attaches to a job

Attribution confidenceInvoicesAmount
exact_po — document carries a purchase-order / job number8$6,688.09
unit_and_vendor — unit ref + matching work order / turnover1$264.42
vendor_and_date — unit ref, no matching job in window2$2,451.44
none33$30,179.66
Actually attributed to a turnover1$264.42

The one attributed invoice is MTech 165501, unit 422, against turnover turnover_d188d1f2… (opened 2026-07-08). Everything else is either recurring contract spend with no job reference, or carries a purchase-order number we cannot resolve — which is the headline finding.

Headline callout: vendor invoices carry AppFolio purchase-order numbers, and we have zero purchase-order rows.

Real PO numbers printed on real Camellia invoices in 2026: 262, 587, 590, 591, 592, 593, 595, 596. The Home Depot alone had 5 of 5 receipts carrying a job/PO number.

The PurchaseOrder entity (PK=PROP#<propertyId>, SK=PO#<externalPurchaseOrderId>) has 0 rows in production, portfolio-wide. listPurchaseOrders has never been populated for any property.

Consequence: $6,688.09 of 2026 spend already carries a hard join key that resolves to nothing. Turning on PO sync for Camellia converts that — plus every future MTech, Home Depot, and Royal Textile invoice — from "unattributed" to hard job attribution, with no parsing improvement required. This is the single highest-leverage gap in the dataset.

Other gaps worth knowing before anyone builds on this

3. Detection architecture

The structural blocker, stated plainly. Vendor emails today are understood by the classifier and then discarded — they take the skip arm and are signal-only, so nothing durable is written: no conversation, no work-order touch. And vendor recognition is phone-only — a vendor is matched by the number they call from. An email sender is therefore structurally unreachable: an email_user:<address> claim fails phone normalization outright, so there is no path from brenda@americangaragedoor.net to a vendor company record, and vendor rows carry no email at all. Measured: only 6 of 2,513 Camellia inbound emails exact-match a known vendor email claim. That is why slice 0 below is an index, not a feature.

3a. The signal ladder

RungSignalWhere it lives today2026 volume
1Schedule confirmation — "we'll be out Tuesday" Inbound email, classified then discarded. Voice/SMS already write through the channel-agnostic outcome machinery.101
2Job done — completion narrative, FSR, "COMPLETE - 6/22/26" WorkOrder.completionNotes / completionPhotos / resolvedAt; vendor SMS on CONV# threads; the deterministic assistant marker "Work order <id> … has been marked complete."32
3Invoice + amount PDF attachment. Not persisted on the Graph path. Extraction happens nowhere today. 44

3b. Parser lanes and cost

There is nothing to rebuild. PropFlow already has a production multi-strategy PDF→JSON parser whose extraction prompt is literally titled "You are a vendor invoice/quote parser", and both of its legs already execute inside the production inbound-email Lambdapdf-parse (pure JS, bundled, no layer) for text-layer documents and native Claude PDF document blocks for scanned ones. The only local-only piece is an optional free Ollama pre-pass, which must be bypassed in Lambda rather than merely allowed to fail.

LaneWhenCredentialRuntimeCost
(a) Subscription runnerNOW — proof + 2026 backfill Claude subscription bearer, resolved automatically from the runtime; runner aborts with exit 2 if chooseCredential() returns anything but bearer Local tsx, sequential, resumable, throttle-aware $0 metered — subscription tokens only
(b) Metered LambdaLATER — live per-email extraction, ships dormant behind an off-by-default flag Metered key; AWS_LAMBDA_FUNCTION_NAME forces metered and there is deliberately no force-bearer override Existing inbound-email Lambda $0.003–$0.009 / invoice; <$1/month at Camellia volume

Blended per-invoice metered cost at 70% text-layer / 30% scanned: $0.0029 all-Haiku, $0.0059 Haiku-text + Sonnet-scanned, $0.0087 all-Sonnet. At an estimated 60 invoices/month that is $0.17–$0.52/month; a full ~700-document 2026 backfill batched is $1–$3 one-time. The approval ask for lane (b) is well under a dollar a month — so the model should be chosen on accuracy, not price.

Volume of 40–80 invoices/month (midpoint 60) is the one soft number here — it is an estimate, not a measurement. The 44 unique invoices this mining pass found are Camellia only, one property. Count the real s3://propflow-photos/email-attachments/ prefix before quoting the cost figure onward.

Parse proof — already run, on the subscription lane

15 of 15 parses succeeded across two runs against real vendor PDFs: Run A = 13/13 documents through the pdf-parse → Haiku text leg, 0 failures, 0 exceptions, 0 retries, no escalation triggered. Run B forced 2 of those documents through the native-PDF vision leg to exercise the branch the corpus never hit.

Both legs agreed on the money where they overlapped (3575 and 3650, exact match). That agreement is the most reassuring number in the run, because a wrong total is the expensive failure mode.

Combined spend: 45,895 subscription tokens (44,569 in / 1,326 out). $0 meteredANTHROPIC_API_KEY was deleted from the environment before the client module was imported. Dry-run throughout; nothing written, no Camellia data touched.

Separately, the mining pass parsed 220 of 220 Camellia PDFs — 181 via text layer, 39 via local OCR, 0 unparseable, 0 model tokens spent.

Two known parser weaknesses, both unfixed. (1) unitReference has a real false-positive mode — four documents returned Suite 665, almost certainly the vendor's own letterhead suite rather than the work location. The same class of error as American Garage Door's own Unit #113 in the Camellia corpus. The PO field has a "P.O. Box" guard; the unit field has no guard yet. Fix: require unit refs to validate against the Camellia roster (120 units) and sit adjacent to a GRANT ST / UNIT / S/M: UNIT marker. (2) The text leg is not always the better leg — on one document vision recovered an invoice date and number the text leg returned null for, while on another the text leg won. The ladder currently always prefers text when a text layer exists.

3c. What ships dormant vs. what runs as backfill

3d. Per-vendor format profiles (the parser spec, summarized)

VendorDeliveryKey fieldsQuirk to guard
MTech MechanicalPDF, JORDAN PERLMUTTER & CO <inv#>.pdf; invoice # also in subject Work Order, Service Location … #<unit>, Customer PO, Total Best attribution in the portfolio — the only vendor reliably carrying both unit and customer PO. Total has no $. They misspell their own "JORDON/JORDAN".
Anchor Pest ControlTwo families: per-service Invoice #<n>.pdf and monthly statements_bill.pdf Invoice #, Service date, Total / Amount due Treat the statement as a roll-up — never add it to spend, it re-states counted invoices.
TK Elevator<invoice#>.pdf, plus plain-text Statement of Account and amount-free dunning notices Clean labelled-colon layout: INVOICE #:, SERVICE DATE:, TOTAL DUE: $x Easiest vendor to parse. Same invoice number re-mailed on statement runs — dedupe required.
Apartments.com / BilltrustOne invoice per month, Acct No. 29316887 Invoice Number, Service Period, Invoice Amount USD Parse the PDF, not the body. A naive columnar PO scrape picks up 379 out of 1,379.00.
The Home DepotPOS receipt PDFs, H1505-<order#> Receipt.pdf PO / Job Name <n> ← the AppFolio PO number; per-SKU lines; Order Total Best PO coverage of any vendor (5/5). Receipt names the buying tech — a weak identity signal.
American Garage DoorInvoice per call; the body restates Amount Due: $x, parseable without opening the PDF Invoice #, Project: 1235 N Grant St., Balance Due Their own address contains Unit #113 — a naive unit regex mis-attributes. Line items carry a completion date, a job-done signal inside an invoice.
Royal Textile ProductsQuote first, invoice after install (10 quotes vs 2 invoices in 2026) PO Number (columnar — value under the header, not after a colon), S/M: UNIT <n> Total is right-aligned on a line whose left side is item text. Multiple units per document is normal.
Alpine GlassEstimates are scanned image PDFs with no text layer; invoices are native text Project: CR_Camellia-<unit> ← unit ref lives in the Project field, not line items Estimates need OCR; invoices don't. The clean split the two-leg ladder was built for.
ConserviceMonthly report packages, prebill approvals, forwarded Denver Water bills AMOUNT DUE on the Denver Water bill; account key ca734 in nearly every subject Highest email volume of any vendor (52) but almost all coordination, not billing. Reports packages are informational.
Chem-AquaMonthly Field Service Report, site ID 429108 Technician findings narrative in the email body Pure job-done signal — no amounts. The cleanest rung-2 source in the mailbox.

4. UI placement options

Almost nothing new needs to be invented. The app already has a shared progress rail driven by a pure server-side step derivation, a vendor-invoice rollup page, a dashboard stat-card grammar, and a registered mtdVendorSpend metric. All four mockups below are extensions of one of those.

Mock data notice. These are illustrative mockups, not screenshots. Layout, spacing, type scale, and color tokens are lifted from the real components. Vendor names and dollar amounts are real mined values where they fit naturally (MTech $264.42 / unit 422, American Garage Door $1,038.61, TK Elevator $1,378.80, Anchor $333.00, Royal Textile $1,370.39 / unit 117, Home Depot $949.36); dates, work-order IDs, and progress states are illustrative. No tenant names anywhere.
Option A — WO progress rail gains Scheduled and Invoiced EFFORT S /maintenance/[displayId] → WorkOrderProgressCard
Progress
Reported
Jun 18
Dispatched
American Garage Door
Scheduled
Jun 22 · AM
In progress
 
Work complete
Jun 22
Invoiced
$1,038.61
·
Rating
 
·
Closed
 
Two new steps highlighted. The dollar amount rides the existing sublabel slot on StepperStep.
Invoice
Invoice #
95245
Received
Jun 23, 2026
Amount
$1,038.61
Vendor
American Garage Door
Access & Details
Estimated Cost
Attribution
no PO on document
Option B pairs with A: an additive fact block cloned from the existing "Access & Details" pattern, plus one "Invoice received — $1,038.61" milestone in the story derivation that the sidebar checkpoint feed picks up with zero component change.

Why it's S: two entries in WoProgressStepKey, two in LABELS, two in doneFlags, and three new fields on the derivation input. UI diff: zerotoStepperSteps maps state 1:1 already. The one thing to eyeball is eight horizontal steps on a phone; the rail already wraps in overflow-x-auto and supports vertical orientation.

Option D1 — a fourth stat card on Vendor Costs EFFORT S /maintenance/costs → CostsClient CardExpandGroup
Total Vendor Spend
$39,584
Jan 1 – Jul 29, 2026
Active Vendors
9
with a paid invoice
Invoices Captured
44
3 duplicate re-sends excluded
Attributed to Work
$264
1 of 44 invoices · 0.7%
New card ringed. gridClassName goes sm:grid-cols-3sm:grid-cols-2 lg:grid-cols-4; identity color is the maintenance-family cyan so it reads as maintenance rather than generic vendor spend. Zero new components.

Why it's S: one ClickableStatCard inserted into an existing group plus a grid-class change — roughly ten lines. The card doubles as the honest scoreboard for the attribution gap: today it reads 0.7%, and it is the number PO sync is supposed to move.

Option E — invoiced totals on the turnover make-ready tracker EFFORT S /maintenance/turnovers/[id] → MakeReadyWorkTracker
Make-ready work
67% complete (2/3)
Invoiced $264.42 of 3 jobs
Unit 422 — kitchen drain backup Complete MTech Mechanical
$264.42
Unit 422 — blind replacement Complete Royal Textile Products
awaiting invoice
Unit 422 — paint & patch In progress in-house
The tracker carries no money at all today. This is the cheapest place in the app for turnover-level actual spend, and the $264.42 is the one 2026 invoice that genuinely attributes to a turnover.

Why it's S: a header total beside the existing percent counter and a per-row amount opposite the existing "View work order →" link. No new component. Em-dash for unknowns — never fabricate a number.

Option C — money columns on the two list surfaces EFFORT M /maintenance/list → WorkOrderTable · /maintenance/turnovers → TurnoversClient
Work orders
TitleUnitVendorStatus InvoicedCreated
Garage door opener faultAmerican Garage Door Complete $1,038.61Jun 18
Elevator monthly maintenanceTK Elevator Complete $1,378.80May 19
Kitchen drain backup422MTech Mechanical Complete $264.42Jun 14
Blind replacement117Royal Textile Products Complete $1,370.39May 12
Monthly pest serviceAnchor Pest Control Complete $333.00Jul 1
Common-area glass panelAlpine Glass Scheduled Jul 24
New column tinted. Right-aligned tabular-nums, faint em-dash when absent — the exact shape of the turnover charges cell.
Turnovers
UnitStageWork orders Charges (est.)Invoiced (actual)Days
422Make-ready2/3 $1,850$264.4221
117Ready to lease3/3 $1,400$1,370.3934
207Make-ready1/2 $6009
Estimated and actual side by side — the comparison the board cannot make today, because Turnover.totalActualCost is null on all 26 Camellia turnovers.

Why it's M, not S: not design — the work-order list fetches ?view=meta (scalars only), so the new field has to be added to the meta projection, and the mobile card renderer must move in lockstep with the column array. Estimated figures in the turnover mock are illustrative; only the invoiced column carries mined values.

Recommendation. A + B on the work-order detail page is the minimum-delta core — one pure-derivation change plus one additive markup block, no new components. D1 gives the spend figure a home in about ten lines on a page already titled "Vendor Costs". E is the cheapest turnover attribution. C is the right follow-on once the invoice field exists on the work-order row. Skip conversations entirely — a work-order door-card on the conversation page was deliberately deleted 2026-07-17 ("one affordance, not two").

5. Effort ladder and decisions needed

SliceWhat it deliversWhy it's the prerequisite it is EffortBlocked on
0 Email → vendor reverse index + PM-confirmed domain backfill Vendor recognition is phone-only and VendorCompany stores no email, so an email sender cannot resolve to a vendor at all. Nothing downstream can attribute without this. S PM confirmation of preferred-vendor domains
1 Detection + surface — classify the schedule/job-done/invoice signal and show it (options A, B, D1) Vendor email is understood today and then discarded on the skip arm. Slice 1 stops throwing the signal away and gives it a place to appear. S Slice 0
2 Schedule-write parity — email-sourced schedule confirmations write through the same channel-agnostic outcome machinery voice and SMS already use 101 schedule confirmations arrived by email in 2026 and none of them moved a work order. The machinery is already channel-agnostic by mandateVendorCallSessionChannel includes 'email' — so this is mostly reuse, not new code. Email is simply not wired into it. M Slice 1
3 Attachment fetch + invoice extract + AP handoff The Graph path persists no attachments, so the bytes have to be fetched before they can be parsed. The parser itself is already in the Lambda. M Trusted-sender gate policy; metered spend approval for lane (b)
4 Invoice → purchase-order matching Converts the $6,688.09 of exact_po spend into hard job attribution using PO numbers already printed on the documents. M Hard-blockedPurchaseOrder has 0 rows portfolio-wide

Decisions needed from Fede

  1. Trusted-sender gate policy for vendor attachments. The inbound path currently hard-rejects an untrusted sender carrying a data attachment. Random vendors will not be on the trusted-data-sender list, so as written, slice 3 rejects almost every real invoice at the gate. Decide whether vendor invoices get their own sender-trust rule (e.g. trust follows the PM-confirmed vendor domain from slice 0) or whether the gate stays absolute. This is the likeliest blocker to a working end-to-end demo.
  2. Enable purchase-order sync for Camellia. Unblocks slice 4 and converts $6,688.09 of already-collected 2026 spend — plus every future MTech, Home Depot, and Royal Textile invoice — into hard attribution. Nothing else in the ladder moves the 0.7% attribution number nearly as far.
  3. Preferred-vendor domain confirmations. There is no preferred-vendor concept: org_jpco's membership list is all 816 AppFolio vendors with no flag, and the nine-vendor list in this brief was derived empirically from mail traffic by fuzzy name/domain match. Slice 0 needs a PM confirm screen — "is brenda@americangaragedoor.net American Garage Door?" — to turn that fuzzy match into a fact. Decide whether that screen is a one-time backfill sweep or an ongoing inbox affordance.
  4. Metered-spend approval for the Lambda lane. Until that approval lands, extraction runs subscription-only (lane a) and the inbound-email hook ships dormant behind an off-by-default flag. The ask is small — $0.003–$0.009 per invoice, well under $1/month at Camellia volume, with a one-time backfill of $1–$3 — but it is a real gate: AWS_LAMBDA_FUNCTION_NAME forces the metered credential and there is deliberately no force-bearer override, so "Lambda on the subscription" is not an option.
PropFlow Docs