# 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` + `CreatedAt` (auto-stamped by `AuditSaveChangesInterceptor`); the creator display name is resolved at read time, not stored | **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, extends `SoftDeleteEntity` (so it gets `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`, `IsDeleted` auto-stamped). Fields: `Id`, `Name`, `MinistryId`, `Description`, `VendorName`, `CheckNumber`, `Notes`. Owns `Lines`. The creator's display name is resolved at read time (mirroring `ReviewedByName`), not stored. - **`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 (`CreatedBy`/`CreatedAt` auto-stamped by the interceptor) | | 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.