# Zoho Receipts Pipeline — Rebuild Guide

**Status:** Active
**Last updated:** 2026-06-13
**Owns:** Full from-scratch rebuild guide. Assumes everything is gone.
**Does NOT own:** Resource IDs (Architecture Reference), day-to-day procedures (Operating Runbook), cost (Cost-Impact Analysis), script flag detail (`cbb-utils/zoho/README.md` and `cbb-utils/asset-register/README.md`).

This guide rebuilds the entire Zoho receipts pipeline + Asset Register service from scratch. Written assuming zero prior platform experience per CLAUDE.md "Instructions must assume zero prior platform experience — MANDATORY".

---

## 1. When to use this guide

Use the full guide if **any** of the following are true:

- The Azure subscription containing `rg-cbb-receipts-prod` is destroyed or unrecoverable.
- The Zoho Books org is migrated to a new org (e.g., new entity post-acquisition).
- The CBB M365 tenant is replaced.
- A buyer's engineering team is standing up a parallel environment.

Use individual sections if a single component needs re-provisioning:

- §4: Azure resources only
- §5: Zoho Self Client only
- §6: Populate Key Vault with Zoho credentials
- §7: ApplicationAccessPolicy only (receipts mailbox grant)
- §7b: Asset Register Graph Sites.Selected + per-site grant
- §8b: SharePoint Asset Register list schema check
- §10: COA hydration (doubled-COA accounts)
- §11: Smoke test

## 2. Pre-requisites

You must have:

| Item | How to verify |
|---|---|
| An Azure subscription with Owner role | `az account show` returns a subscription with role `Owner` |
| An M365 tenant where `finance@coldboreballisticsllc.com` is a shared mailbox | M365 admin center → Recipients → Shared → `finance@` is listed |
| The **CBB Asset Register** SharePoint list exists | Navigate to the list URL in §9b.1 of Architecture Reference; the list page loads |
| A Zoho Books org (Standard or higher) | Sign in to `https://books.zoho.com`; the org appears in the dropdown |
| Admin access to all four above | Self-explanatory |
| The `cbb-infra` repo cloned locally | `ls cbb-infra/` shows Terraform files |
| The `cbb-utils` repo cloned locally | `ls cbb-utils/zoho/` and `ls cbb-utils/asset-register/` show scripts |
| Terraform installed | `terraform version` returns 1.5 or newer |
| Azure CLI installed | `az version` returns 2.50 or newer |
| Exchange Online PowerShell module installed | `Get-Module -ListAvailable -Name ExchangeOnlineManagement` returns a row |
| Python 3.11+ installed | `python3 --version` returns 3.11 or newer |

If any of these are missing, install them per the install instructions in the `cbb-utils/` top-level README (which follows the multi-platform install convention from CLAUDE.md).

## 3. Order of operations

The rebuild must run in this order due to dependencies:

1. Azure resources (RG, both Key Vaults, Storage Account with queue, Container Apps Environment)
2. Zoho Self Client + initial refresh token
3. Populate `kv-cbb-receipts-prod` with Zoho credentials
4. Container Apps Jobs — receipts (4) + asset register (1) — depend on Key Vaults and MIs
5. Graph permission grant + ApplicationAccessPolicy for receipts ingest MI
6. **Graph Sites.Selected grant + per-site write permission for asset-register MI; populate `kv-cbb-asset-register-prod`** (see §7b)
7. CBB Asset Register SharePoint list schema check — confirm required columns exist (see §8b)
8. COA hydration — create the doubled-COA accounts in Zoho Books (see §10)
9. Manual smoke test of both pipelines
10. Schedule + queue-trigger activation

## 4. Azure resources

### 4.1 Sign in and select subscription

```bash
az login
az account set --subscription "<subscription-name-or-id>"
az account show
```

Confirm the subscription is the intended target before continuing.

### 4.2 Apply Terraform

```bash
cd infra/cbb-infra
terraform init
terraform plan -target=module.zoho_receipts
terraform apply -target=module.zoho_receipts
```

The module provisions every resource listed in Architecture Reference §3. Confirm the plan shows exactly those resources before approving with `yes`.

**Region:** every resource will be created in `northcentralus` per the CLAUDE.md MANDATORY directive. The module hard-codes this. If a different region is required, override on the command line: `terraform apply -var="location=<region>"` — and document the deviation in the as-built Architecture Reference.

### 4.3 Record output GUIDs

After `terraform apply` completes:

```bash
terraform output -json zoho_receipts > zoho-receipts-as-built.json
```

