---
name: papa-expenses
description: Weekly expense report for papa Antoine's home care. Fetches receipts from the WhatsApp group "Expenses Papa", decrypts and OCRs images, categorises all items, builds a formatted Markdown report, generates a compressed PDF, and commits everything to git. Run this skill every Tuesday evening or Wednesday morning.
---

# Papa Expenses — Weekly Report

## Repo
`/home/mnm/workspaces/papa`

---

## Workflow

Run the full workflow in order. Every step is programmatic — only pause to ask when genuinely ambiguous.

### 1. Determine period

Period is **Wednesday → Tuesday** (7 days).
Default start = last Wednesday from today. Derive end = start + 6 days.

```bash
START=$(date -d 'last wednesday' +%Y-%m-%d)
END=$(date -d "$START + 6 days" +%Y-%m-%d)
PERIOD="${START}_${END}"
DIR="/home/mnm/workspaces/papa/reports/$PERIOD"
```

If the user provides a start date, use that instead.

---

### 2. Fetch WA messages

```bash
curl -s \
  -H "X-Device-Id: 4651709f-88a1-4861-90e7-383498ca3250" \
  "http://localhost:19283/chat/120363426705649036@g.us/messages?limit=200&offset=0"
```

Filter to the period window. Separate:
- **Text messages** (content field non-empty) → direct expense entries
- **Image messages** (media_type = "image") → receipts to decrypt and OCR

---

### 3. Decrypt & OCR images

For each image, get media key from SQLite:

```bash
SQLITE=/nix/store/28vqy6w3pi18wy12gd4fr3k7if1ba4j6-sqlite-3.50.4-bin/bin/sqlite3
$SQLITE /home/mnm/.local/share/whatsapp-web/storages/chatstorage.db \
  "SELECT hex(media_key), url FROM messages WHERE id='<MSG_ID>' LIMIT 1;"
```

Decrypt (WhatsApp AES-256-CBC + HKDF, info = "WhatsApp Image Keys"):

```js
// node --input-type=module
import { createDecipheriv, hkdfSync } from 'crypto';
import { writeFileSync } from 'fs';
const mediaKey = Buffer.from('<HEX_KEY>', 'hex');
const exp = hkdfSync('sha256', mediaKey, Buffer.alloc(0), 'WhatsApp Image Keys', 112);
const iv = Buffer.from(exp).subarray(0,16), aesKey = Buffer.from(exp).subarray(16,48);
const resp = await fetch('<URL>');
const enc = Buffer.from(await resp.arrayBuffer());
const d = createDecipheriv('aes-256-cbc', aesKey, iv); d.setAutoPadding(false);
writeFileSync('<OUTFILE>', Buffer.concat([d.update(enc.subarray(0, enc.length-10)), d.final()]));
```

OCR via Ollama HTTP API (do NOT use `ollama run` + stdin — it hangs):

```bash
node -e "
const fs=require('fs');
const b64=fs.readFileSync('<IMAGE>').toString('base64');
fs.writeFileSync('/tmp/ocr-req.json', JSON.stringify({
  model: 'qwen3-vl:235b-cloud',
  prompt: 'Extract ALL text and numbers from this receipt. List each line item with its amount.',
  images: [b64], stream: false
}));"
curl -s http://localhost:11434/api/generate \
  -H "Content-Type: application/json" \
  -d @/tmp/ocr-req.json --max-time 120 | \
  node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); console.log(d.response);"
```

Save decrypted images to `$DIR/receipts/`.

---

### 4. Parse & categorise all items

Process text messages and OCR output into structured line items.

**Text message format** (people type these directly):
```
$AMOUNT description YYYY/MM/DD
$AMOUNT description (date in text)
```
Parse amount, description, date. Categorise immediately.

**OCR receipts** — identify store/app, extract line items, amounts, date.

#### Categorisation rules

| Category | What goes in |
|----------|-------------|
| **Setup** (capex) | Equipment, furniture, one-off installations (camera, router, etc.) |
| **Groceries** | Food items, dairy, cleaning supplies, personal care, medical consumables (underpads, aprons, wipes) — anything from Toters Fresh or similar delivery apps |
| **Household** | Small non-consumable hardware/tools (mops, sticks, lighters) |
| **Food** | Restaurant meals, food delivery orders |
| **Medical** | Physiotherapy sessions, nurse provision, doctor visits |
| **Cleaning** | Housecleaner sessions (log hours × rate) |

