# 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.
