# Zoho Receipts Pipeline — Recommendations

**Status:** Active
**Last updated:** 2026-06-13
**Owns:** Advisory notes, upgrade paths, open decisions, watchlist items.
**Does NOT own:** As-built state (Architecture Reference), procedures (Operating Runbook), recovery (Rebuild Guide), costs (Cost-Impact Analysis), integration patterns (Integration Guide).

This document captures decisions deferred, upgrade gates to monitor, and improvements worth pursuing when the conditions are right. Each entry has a **trigger** condition — when it fires, that's when to act.

---

## R-1: Upgrade to Zoho Books Premium for native Reporting Tags

**Status:** Deferred
**Cost delta:** +$50/mo ($20 → $70) — not currently justified
**Trigger to revisit:** *Any one* of:
- Monthly time spent on the "two filtered P&L reports + spreadsheet combine" workflow exceeds 30 minutes
- A third product line is added (CBB Sign + ScopeDOPE + new = three lines pushes manual COA-list maintenance into annoying territory)
- Real-time combined dashboards become a hard requirement (not just monthly close)
- Active acquisition diligence begins — buyers want native columnar product P&L; manual combine works for first-pass but is friction at second-pass

**Why deferred:** Standard plan + doubled-COA convention (`<Account> - CBB` / `<Account> - ScopeDOPE`) produces clean per-line filterable P&L reports. The data is correct; the workflow is the bottleneck, not the data model. Spending $50/mo to remove a once-a-month spreadsheet step is not yet justified.

**Migration plan when triggered:**
1. Provision a single reporting tag dimension `Product Line` with values `CBB` and `ScopeDOPE` (and any new lines).
2. Bulk-update existing expenses to attach the tag, retaining the `-CBB`/`-ScopeDOPE` suffix in account names initially.
3. Re-create P&L custom reports using **Compare by Reporting Tag** instead of **Filter Accounts**.
4. Phase out the doubled-COA suffix over the next fiscal year by merging paired accounts (`Computer Hardware - CBB` + `Computer Hardware - ScopeDOPE` → `Computer Hardware`, tagged by product line).
5. The pipeline's category-mapping logic in `cbb-utils/zoho/extractors/` switches from "compute suffix" to "compute account + tag" — one PR, no schema migration needed.

**Acquisition lens:** A buyer's CFO will prefer native tag-based P&L. Premium for the diligence period (could be as short as 1 quarter) is a smart pre-diligence investment — call it $150–$200 to materially improve due-diligence readability.

---

## R-2: Automated combined P&L export

**Status:** Worth doing in pipeline v1.1
**Cost delta:** $0 (within free grant)
**Trigger:** Immediately after first successful monthly close runs of the standalone P&L reports

**What:** A new Container Apps Job scheduled monthly that:
1. Pulls both `Monthly P&L - CBB` and `Monthly P&L - ScopeDOPE` via Zoho API
2. Merges them into a single PDF + CSV showing CBB and ScopeDOPE side-by-side with a totals column
3. Saves to `OneDrive/CBB_Corporate/03_-_Confidential/Financial_Records/Monthly_PandL/<YYYY-MM>/Combined.pdf` and `.csv`
4. DMs the user with a link

**Why now:** Removes the only manual step in the monthly close. Acquisition-lens win — the combined PDF is what a buyer wants to see.

**Why not in pipeline v1.0:** Get the ingestion pipeline running and proven for two full monthly cycles before adding the export job. Premature optimization otherwise.

---

## R-3: Switch Log Analytics to Basic Logs tier

**Status:** Optimization candidate — defer until v1.0 baseline observed
**Cost delta:** ~$0.20/mo savings (from ~$0.30/mo to ~$0.10/mo)
**Trigger:** After 3 months of v1.0 operation, if log volume confirms the projected sub-150-MB/mo profile

