5d03e42302
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
130 lines
6.0 KiB
Markdown
130 lines
6.0 KiB
Markdown
# 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.
|