From d4c20df34f935c26c9d34350d8b1da09e3710451 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Thu, 25 Jun 2026 14:37:00 -0700 Subject: [PATCH] docs(expense-snapshot): design spec for vendor payment snapshot Co-Authored-By: Claude Opus 4.8 --- ...26-06-25-vendor-payment-snapshot-design.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md diff --git a/docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md b/docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md new file mode 100644 index 0000000..f437a61 --- /dev/null +++ b/docs/superpowers/specs/2026-06-25-vendor-payment-snapshot-design.md @@ -0,0 +1,128 @@ +# Vendor Payment Snapshot — Design + +**Date:** 2026-06-25 +**Status:** Approved (design); pending implementation plan + +## Problem + +Bookkeepers re-enter the same recurring vendor payments every month — rent, internet, +a fixed catered-meal cost — retyping vendor, categories, amounts, and memo each time. +The only thing that genuinely changes per entry is the **Expense Date**. + +A **Snapshot** lets a user save the current vendor-payment form as a reusable, named +template and re-apply it later with one click, pre-filling everything except the date. + +## Scope + +- Applies to **vendor payments only** (`Expense.Type == "VendorPayment"`). +- Snapshots are **shared church-wide** — every user with vendor-payment access sees the + full list — and each row is **tagged with the creator** for accountability. +- A snapshot is **independent** of any expense: deleting an expense never affects a + snapshot, and applying a snapshot creates a brand-new expense. + +## What a snapshot captures + +Everything needed to refill the vendor-payment form **except `ExpenseDate`**. + +| Group | Fields | +|---|---| +| Label | `Name` (required, user-supplied, e.g. "Monthly Rent — Landlord X") | +| Header | `MinistryId`, `Description`, `VendorName`, `CheckNumber`, `Notes` | +| Lines (1..n) | `CategoryGroupId`, `SubCategoryId`, `Amount`, `FunctionalClass`, `Description` | +| Audit | `CreatedBy`, `CreatedByName`, `CreatedAt` (+ soft-delete / auditable fields) | + +**Excluded:** `ExpenseDate` (always starts fresh / today), the receipt file, and `MemberId` +(not used in vendor mode). + +**Note on `CheckNumber`:** captured to honor "only Expense Date is excluded." Because every +field stays editable on apply (see below), a stale check number is harmless if the user +overwrites it. Captured value is shown editable in the form. + +## On apply + +- Pre-fill **all** header + line fields from the snapshot. +- **All fields remain fully editable** — the snapshot is a starting point, not a lock. + A varying value (e.g. this month's meal cost) is a one-field tweak. +- `ExpenseDate` starts at today (fresh), never taken from the snapshot. +- The user then saves it as a normal expense via the existing `create` path. + +## Backend + +### Entities (`API/ROLAC.API/Entities/`) + +- **`ExpenseSnapshot`** — header. Fields: `Id`, `Name`, `MinistryId`, `Description`, + `VendorName`, `CheckNumber`, `Notes`, `CreatedBy`, `CreatedByName`, plus auditable + (`CreatedAt`, `UpdatedAt`, `UpdatedBy`) and soft-delete (`IsDeleted`). Owns `Lines`. +- **`ExpenseSnapshotLine`** — mirrors `ExpenseLine`: `Id`, `SnapshotId` (cascade), + `CategoryGroupId`, `SubCategoryId`, `Amount`, `FunctionalClass`, `Description`. + Category FKs use `Restrict` delete (same as `ExpenseLine`). + +Registered as `DbSet`s in `API/ROLAC.API/Data/AppDbContext.cs` with a soft-delete query +filter on `ExpenseSnapshot`. One EF migration adds both tables (PostgreSQL). + +### DTOs (`API/ROLAC.API/DTOs/Expense/`) + +- `ExpenseSnapshotDto` — list + detail response, includes `lines`, `createdByName`, + computed `totalAmount` and `lineCount` for the list view. +- `CreateExpenseSnapshotRequest` — same shape as the expense create payload minus + `expenseDate`/receipt/`memberId`, plus required `name`. Reused for update. + +### Controller (`API/ROLAC.API/Controllers/ExpenseSnapshotsController.cs`) + +Route prefix `api/expense-snapshots`. Authorization reuses the vendor-payment **Write** +permission check used by `ExpensesController`. + +| Method | Route | Purpose | +|---|---|---| +| GET | `/` | List all snapshots (shared), newest first, with `createdByName` + totals | +| GET | `/{id}` | Full snapshot incl. lines (for apply / management edit) | +| POST | `/` | Create from form payload; stamps `CreatedBy`/`CreatedByName` from current user | +| PUT | `/{id}` | Update (rename and/or re-save fields from the management page) | +| DELETE | `/{id}` | Soft-delete | + +`CreatedBy` is read with the `?? "sub"` JWT fallback used elsewhere in the project. + +## Frontend + +### Service + models (`APP/src/app/features/expense/`) + +- `services/expense-snapshot-api.service.ts` — `getAll()`, `getById(id)`, `create(req)`, + `update(id, req)`, `delete(id)`. +- `models/expense-snapshot.model.ts` — `ExpenseSnapshotDto`, `CreateExpenseSnapshotRequest`, + line interface. + +### Expense form dialog (vendor mode only) + +`components/expense-form-dialog/` — gated on `mode === 'vendor'`: + +- **"Load from snapshot…"** picker near the top of the form. Selecting one fetches the + snapshot detail and patches the FormGroup, reusing the existing line-building logic so + category sub-lists populate correctly. `ExpenseDate` is left at today. +- **"Save as snapshot"** button. Prompts for the required `Name`, then posts the current + header + lines via `create()`. Shows a success toast; does not submit the expense. + +### Management page + +A simple page wired into the finance sidebar nav (`portals/user-portal`, +`financeNavItems` + `getPageTitle`, per the unified-header route-data convention): + +- List columns: label (`Name`), vendor, total amount, created-by, created date. +- Actions: **rename** and **delete** (row context menu per house convention; single + primary action may be inline). +- **Mobile-friendly:** desktop `hidden md:block` grid + a `md:hidden` card list; layout + via Tailwind utilities (no `display` in component SCSS), per project house rules. + +## Testing + +- **Backend:** controller/integration tests for create → list → get → update → delete, + asserting `ExpenseDate` is never stored and `CreatedByName` is stamped. Lines round-trip + intact. +- **Frontend:** service unit tests (inline-template components per the test-runner gotcha) + covering apply-patches-form-without-date and save-posts-current-state. + +## Out of scope (YAGNI) + +- Per-field "ask every time" configuration (decided: all fields editable instead). +- Snapshot from an already-saved expense (decided: save from the form only). +- Per-user private snapshots (decided: shared with creator tag). +- Reimbursement-mode snapshots.