**What:** Container Apps Jobs write logs to Log Analytics. The default `Analytics Logs` tier costs ~$2.30/GB; the **Basic Logs** tier costs ~$0.50/GB for log types that don't require full Kusto query capability. Pipeline logs are append-only and rarely queried — Basic tier suffices.

**Why not now:** Basic Logs has reduced query capability and retention defaults that may matter for incident investigations. Verify the workload pattern over 3 months first.

---

## R-4: Move to Azure Container Registry (ACR) Basic

**Status:** Deferred
**Cost delta:** +$5/mo (Basic SKU)
**Trigger:** First time the pipeline needs a private container image (proprietary extractor logic, embedded confidential templates, etc.)

**What:** Today, the pipeline runs from a public Microsoft Container Registry or Docker Hub base image. If a private image becomes necessary, provision ACR Basic and update Container Apps Jobs to pull from it via managed-identity ACR Pull RBAC.

**Why not now:** No private-image requirement exists at v1.0. The receipts pipeline logic is in `cbb-utils/zoho/` — that repo is private, but the runtime image can be public Python + a runtime mount of the script (via Container Apps' env-injected secret + git clone in entrypoint, or via Azure Files mount).

---

## R-5: Per-vendor cost-drift alerts

**Status:** Worth doing in v1.1 or v1.2
**Cost delta:** $0 (extends consistency-check job)
**Trigger:** After 12 months of receipt history exists in Zoho (need a baseline to detect drift against)

**What:** Add a check to the weekly consistency job that flags vendors whose monthly spend has changed by >20% vs. the trailing 3-month average. Catches pricing surprises (Anthropic plan upgrade, Namecheap renewal hike, etc.) and dormant subscriptions slowly growing.

**Why useful:** A real ops signal at zero cost. Acquisition-lens win — demonstrates cost discipline.

---

## R-6: Asset event-emission thresholds

**Status:** Done (2026-06-13)
**Cost delta:** $0
**Trigger:** Before pipeline goes live — resolved

**What:** The receipts pipeline emits `asset_candidate` events for line items matching any of these rules:
- **Hardware purchases ≥ $50** (decision: 2026-06-13)
- All software subscriptions regardless of amount
- All domain registrations
- All certificates / legal-IP filings

**Why this exists:** The asset-register service writes every event it sees (after dedup). To avoid the SharePoint list filling with USB cables, the receipts pipeline filters at emission time.

**Implementation:** The thresholds live in `cbb-utils/zoho/asset_filter.py` (or equivalent) and are referenced by the receipts ingest job. Future threshold changes go through a PR to `cbb-utils`, not through Zoho or SharePoint config.

**Note:** The original framing of R-6 was "notification thresholds" when the design was notify-only. The design has changed — the asset-register service auto-populates the SharePoint list, so these thresholds now control event emission (and therefore SharePoint row creation), not notification.

---

## R-7: Backup format consolidation review at year 2

**Status:** Watchlist item
**Cost delta:** Potentially negative (savings)
**Trigger:** 2 years post-launch (~2028-06)

**What:** Today the pipeline runs **both** nightly JSON-per-module exports and weekly full org exports. Storage cost is trivial in year 1, but at year 7 the redundant overlap may be optimizable — e.g., drop nightly JSON for modules that change rarely (Chart of Accounts), keep nightly only for high-churn modules (Expenses, Customer Payments).

**Why not now:** Premature optimization. At year-2 the storage cost is still under $0.50/mo. Revisit when total cost crosses $5/mo.

---

## R-8: Annual review of the doubled-COA limitations

**Status:** Calendar item, not a one-shot
**Cost delta:** $0 to review; could trigger R-1
**Trigger:** Every January, immediately after annual close

**What:** Review whether the doubled-COA convention is still serving the business. Specifically:
- Is the manual combined-P&L workflow still <30 min/mo? (Triggers R-1 if exceeded)
- Have any new product lines been added without doubled accounts being created?
- Have any account-list updates in Zoho silently broken the saved custom reports?

**Why annually:** Aligned with fiscal cycle; aligned with R-1's trigger conditions.

---

## R-9: Pipeline error budget and SLO

**Status:** Not yet defined
**Cost delta:** $0
**Trigger:** After 3 months of v1.0 baseline data

**What:** Define an error budget — e.g., "no more than 2% of receipts may land in `_needs_review/` in any rolling 30-day window." Below the budget, treat extractor failures as a normal background rate. Above the budget, treat it as a defect and prioritize extractor fixes.

**Why useful:** Gives Operating Runbook §6 a quantitative trigger for escalation rather than a vibes-based one. Also a real acquisition-lens artifact: "the pipeline has an SLO with a 6-quarter clean record" is exactly what a buyer wants in a TDD packet.

---

## R-10: Decouple notification routing from pipeline code

**Status:** v1.2+ refactor candidate
**Cost delta:** $0
**Trigger:** When a second notification channel is needed (e.g., posting to a `#finance-ops` channel in addition to DMs)

**What:** Today, the pipeline calls Slack and SendGrid APIs directly. A small refactor to push events to an Azure Event Grid topic and subscribe channel-specific consumers separately would isolate routing changes from pipeline-logic changes. Useful when notification policy gets more complex.

**Why not now:** YAGNI at v1.0. The current direct-call design is fine for the current channel mix.

---

## R-11: Anthropic-style "account-holder email" vendor audit

**Status:** Done (2026-06-13)
**Cost delta:** $0
**Trigger:** Before pipeline goes live — resolved

**What:** Some vendors (Anthropic confirmed; likely others) send receipts to "the account-holder email" regardless of any explicit billing-email setting. For those vendors, the account-holder email itself needs to be `finance@coldboreballisticsllc.com`, not `jschwefel@coldboreballisticsllc.com`.

**Resolution:** User completed the vendor audit and migrated account-holder emails to `finance@coldboreballisticsllc.com` for the affected vendors.

**Forward-looking checklist:** When onboarding any new vendor, default the account-holder email to `finance@coldboreballisticsllc.com` from the start — don't rely on a billing-email override that the vendor may ignore.

---

## R-12: GitHub receipt — personal Gmail collision

**Status:** In Progress (resolution chosen 2026-06-13; awaiting next GitHub billing cycle to confirm)
**Cost delta:** $0
**Trigger:** Before pipeline goes live — resolution chosen

**What:** `GITHUB-61G78710HR001791H-20260603.md` in the existing receipts corpus records `jason.schwefel78@gmail.com` as the PayPal funding source. CLAUDE.md MANDATORY rule forbids this email in any CBB documentation.

**Resolution chosen (option 3):** The GitHub funding source is being migrated to a CBB-owned PayPal account on `finance@coldboreballisticsllc.com`. Future GitHub receipts will not include the personal Gmail. The historical receipt remains in place as a documented one-time legacy fact — no sanitization, no rewriting history.

**Verification step:** After the next GitHub billing cycle, confirm the new receipt arrives at `finance@` and contains no personal Gmail reference. At that point, flip this status to `Done`.

**Forward-looking rule:** The personal-Gmail-as-funding-source pattern is treated as historical only. Any new vendor connection must use a CBB-owned funding instrument from day one.

---

## R-13: Asset service — update/correction support

**Status:** Deferred (v1.2+)
**Cost delta:** $0
**Trigger:** First time an asset row in SharePoint needs an automated update (e.g., subscription tier change, vendor name change)

**What:** Today the asset service is append-only — it inserts on first sight, no-ops on dedup match, and never updates existing rows. Adding update support requires:
- New event types (`asset_update`, `asset_decommission`)
- Conflict resolution rules (event wins vs. human edit wins)
- Audit log of changes

**Why deferred:** v1.0 ships with manual correction via the SharePoint UI. Append-only is simpler and safer for the first 6–12 months; the operator can build mental model of when updates are needed before automating.

---

## R-14: Asset service — Event Grid refactor

**Status:** Watchlist
**Cost delta:** ~$0 at current volume
**Trigger:** When a third event consumer is needed beyond the asset register (e.g., a future analytics dashboard, a future expense-pattern alerter)

**What:** Migrate `asset-candidate-events` from a Storage Queue to an Event Grid topic. Multiple subscribers can then independently consume the same events. The asset-register service becomes one subscriber among several.

**Why deferred:** R-10 already noted the Event Grid refactor at the notification layer; this would extend the same pattern to the event layer. Premature with one consumer.

---

## R-15: Free-trial tracking mechanism

**Status:** Open — TODO
**Cost delta:** $0 (or $0 if the decision is "calendar reminder", up to $0 for SharePoint list, up to <$1/mo for a small Zoho Books recurring-bill use)
**Trigger:** Before the first zero-dollar receipt arrives at `finance@` (which will be soon — Anthropic and several other vendors send $0 statement-only receipts during free trial periods)

**What:** Decide where and how CBB tracks free trials so they don't silently convert to paid subscriptions without anyone noticing. The receipts pipeline filters zero-dollar receipts **out of Zoho entirely** — they don't become Zoho expenses, no synthetic posting, no Free-Trial-bucket pattern (decision: 2026-06-13). So the tracking has to live somewhere else.

**Decision deferred — needs design conversation.** Candidate options:
- **Calendar reminders** — pipeline creates an Outlook calendar event on the convert-date with the trial details in the body. Reuses the existing `m365cal` CLI pattern. $0 cost. No new SOT.
- **Dedicated SharePoint list** — "CBB Free Trials" list with columns for vendor, trial-start, convert-date, action-due-date, status. SOT pattern matches the RR and Asset Register. Pipeline writes via a similar minimum-scope service. $0 cost.
- **Zoho Books Recurring Bills** (`/recurring-bills`) — schedule a bill at the convert-date with a "review before this fires" reminder. Native Zoho mechanism; requires `bills.ALL` scope (currently the Self Client has `bills.READ` only). Couples free-trial tracking to the accounting system.
- **Append to existing notification flow** — Slack DM + email "Free trial detected: vendor X, ends YYYY-MM-DD; decide before then." Lowest-tech. No persistent record beyond email history.

**Why open:** none of the options is clearly correct without more context on how trials actually flow at CBB. Free trials are rare today (the corpus has none); design effort should be deferred until volume justifies it.

**Stopgap until decided:** the receipts pipeline filters zero-dollar receipts and emits a `zero_dollar_receipt` notification (Slack DM + email) with the vendor, date, and a "review and decide" prompt. The human takes manual action. This is a working v1.0 behavior even with R-15 unresolved.

---

## R-16: Receipt-to-PDF rendering as a mandatory pipeline stage

**Status:** Confirmed design constraint (2026-06-13 integration test)
**Cost delta:** $0 within free grant (rendering is ~100ms per receipt in the container)
**Trigger:** Implementation of the receipts ingest job

**What:** The Zoho Books `POST /expenses/{id}/receipt` endpoint **only accepts image / PDF formats** — Markdown (`.md`) and plain text (`.txt`) are explicitly rejected with `code 40 "File types with extension X are not supported."` (verified empirically 2026-06-13). The Markdown corpus is therefore **not** the receipt attached to Zoho; it is the human-readable index that lives in OneDrive.

**Canonical receipt = whatever the vendor sent, converted to PDF.** Pipeline stage flow:

1. **PDF attachment present** in the source email (Anthropic, GitHub, Best Buy often send these) → use the original PDF as-is. Save to `Receipts/_raw/<message-id>/original.pdf`.
2. **Inline HTML body only** (most Amazon, Namecheap receipts) → render the HTML body to PDF via a headless renderer (recommended: `weasyprint` for HTML+CSS fidelity, or `pandoc` for simple cases). Save the rendered PDF to `Receipts/_raw/<message-id>/rendered.pdf`.
3. **Image attachment** (rare, forwarded vendor receipts) → embed in a single-page PDF wrapper.
4. **The PDF (original or rendered)** is what `POST /expenses/{id}/receipt` uploads to Zoho.
5. **The Markdown summary** is filed under `Receipts/<line>/<YYYY>/<MM>/` as the human-readable canonical record, but is **not attached to Zoho**.

**Implementation:** the `cbb-utils/zoho/render_to_pdf.py` module handles all three cases. Dependencies: `weasyprint` (Python package, MIT-licensed, ~30 MB Docker image size addition).

**Why now:** the integration test (2026-06-13) proved Zoho rejects `.md` and `.txt` and accepts `.pdf`. Without this stage, no receipt gets attached, and the audit trail breaks. Non-negotiable for v1.0.

---

## R-17: Backfill historical receipt PDFs from `finance@`

**Status:** Deferred — to be done as part of backlog import, not before
**Cost delta:** $0 (one-shot use of the receipts ingest MI's `Mail.ReadWrite` permission)
**Trigger:** When the backlog-import script runs (after pipeline provisioning is complete)

**What:** The 27 existing normalized Markdown files in `Receipts/CBB/2026/` and `Receipts/ScopeDOPE/2026/` have **no source receipt PDFs alongside them**. The originals only exist in the `finance@` shared mailbox. If a mailbox message is deleted, the source receipt for that expense is gone forever — the Markdown summary is not an audit-grade substitute.

**Backfill plan (executed as part of backlog import, not as standalone task):**

1. Backlog-import script (`cbb-utils/zoho/import_backlog_receipts.py`) uses the receipts ingest managed identity's Graph permission to search `finance@` for each historical receipt by reference number, vendor + date, or message-ID if known.
2. For each match: pull the source (PDF attachment or HTML body), render to PDF if needed, save to `Receipts/_raw_backfill/<receipt-ref>/original.pdf`.
3. POST the historical Markdown's contents as the Zoho expense, attach the backfilled PDF to the expense.
4. For receipts where no `finance@` match is found (e.g., older receipts that pre-date the `finance@` migration), the Markdown imports without a PDF, and the operator gets a `needs_review` notification listing the gaps.

**Why deferred to backlog-import time:** doing this as a standalone task duplicates infrastructure-provisioning work. Pairing the backfill with the backlog import means we provision the Graph permission once, use it for both, and have a single audit trail of "this batch of receipts was reconciled on date X."

**Acquisition note:** the historical receipt PDF gap is a documented limitation. A buyer-side TDD reviewing the financial records would flag it. R-17 closes the gap as part of backlog import. Until R-17 runs, the 27 historical Markdown-only receipts are listed as a known limitation in the Acquisition Brief §6.

---

## Format for adding new recommendations

When you add a new recommendation, follow this structure:

```
## R-<n>: <one-line title>

**Status:** <Deferred | Watchlist | Open | In Progress | Done>
**Cost delta:** <$X/mo or zero>
**Trigger:** <specific condition under which to act>

**What:** <one paragraph>

**Why <not> now:** <one paragraph>

**Migration plan / steps when triggered:** <bullet list>

**Acquisition lens:** <optional — what a buyer sees>
```

The numbered IDs (R-1, R-2, …) never get reused — when a recommendation is implemented or rejected, mark its status and leave the slot in place. Future-you needs to be able to follow links back to the rationale.

---

## Reference cross-links

- `Zoho_Architecture_Reference.md` — as-built state
- `Zoho_Operating_Runbook.md` — operations
- `Zoho_Rebuild_Guide.md` — rebuild
- `Zoho_Cost_Impact_Analysis.md` — costs
- `Zoho_Integration_Guide.md` — integration patterns
- `Zoho_Acquisition_Brief.md` — buyer summary
- `infra/cbb-utils/zoho/README.md` — receipts pipeline scripts
- `infra/cbb-utils/asset-register/README.md` — asset service scripts
- `reference_zoho_books_api.md` (memory) — API reference
- `project_normalized_receipts.md` (memory) — corpus schema