**Recurring defaults** (pre-fill every week unless contradicted):
| Item | Rate | Default |
|------|------|---------|
| Nurse | $60/24h | 7 days = $420 *(provision)* |
| Housecleaner | $5/h | fill hours when known |
| Physiotherapy | $40/45 min | fill sessions when known |

#### Currency rules
- Lebanese receipts → LBP column only
- USD-priced items → USD column only
- FX conversion (to compute totals) → fill **both** columns
- Food section total row: fill both (LBP sum + USD equivalent at ~89,500 LBP/USD)

---

### 5. Build the report file

File: `$DIR/${PERIOD}_expenses-papa.md`

#### Structure
```
# Expenses Report — Papa's Care
### <Mon DD Mon> – <Tue DD Mon YYYY>

---

## Summary
[Weekly costs table]
[One-time costs table]
Grand total: ~$X

---
## 1. Setup
## 2. Groceries
## 3. Household
## 4. Food
## 5. Medical
## 6. Cleaning
```

#### Table format (all breakdown sections)
```markdown
| # | Date | Description | Amount (LBP) | Amount (USD) |
|--:|------|-------------|-------------:|-------------:|
```
- Items numbered sequentially within section
- Sorted by date ascending; undated (`—`) at bottom
- Delivery & tips consolidated as one line per section
- No order subtotals, no comments, no footnotes

#### Summary tables
```markdown
**Weekly costs**
| Category | Amount (USD) |

**One-time costs**
| Category | Amount (USD) |

**Grand total: ~$X**
```

Use the most recent completed weekly report as the exact Markdown/PDF formatting template for the next one. Preserve the previous report's headings, summary tables, section order, blank-section handling, total rows, spacing, and table alignment; only update dates, line items, category totals, receipt archive contents, and week-specific details.

Fallback formatting reference if no prior report is available:
`reports/2026-04-14_2026-04-21/2026-04-14_2026-04-21_expenses-papa.md`

---

### 6. Generate PDF

Use the same PDF renderer/template as the previous completed report: **pandoc Markdown → standalone HTML with `pdf-template.css` → HeadlessChrome print-to-PDF → ghostscript compress**. The resulting PDF should have metadata like the previous report (`Title: expenses-report-papa`, `Creator: HeadlessChrome`).

```bash
cd /home/mnm/workspaces/papa

tmpdir="${TMPDIR:-/tmp}/papa-pdf"; mkdir -p "$tmpdir"
HTML="$tmpdir/${PERIOD}_expenses-papa.html"
RAW="$DIR/${PERIOD}_expenses-papa.raw.pdf"
OUT="$DIR/${PERIOD}_expenses-papa.pdf"
CHROMIUM="$(find /nix/store -maxdepth 3 -type f -path '*/chromium-*/bin/chromium' | sort | tail -1)"

nix run nixpkgs#pandoc -- \
  "$DIR/${PERIOD}_expenses-papa.md" \
  --standalone \
  --embed-resources \
  --css=/home/mnm/workspaces/papa/pdf-template.css \
  -o "$HTML"

"$CHROMIUM" \
  --headless=new \
  --no-sandbox \
  --print-to-pdf="$RAW" \
  --print-to-pdf-no-header \
  --no-pdf-header-footer \
  "file://$HTML"

gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook \
  -dNOPAUSE -dQUIET -dBATCH \
  -sOutputFile="$OUT" "$RAW"

qpdf --linearize "$OUT" "$OUT.tmp"
mv "$OUT.tmp" "$OUT"

rm "$RAW"
```

Do **not** use a Typst-based `build-pdf`. Only run the explicit Chrome command above.

---

### 7. Commit

```bash
cd /home/mnm/workspaces/papa
git add "reports/$PERIOD" AGENTS.md
git commit -m "report: $PERIOD expenses"
```

Or: `commit-week $PERIOD`

---

## Judgment calls

Pause and ask the user only when:
- An OCR item is ambiguous and could fit multiple categories
- An amount seems anomalous (e.g. 10× higher than usual)
- A receipt date is illegible or contradicts the message timestamp
- A new recurring cost appears that isn't in the defaults table

Otherwise proceed autonomously.
