docs(04): create phase plan — Inventory, Expenses, and Financial Reports
Phase 04: 5 plans in 2 waves - Wave 1: 04-01 (inventory event-ledger), 04-03 (expense tracking), 04-05 (financial reports) — parallel - Wave 2: 04-02 (asset management), 04-04 (expense reports + audit trail) — sequential - Ready for execution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
---
|
||||
phase: 04-inventory-expenses-and-financial-reports
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- prisma/schema.prisma
|
||||
- src/lib/services/expense-service.ts
|
||||
- src/lib/services/vendor-service.ts
|
||||
- src/lib/tenant.ts
|
||||
- src/app/api/expenses/route.ts
|
||||
- src/app/api/expenses/[id]/route.ts
|
||||
- src/app/api/expenses/[id]/approve/route.ts
|
||||
- src/app/api/expenses/categories/route.ts
|
||||
- src/app/api/expenses/categories/[id]/route.ts
|
||||
- src/app/api/vendors/route.ts
|
||||
- src/app/api/vendors/[id]/route.ts
|
||||
- src/lib/__tests__/expense-service.test.ts
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Staff can record an expense with amount, date, category, vendor, and description"
|
||||
- "Expense categories are pre-seeded at tenant creation and admin can add/edit custom categories"
|
||||
- "Vendors are managed entities with CRUD operations"
|
||||
- "Posted expense automatically creates a balanced JE (DR expense account, CR 2010 AP or 1010 Cash)"
|
||||
- "Optional approval workflow: DRAFT -> APPROVED -> POSTED when enabled; immediate post when disabled"
|
||||
- "Single file attachment per expense (receipt image or PDF path stored)"
|
||||
artifacts:
|
||||
- path: "prisma/schema.prisma"
|
||||
provides: "Vendor, ExpenseCategory, Expense models and ExpenseStatus enum"
|
||||
contains: "model Expense"
|
||||
- path: "src/lib/services/expense-service.ts"
|
||||
provides: "createExpense, approveExpense, postExpense, listExpenses"
|
||||
exports: ["ExpenseService"]
|
||||
- path: "src/lib/services/vendor-service.ts"
|
||||
provides: "createVendor, updateVendor, listVendors"
|
||||
exports: ["VendorService"]
|
||||
- path: "src/lib/__tests__/expense-service.test.ts"
|
||||
provides: "Tests for expense CRUD, approval workflow, JE posting, category seeding"
|
||||
min_lines: 100
|
||||
key_links:
|
||||
- from: "src/lib/services/expense-service.ts"
|
||||
to: "src/lib/accounting/journal-entry-service.ts"
|
||||
via: "JournalEntryService.createEntry when expense posts"
|
||||
pattern: "JournalEntryService\\.createEntry"
|
||||
- from: "src/lib/tenant.ts"
|
||||
to: "prisma.expenseCategory.createMany"
|
||||
via: "Expense category seeding in createTenant transaction"
|
||||
pattern: "expenseCategory\\.createMany"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build expense tracking with vendor management, category management (pre-seeded at tenant creation), optional approval workflow, and automatic journal entry posting on expense approval/post.
|
||||
|
||||
Purpose: Completes the "what's been spent" visibility for ISP owners. Every expense hits the double-entry ledger automatically, ensuring the financial reports in 04-05 include all costs.
|
||||
Output: Vendor, ExpenseCategory, Expense schema; VendorService, ExpenseService; API routes; expense category seeding; tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:\Users\KevinAsprec\.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-inventory-expenses-and-financial-reports/04-CONTEXT.md
|
||||
@prisma/schema.prisma
|
||||
@src/lib/accounting/journal-entry-service.ts
|
||||
@src/lib/accounting/chart-of-accounts.ts
|
||||
@src/lib/tenant.ts
|
||||
@src/lib/services/payment-service.ts (reference for JE-posting service pattern)
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Schema — Vendor, ExpenseCategory, Expense models + expense category seeding</name>
|
||||
<files>
|
||||
prisma/schema.prisma
|
||||
src/lib/tenant.ts
|
||||
src/lib/accounting/chart-of-accounts.ts
|
||||
</files>
|
||||
<action>
|
||||
**Enums:**
|
||||
- `ExpenseStatus`: `DRAFT`, `APPROVED`, `POSTED`, `VOIDED`
|
||||
- `ExpensePaymentMethod`: `CASH`, `BANK_TRANSFER`, `CHECK` — how the expense was paid
|
||||
|
||||
**Vendor model:**
|
||||
- id (UUID), tenantId (String)
|
||||
- name (String), contactPerson (String?), phone (String?), email (String?), address (String?)
|
||||
- servicesProvided (String? — freetext description of what vendor provides)
|
||||
- isActive (Boolean @default(true))
|
||||
- createdAt, updatedAt
|
||||
- expenses Expense[]
|
||||
- @@unique([tenantId, name])
|
||||
- @@index([tenantId])
|
||||
|
||||
**ExpenseCategory model:**
|
||||
- id (UUID), tenantId (String)
|
||||
- name (String), description (String?)
|
||||
- accountCode (String — maps to COA expense account, e.g., "5040" for bandwidth. This links the category to a specific expense account for JE posting)
|
||||
- isSystemCategory (Boolean @default(false) — true for pre-seeded categories, cannot be deleted)
|
||||
- isActive (Boolean @default(true))
|
||||
- createdAt, updatedAt
|
||||
- expenses Expense[]
|
||||
- @@unique([tenantId, name])
|
||||
- @@index([tenantId])
|
||||
|
||||
**Expense model:**
|
||||
- id (UUID), tenantId (String)
|
||||
- expenseNumber (String — auto-generated "EXP-NNNN")
|
||||
- categoryId (String) — FK to ExpenseCategory
|
||||
- vendorId (String?) — FK to Vendor (optional)
|
||||
- amount (Decimal @db.Decimal(10,2))
|
||||
- expenseDate (DateTime)
|
||||
- description (String)
|
||||
- paymentMethod (ExpensePaymentMethod)
|
||||
- status (ExpenseStatus @default(DRAFT))
|
||||
- attachmentPath (String? — file path for receipt image/PDF)
|
||||
- journalEntryId (String? — set when expense is posted)
|
||||
- createdById (String) — FK to User
|
||||
- approvedById (String?) — FK to User
|
||||
- approvedAt (DateTime?)
|
||||
- postedAt (DateTime?)
|
||||
- voidedAt (DateTime?)
|
||||
- createdAt, updatedAt
|
||||
- Relations: category ExpenseCategory, vendor Vendor?, createdBy User, approvedBy User?
|
||||
- @@unique([tenantId, expenseNumber])
|
||||
- @@index([tenantId]), @@index([tenantId, categoryId]), @@index([tenantId, vendorId]), @@index([tenantId, status])
|
||||
|
||||
Add User relations: `createdExpenses Expense[] @relation("ExpenseCreatedBy")`, `approvedExpenses Expense[] @relation("ExpenseApprovedBy")`
|
||||
|
||||
**COA additions** (chart-of-accounts.ts):
|
||||
Add these expense sub-accounts under 5000 if not already present:
|
||||
- 5080 "Fuel and Transportation" (parentCode: "5000", EXPENSE, DEBIT) — common Philippine ISP expense
|
||||
- 5085 "Rent Expense" (parentCode: "5000", EXPENSE, DEBIT)
|
||||
|
||||
This brings the COA to 31 accounts.
|
||||
|
||||
**Expense category seeding** (tenant.ts):
|
||||
Inside the createTenant $transaction, after ticket categories, seed default expense categories:
|
||||
```
|
||||
[
|
||||
{ name: "Internet Bandwidth", accountCode: "5040", isSystemCategory: true },
|
||||
{ name: "Equipment & Supplies", accountCode: "5030", isSystemCategory: true },
|
||||
{ name: "Salary & Wages", accountCode: "5010", isSystemCategory: true },
|
||||
{ name: "Technician Compensation", accountCode: "5020", isSystemCategory: true },
|
||||
{ name: "Office Supplies", accountCode: "5050", isSystemCategory: true },
|
||||
{ name: "Utilities", accountCode: "5060", isSystemCategory: true },
|
||||
{ name: "Fuel & Transportation", accountCode: "5080", isSystemCategory: true },
|
||||
{ name: "Rent", accountCode: "5085", isSystemCategory: true },
|
||||
{ name: "Other", accountCode: "5090", isSystemCategory: true },
|
||||
]
|
||||
```
|
||||
Use tx.expenseCategory.createMany with tenantId injected.
|
||||
|
||||
Run `npx prisma generate` after schema changes.
|
||||
</action>
|
||||
<verify>npx prisma validate passes; tenant.ts compiles without errors</verify>
|
||||
<done>Vendor, ExpenseCategory, Expense models exist; expense categories pre-seeded in createTenant; COA has 31 accounts</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: VendorService, ExpenseService, API routes, migration, and tests</name>
|
||||
<files>
|
||||
src/lib/services/vendor-service.ts
|
||||
src/lib/services/expense-service.ts
|
||||
src/app/api/expenses/route.ts
|
||||
src/app/api/expenses/[id]/route.ts
|
||||
src/app/api/expenses/[id]/approve/route.ts
|
||||
src/app/api/expenses/categories/route.ts
|
||||
src/app/api/expenses/categories/[id]/route.ts
|
||||
src/app/api/vendors/route.ts
|
||||
src/app/api/vendors/[id]/route.ts
|
||||
src/lib/__tests__/expense-service.test.ts
|
||||
</files>
|
||||
<action>
|
||||
**VendorService** (`src/lib/services/vendor-service.ts`):
|
||||
Static class:
|
||||
- `createVendor(tenantPrisma, data)` — create vendor, validate unique name per tenant
|
||||
- `updateVendor(tenantPrisma, vendorId, data)` — update name, contact, etc.
|
||||
- `listVendors(tenantPrisma, { isActive? })` — list with optional filter
|
||||
- `getVendor(tenantPrisma, vendorId)` — single vendor detail
|
||||
|
||||
**ExpenseService** (`src/lib/services/expense-service.ts`):
|
||||
Static class:
|
||||
- `createExpense(tenantPrisma, tenantId, data)`:
|
||||
- Auto-generate expenseNumber (pattern: "EXP-NNNN" sequential per tenant — same approach as invoice/ticket numbering)
|
||||
- Validate category exists and is active
|
||||
- Validate vendor exists if vendorId provided
|
||||
- If approval workflow disabled (default for now — no TenantSettings field needed, just default to immediate post): create as DRAFT, then immediately call postExpense
|
||||
- If approval workflow enabled: create as DRAFT only
|
||||
- Return created expense
|
||||
|
||||
- `approveExpense(tenantPrisma, tenantId, { expenseId, approvedById })`:
|
||||
- Validate status is DRAFT
|
||||
- Update status to APPROVED, set approvedById and approvedAt
|
||||
- Then call postExpense
|
||||
|
||||
- `postExpense(tenantPrisma, tenantId, { expenseId, postedById })`:
|
||||
- Validate status is DRAFT or APPROVED
|
||||
- Look up the expense's category.accountCode to find the DR account
|
||||
- Determine CR account: if paymentMethod is CASH -> 1010 Cash on Hand; if BANK_TRANSFER -> 1020 Cash in Bank; if CHECK -> 1020 Cash in Bank
|
||||
- Create JE via JournalEntryService.createEntry: DR {category expense account}, CR {cash account}. Amount = expense.amount. Source=SYSTEM, referenceType="Expense", referenceId=expense.id, description="Expense: {expense.description}"
|
||||
- Update expense: status=POSTED, journalEntryId, postedAt=now()
|
||||
|
||||
- `voidExpense(tenantPrisma, tenantId, { expenseId, voidedById })`:
|
||||
- Only POSTED expenses can be voided
|
||||
- Reverse the JE via JournalEntryService.reverseEntry
|
||||
- Update expense: status=VOIDED, voidedAt=now()
|
||||
|
||||
- `listExpenses(tenantPrisma, filters?)` — filter by status, categoryId, vendorId, date range
|
||||
- `getExpense(tenantPrisma, expenseId)` — detail with category, vendor, createdBy includes
|
||||
|
||||
**API Routes:**
|
||||
- `POST /api/expenses` — create expense (ADMIN, OFFICE_STAFF). Body: { categoryId, vendorId?, amount, expenseDate, description, paymentMethod, attachmentPath? }
|
||||
- `GET /api/expenses` — list expenses (ADMIN, OFFICE_STAFF). Query: status, categoryId, vendorId, startDate, endDate
|
||||
- `GET /api/expenses/[id]` — expense detail (ADMIN, OFFICE_STAFF)
|
||||
- `POST /api/expenses/[id]/approve` — approve expense (ADMIN only)
|
||||
- `GET /api/expenses/categories` — list categories (ADMIN, OFFICE_STAFF)
|
||||
- `POST /api/expenses/categories` — create category (ADMIN). Body: { name, description?, accountCode }
|
||||
- `PUT /api/expenses/categories/[id]` — update category (ADMIN). Cannot change isSystemCategory.
|
||||
- `GET /api/vendors` — list vendors (ADMIN, OFFICE_STAFF)
|
||||
- `POST /api/vendors` — create vendor (ADMIN, OFFICE_STAFF). Body: { name, contactPerson?, phone?, email?, address?, servicesProvided? }
|
||||
- `GET /api/vendors/[id]` — vendor detail (ADMIN, OFFICE_STAFF)
|
||||
- `PUT /api/vendors/[id]` — update vendor (ADMIN, OFFICE_STAFF)
|
||||
|
||||
**Migration:**
|
||||
Apply via Docker exec psql + prisma migrate resolve --applied. Migration name: `add_expense_vendor_models`.
|
||||
|
||||
**Tests** (`src/lib/__tests__/expense-service.test.ts`):
|
||||
1. Expense categories seeded at tenant creation (verify 9 default categories exist)
|
||||
2. Create vendor with CRUD operations
|
||||
3. Create expense with valid category and vendor — status transitions to POSTED with JE
|
||||
4. Verify posted expense JE: DR correct expense account (from category.accountCode), CR 1010 (cash)
|
||||
5. Create expense with BANK_TRANSFER — CR account is 1020
|
||||
6. Void expense — JE reversed, status=VOIDED
|
||||
7. Reject void on non-POSTED expense
|
||||
8. Create custom expense category
|
||||
9. Cannot delete system expense category
|
||||
10. List expenses with filters (by category, vendor, date range)
|
||||
|
||||
Cleanup order: expenses -> vendors -> expenseCategories (non-system) -> journalEntryLines -> null reversesEntryId -> journalEntries -> accountingPeriods -> accounts -> ticketCategories -> expenseCategories (system) -> users -> tenant
|
||||
</action>
|
||||
<verify>npx jest expense-service --verbose passes all tests</verify>
|
||||
<done>VendorService and ExpenseService handle full CRUD, approval workflow, JE posting, and voiding. All API routes respond correctly. All tests pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `npx prisma validate` passes
|
||||
- `npx jest expense-service --verbose` — all tests pass
|
||||
- Expense creates JE with correct DR/CR accounts
|
||||
- Expense categories seeded at tenant creation (9 defaults)
|
||||
- Vendor CRUD works
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Staff can record expenses with amount, date, category, vendor, and description
|
||||
- Expense categories pre-seeded (bandwidth, equipment, salary, etc.) and admin can add custom
|
||||
- Vendors are managed entities
|
||||
- Every posted expense creates a balanced JE
|
||||
- All tests pass
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-inventory-expenses-and-financial-reports/04-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user