This file contains the managed identity Object IDs, Key Vault URI, Storage Account name, Container Apps Job names, and queue URLs. **Open the Architecture Reference and update §3.6 with the actual managed identity Object IDs.** Then delete `zoho-receipts-as-built.json` from local disk — it's an intermediate artifact.

### 4.4 Verify resource group contents

```bash
az resource list --resource-group rg-cbb-receipts-prod --output table
```

Confirm the list matches Architecture Reference §3 (RG, CAE, five Container Apps Jobs, Storage Account with `asset-candidate-events` queue, two Key Vaults, Log Analytics Workspace).

## 5. Zoho Self Client provisioning

### 5.1 Create the Self Client

1. Open `https://api-console.zoho.com/` in a browser.
2. Sign in with the Zoho Books admin account *(TBD — see open question in change log)*.
3. The console may show a "Choose a Client Type" screen directly (with or without an intermediate Get Started step depending on prior usage).
4. Select **Self Client** as the client type and click **CREATE NOW**.
5. Confirm the prompt.

The console now shows your Self Client with two tabs: **Client Secret** and **Generate Code**.

### 5.2 Capture Client ID and Client Secret

On the **Client Secret** tab, you'll see the **Client ID** and **Client Secret** displayed.

**Do not store either of these in Key Vault yet** — Key Vault gets populated in §6 (one batch of commands at the end of §5, after you've also captured the refresh token and organization ID). For now, just capture these two values into a temporary secure scratch buffer.

1. Copy the **Client ID** to a secure scratch buffer (e.g., a password manager note). This value will be stored in Key Vault as the secret named `zoho-books-client-id`.
2. Copy the **Client Secret** to the same scratch buffer. This value will be stored as `zoho-books-client-secret`.

**Sensitivity:** the Client Secret is highly sensitive. Treat it like a password. Do not paste it into chat, email, screenshots, Slack, or anywhere else it might be logged or retained. Do not commit it to any repo.

You will return to these values in §6.

### 5.3 Set the Client Name and identify it

The Zoho Self Client UI may auto-name the client "Self Client" with the creation date and may not expose a rename option. If a Settings or gear icon is available on the client row, set the name to `cbb-receipts-ingestion-prod` for human-readable identification. If not available, skip this step — the Client ID is the functional identifier; the name is cosmetic.

### 5.4 Generate the initial authorization code

1. Click the **Generate Code** tab.
2. In the **Scope** field, enter (comma-separated, no spaces):
   ```
   ZohoBooks.contacts.ALL,ZohoBooks.expenses.ALL,ZohoBooks.bills.READ,ZohoBooks.settings.READ,ZohoBooks.accountants.READ
   ```
3. Set **Code expiry duration** to the longest available value (typically 10 minutes; some Zoho UIs may only offer 3 minutes — that works but is tight).
4. **Scope Description**: `CBB receipts pipeline initial bootstrap`.
5. Click **Create**.
6. Copy or DOWNLOAD the displayed authorization code immediately — the displayed text in the dialog is truncated. Use COPY or DOWNLOAD to capture the full string.

### 5.5 Exchange the authorization code for a refresh token

```bash
cd infra/cbb-utils/zoho
python3 bootstrap_refresh_token.py \
  --client-id <client-id> \
  --client-secret <client-secret> \
  --grant <authorization-code>
```

The script prints the refresh token to stdout. **Copy it to your scratch buffer** (the same one from §5.2). This value will be stored in Key Vault as `zoho-books-refresh-token`. Treat it like the Client Secret — highly sensitive, never logged or pasted into chat.

### 5.6 Discover the organization ID

```bash
python3 list_orgs.py --client-id <id> --client-secret <secret> --refresh-token <token>
```

The script prints all orgs accessible to the credentials. Identify "Cold Bore Ballistics LLC" and **copy its organization ID to your scratch buffer**. This value will be stored in Key Vault as `zoho-books-organization-id`.

For reference, the current CBB org ID is `927659458` (validated 2026-06-13).

You now have all four values needed for §6:

| Value | Stored in Key Vault as |
|---|---|
| Client ID (from §5.2) | `zoho-books-client-id` |
| Client Secret (from §5.2) | `zoho-books-client-secret` |
| Refresh token (from §5.5) | `zoho-books-refresh-token` |
| Organization ID (from §5.6) | `zoho-books-organization-id` |

Proceed to §6.

## 6. Populate Key Vault — Zoho credentials

You now have four pieces of information from §5:

- Client ID (from §5.2)
- Client Secret (from §5.2)
- Refresh token (from §5.5)
- Organization ID (from §5.6)

These go into **Azure Key Vault** — Azure's managed secrets store. The pipeline reads them at runtime via a managed identity (no secrets in code, no secrets in config files). The vault for this pipeline is named `kv-cbb-receipts-prod`.

### 6.0 Prerequisites

You need:

- **Azure CLI installed.** Install instructions per CLAUDE.md "Multi-platform install instructions — MANDATORY":

| Platform | Command |
|---|---|
| Debian | `curl -fsSL https://aka.ms/InstallAzureCLIDeb \| sudo bash` *(do NOT use the apt `azure-cli` package)* |
| Ubuntu | Same as Debian |
| RHEL/Fedora | `sudo dnf install azure-cli` |
| Windows | `winget install -e --id Microsoft.AzureCLI` |
| macOS | `brew install azure-cli` |

- **Signed into Azure CLI as a user with `Key Vault Secrets Officer` role on `kv-cbb-receipts-prod`** (the role that allows writing secrets). The CBB owner has this role by default per Architecture Reference §3.5.

Sign in (only needed once per machine, or after token expiry):

```bash
az login
```

This opens a browser to the Microsoft login page. Sign in with your CBB user. A successful login prints a table of subscriptions.

Confirm the right subscription is active:

```bash
az account show --query name -o tsv
```

If this prints the wrong subscription, switch with `az account set --subscription "<subscription-name>"`.

### 6.1 Confirm the Key Vault exists

```bash
az keyvault show --name kv-cbb-receipts-prod --query name -o tsv
```

- If this prints `kv-cbb-receipts-prod`, the vault exists. Continue to §6.2.
- If you get `(ResourceNotFound)` or `(VaultNotFound)`, the vault has not been provisioned yet. Stop here and complete §4 (Azure resources via Terraform) first. The vault is created by Terraform — do not create it by hand.

### 6.2 Set each secret

Replace each `<placeholder>` with the actual value you captured from §5. Run the commands one at a time; each prints a JSON object on success.

**Sensitivity note:** the Client Secret and Refresh Token are highly sensitive. Anyone with them can read and write your CBB Zoho Books data. Do not paste them into chat, screenshots, or any system that logs commands. If your shell history records commands (most do by default), clear it after this step with `history -c` (bash/zsh).

```bash
az keyvault secret set \
  --vault-name kv-cbb-receipts-prod \
  --name zoho-books-client-id \
  --value "<client-id>"

az keyvault secret set \
  --vault-name kv-cbb-receipts-prod \
  --name zoho-books-client-secret \
  --value "<client-secret>"

az keyvault secret set \
  --vault-name kv-cbb-receipts-prod \
  --name zoho-books-refresh-token \
  --value "<refresh-token>"

az keyvault secret set \
  --vault-name kv-cbb-receipts-prod \
  --name zoho-books-organization-id \
  --value "<org-id>"
```

**Alternative for highly sensitive values** — read from stdin instead of passing on the command line (keeps the secret out of your shell history):

```bash
az keyvault secret set \
  --vault-name kv-cbb-receipts-prod \
  --name zoho-books-refresh-token \
  --file /dev/stdin
# Paste the refresh token, press Enter, then Ctrl-D to send EOF
```

### 6.3 Verify all four secrets are present

```bash
az keyvault secret list \
  --vault-name kv-cbb-receipts-prod \
  --query "[].name" \
  -o table
```

Expected output — all four names, in any order:

```
Result
------------------------------
zoho-books-client-id
zoho-books-client-secret
zoho-books-organization-id
zoho-books-refresh-token
```

If any are missing, re-run the corresponding `az keyvault secret set` command from §6.2.

### 6.4 Verify a secret is retrievable (optional, sanity check)

```bash
az keyvault secret show \
  --vault-name kv-cbb-receipts-prod \
  --name zoho-books-organization-id \
  --query value -o tsv
```

Should print the org ID you captured in §5.6. (Use this on the Organization ID only — it's the least sensitive of the four. Don't echo the client secret or refresh token to your terminal.)

### 6.5 Clear shell history (recommended)

```bash
# bash / zsh
history -c
```

If you used PowerShell:

```powershell
Clear-History
Remove-Item (Get-PSReadlineOption).HistorySavePath
```

This removes the secrets from your local shell history file. The secrets are now in Key Vault and nowhere else on your machine.

## 7. Receipts ingest — Graph permission + ApplicationAccessPolicy

### 7.1 Find the managed identity Object ID

From the Terraform output captured in §4.3, locate the Object ID (also called Principal ID) of `caj-cbb-receipts-ingest-prod`'s system-assigned managed identity. Or:

```bash
az containerapp job identity show \
  --resource-group rg-cbb-receipts-prod \
  --name caj-cbb-receipts-ingest-prod \
  --query principalId -o tsv
```

Save the result as `INGEST_MI_OBJ