Files
NetForge/.planning/phases/03-operational-modules/03-02-PLAN.md
kevin-asprec d54b517e2e docs(03): create phase plan
Phase 03: Operational Modules
- 5 plans in 3 waves
- Wave 1: 03-01 (zones), 03-03 (tickets) — parallel
- Wave 2: 03-02 (collector collections), 03-04 (job orders) — parallel
- Wave 3: 03-05 (technician compensation)
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:11:44 +08:00

285 lines
16 KiB
Markdown

---
phase: 03-operational-modules
plan: "02"
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
- prisma/schema.prisma
- src/lib/prisma-tenant.ts
- src/lib/accounting/chart-of-accounts.ts
- src/lib/services/collector-service.ts
- src/lib/services/remittance-service.ts
- src/lib/services/collection-report-service.ts
- src/app/api/collections/route.ts
- src/app/api/collections/[id]/route.ts
- src/app/api/collections/[id]/void/route.ts
- src/app/api/remittances/route.ts
- src/app/api/remittances/[id]/verify/route.ts
- src/app/api/reports/collections/route.ts
- src/lib/__tests__/collector-service.test.ts
- src/lib/__tests__/remittance-service.test.ts
autonomous: true
must_haves:
truths:
- "Collector can log a cash collection against a subscriber (lump sum, FIFO allocation)"
- "Collection creates JE: DR 1030 Cash in Transit, CR 1100 AR"
- "Office staff can verify a remittance by entering their counted total"
- "Remittance verification creates JE: DR 1010 Cash on Hand, CR 1030 Cash in Transit"
- "Variance between collector total and staff count is recorded but does NOT block remittance"
- "Collector can only collect from subscribers in their assigned zones"
- "Daily collection summary shows per-collector totals (collected, remitted, variance)"
- "Collector balances are derived from transactions, never stored"
artifacts:
- path: "prisma/schema.prisma"
provides: "Collection and Remittance models"
contains: "model Collection"
- path: "src/lib/accounting/chart-of-accounts.ts"
provides: "Account 1030 Cash in Transit"
contains: "1030"
- path: "src/lib/services/collector-service.ts"
provides: "Collection recording with FIFO allocation and zone enforcement"
exports: ["recordCollection", "voidCollection", "getCollectionHistory"]
- path: "src/lib/services/remittance-service.ts"
provides: "Remittance creation and verification with JE"
exports: ["createRemittance", "verifyRemittance"]
- path: "src/lib/services/collection-report-service.ts"
provides: "Daily collection summary report"
exports: ["getDailyCollectionSummary"]
- path: "src/lib/__tests__/collector-service.test.ts"
provides: "Collection tests including FIFO, zone enforcement, JE verification"
min_lines: 150
- path: "src/lib/__tests__/remittance-service.test.ts"
provides: "Remittance tests including variance, JE verification"
min_lines: 100
key_links:
- from: "src/lib/services/collector-service.ts"
to: "src/lib/accounting/journal-entry-service.ts"
via: "JournalEntryService.createEntry for collection JE (DR 1030, CR 1100)"
pattern: "JournalEntryService\\.createEntry"
- from: "src/lib/services/remittance-service.ts"
to: "src/lib/accounting/journal-entry-service.ts"
via: "JournalEntryService.createEntry for remittance verification JE (DR 1010, CR 1030)"
pattern: "JournalEntryService\\.createEntry"
- from: "src/lib/services/collector-service.ts"
to: "src/lib/services/zone-service.ts"
via: "zone scoping — validates subscriber is in collector's zones before collection"
pattern: "zone"
---
<objective>
Create the collector field collection and remittance system: Collection model (lump sum with FIFO allocation), Remittance model (two-party verification), accounting journal entries for both events, zone-scoped collection enforcement, daily collection summary report.
Purpose: This is the core cash flow tracking for field operations — collectors log what they receive, management verifies what they remit, and every peso is traceable through the double-entry ledger.
Output: Collection/Remittance models, 3 service files, 6 API routes, integration 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/03-operational-modules/03-CONTEXT.md
@.planning/phases/03-operational-modules/03-RESEARCH.md
@.planning/phases/03-operational-modules/03-01-SUMMARY.md
@prisma/schema.prisma
@src/lib/prisma-tenant.ts
@src/lib/accounting/chart-of-accounts.ts
@src/lib/accounting/journal-entry-service.ts
@src/lib/services/payment-service.ts (FIFO pattern reference — adapt for 1030 account)
@src/lib/services/zone-service.ts (zone scoping reference)
@src/lib/__tests__/payment.test.ts (test pattern reference)
</context>
<tasks>
<task type="auto">
<name>Task 1: Collection/Remittance schema, 1030 account, migration, tenant scoping</name>
<files>
prisma/schema.prisma
src/lib/prisma-tenant.ts
src/lib/accounting/chart-of-accounts.ts
</files>
<action>
1. Add 1030 Cash in Transit to ISP_CHART_OF_ACCOUNTS in chart-of-accounts.ts:
```
{ code: "1030", name: "Cash in Transit", accountType: "ASSET", normalBalance: "DEBIT", parentCode: "1000" }
```
Insert AFTER 1020 Cash in Bank and BEFORE 1100 Accounts Receivable to maintain code order.
2. Add enums to schema.prisma:
- `enum CollectionStatus { COMPLETED VOIDED }`
- `enum RemittanceStatus { PENDING VERIFIED }`
3. Add Collection model:
- id (uuid), tenantId
- collectorId (String, FK to User — the collector who made the collection)
- subscriberId (String, FK to Subscriber)
- amount (Decimal @db.Decimal(10,2)) — lump sum received from subscriber
- collectionDate (DateTime) — when collected in the field
- status (CollectionStatus, default COMPLETED)
- notes (String?)
- journalEntryId (String?) — JE created on collection (DR 1030, CR 1100)
- voidedAt (DateTime?), voidedById (String?), voidJournalEntryId (String?)
- createdAt, updatedAt
- Relations: collector -> User, subscriber -> Subscriber
- Add PaymentAllocation relation: collectionAllocations PaymentAllocation[] (reuse PaymentAllocation or create CollectionAllocation — prefer creating CollectionAllocation to avoid polluting PaymentAllocation with nullable fields)
- Actually, create CollectionAllocation as a separate model (same structure as PaymentAllocation but for collections):
id, tenantId, collectionId, invoiceId, amount, createdAt, @@index([collectionId]), @@index([invoiceId]), @@index([tenantId])
- @@unique([tenantId, collectorId, subscriberId, collectionDate]) — prevent double-recording same subscriber same day same collector
- @@index([tenantId]), @@index([tenantId, collectorId]), @@index([tenantId, collectionDate])
4. Add Remittance model:
- id (uuid), tenantId
- collectorId (String, FK to User)
- remittanceDate (DateTime) — the date of remittance
- collectedTotal (Decimal @db.Decimal(10,2)) — sum of collector's collections for the period (derived at creation time, stored for audit trail)
- verifiedTotal (Decimal? @db.Decimal(10,2)) — amount counted by office staff (null until verified)
- variance (Decimal? @db.Decimal(10,2)) — collectedTotal - verifiedTotal (null until verified)
- status (RemittanceStatus, default PENDING)
- verifiedById (String?, FK to User)
- verifiedAt (DateTime?)
- journalEntryId (String?) — JE created on verification (DR 1010, CR 1030)
- notes (String?)
- createdAt, updatedAt
- Relations: collector -> User, verifiedBy -> User
- @@index([tenantId]), @@index([tenantId, collectorId]), @@index([tenantId, remittanceDate])
5. Add reverse relations on User: `collections Collection[]`, `verifiedRemittances Remittance[]`
Add reverse relation on Subscriber: `collections Collection[]`
6. Run `npx prisma migrate dev --name add-collections-remittances`
7. Add Collection, CollectionAllocation, and Remittance to TENANT_SCOPED_MODELS in prisma-tenant.ts with FULL 12-operation extension blocks.
</action>
<verify>
- `npx prisma migrate dev` succeeds
- `npx tsc --noEmit` passes
- Grep chart-of-accounts.ts confirms "1030" exists
- Grep prisma-tenant.ts confirms "collection", "collectionAllocation", "remittance" in TENANT_SCOPED_MODELS
</verify>
<done>Collection, CollectionAllocation, and Remittance models exist, 1030 Cash in Transit added to COA, migration applied, tenant scoping configured.</done>
</task>
<task type="auto">
<name>Task 2: Collector service, remittance service, report service, APIs, and tests</name>
<files>
src/lib/services/collector-service.ts
src/lib/services/remittance-service.ts
src/lib/services/collection-report-service.ts
src/app/api/collections/route.ts
src/app/api/collections/[id]/route.ts
src/app/api/collections/[id]/void/route.ts
src/app/api/remittances/route.ts
src/app/api/remittances/[id]/verify/route.ts
src/app/api/reports/collections/route.ts
src/lib/__tests__/collector-service.test.ts
src/lib/__tests__/remittance-service.test.ts
</files>
<action>
1. Create src/lib/services/collector-service.ts:
- `recordCollection(tenantPrisma, tenantId, { collectorId, subscriberId, amount, collectionDate, notes? })`:
a. ZONE ENFORCEMENT: Query collector's zone assignments. Query subscriber's zoneId. If subscriber's zone is NOT in collector's assigned zones, throw error "Subscriber not in your assigned zones" (security boundary).
b. FIFO allocation: Query subscriber's unpaid invoices ordered by dueDate ASC (same pattern as PaymentService). Allocate lump sum amount across invoices. Create CollectionAllocation records for each invoice touched. Update invoice.amountPaid and invoice.status atomically.
c. Create JE via JournalEntryService.createEntry: DR 1030 Cash in Transit, CR 1100 Accounts Receivable. Look up accounts by code "1030" and "1100". Use referenceType "Collection", referenceId = collection.id.
d. Handle overpayment: any excess after all invoices paid goes to subscriber.creditBalance (same pattern as PaymentService).
e. All inside a $transaction. Pass tenantId explicitly in all create/update data.
f. Return collection record with allocations.
- `voidCollection(tenantPrisma, tenantId, collectionId, voidedById)`:
- Reverse the JE via JournalEntryService (same pattern as voidPayment)
- Reverse invoice.amountPaid and status for each allocation
- Reverse subscriber.creditBalance if overpayment was applied
- Mark collection as VOIDED
- `getCollectionHistory(tenantPrisma, { collectorId?, subscriberId?, dateFrom?, dateTo?, page?, limit? })` — filtered list
2. Create src/lib/services/remittance-service.ts:
- `createRemittance(tenantPrisma, tenantId, { collectorId, remittanceDate })`:
- Calculate collectedTotal: sum of all COMPLETED collections by this collector for this date (or unremitted collections if date-based grouping is complex — use all COMPLETED collections by collector where no remittance has been created yet). Simpler approach: sum COMPLETED collections by collectorId where collectionDate = remittanceDate.
- Create Remittance with status PENDING, collectedTotal set.
- `verifyRemittance(tenantPrisma, tenantId, { remittanceId, verifiedTotal, verifiedById, notes? })`:
- Load remittance, validate status is PENDING
- Calculate variance: collectedTotal - verifiedTotal (positive = collector short, negative = collector over)
- Create JE via JournalEntryService.createEntry: DR 1010 Cash on Hand (verifiedTotal), CR 1030 Cash in Transit (verifiedTotal). Note: JE is for verified amount, NOT collected total. Variance does NOT block.
- Update remittance: verifiedTotal, variance, verifiedById, verifiedAt, journalEntryId, status = VERIFIED
- `listRemittances(tenantPrisma, { collectorId?, status?, dateFrom?, dateTo? })` — filtered list
3. Create src/lib/services/collection-report-service.ts:
- `getDailyCollectionSummary(tenantPrisma, { date, collectorId? })`:
- For each collector active on the given date:
- collectedTotal: sum of COMPLETED collections on that date
- remittedTotal: sum of VERIFIED remittance verifiedTotal on that date
- variance: collectedTotal - remittedTotal
- collectionCount: count of collections
- Return array of { collectorId, collectorName, collectedTotal, remittedTotal, variance, collectionCount }
- `getCollectorCollectionDetail(tenantPrisma, { collectorId, date })`:
- Per-subscriber breakdown for drill-down: subscriber name, amount, collection time
4. Create API routes:
- POST /api/collections: withPermission("create", "Payment") -> recordCollection (collectors have "create" Payment permission)
- GET /api/collections: withPermission("read", "Payment") -> getCollectionHistory with query param filters
- GET /api/collections/[id]: withPermission("read", "Payment") -> single collection detail
- POST /api/collections/[id]/void: withPermission("manage", "Payment") -> voidCollection (admin/staff only)
- POST /api/remittances: withPermission("manage", "Payment") -> createRemittance (staff initiates)
- POST /api/remittances/[id]/verify: withPermission("manage", "Payment") -> verifyRemittance (staff verifies)
- GET /api/reports/collections: withPermission("read", "Report") -> getDailyCollectionSummary with date query param
5. Create src/lib/__tests__/collector-service.test.ts:
- Setup: create tenant (seeds COA including 1030 now), admin user, collector user, create 2 zones, assign collector to zone 1, create service plan, create 2 subscribers in zone 1, create 1 subscriber in zone 2, generate invoices for subscribers
- Test: recordCollection succeeds for subscriber in collector's zone
- Test: recordCollection FIFO allocates to oldest invoice first
- Test: recordCollection throws for subscriber NOT in collector's zones
- Test: collection creates JE with DR 1030, CR 1100 (verify journal entry lines)
- Test: overpayment adds to subscriber.creditBalance
- Test: voidCollection reverses JE and invoice allocations
- Test: cross-tenant isolation
- Cleanup: collectionAllocations -> collections -> invoiceLines -> invoices -> journalEntryLines -> null reversesEntryId -> journalEntries -> zoneAssignments -> subscribers -> servicePlans -> zones -> tenantSettings -> accountingPeriods -> accounts -> ticketCategories -> users -> tenant
6. Create src/lib/__tests__/remittance-service.test.ts:
- Setup: reuse similar setup, record some collections first
- Test: createRemittance calculates correct collectedTotal
- Test: verifyRemittance with matching amount (zero variance)
- Test: verifyRemittance with different amount (non-zero variance, still completes)
- Test: verification creates JE with DR 1010, CR 1030 for verifiedTotal
- Test: cannot verify already-verified remittance
- Cleanup: remittances -> collectionAllocations -> collections -> (same chain as above)
</action>
<verify>
- `npx vitest run src/lib/__tests__/collector-service.test.ts` — all tests pass
- `npx vitest run src/lib/__tests__/remittance-service.test.ts` — all tests pass
- `npx tsc --noEmit` passes
</verify>
<done>Collectors can record zone-scoped collections with FIFO allocation, collections create correct JEs (DR 1030 CR 1100), remittance verification creates correct JEs (DR 1010 CR 1030), variance recorded but non-blocking, daily summary report works, all tests pass.</done>
</task>
</tasks>
<verification>
- `npx prisma migrate dev` succeeds
- `npx tsc --noEmit` passes
- `npx vitest run src/lib/__tests__/collector-service.test.ts` — all green
- `npx vitest run src/lib/__tests__/remittance-service.test.ts` — all green
- Collection JEs use account 1030 (NOT 1010) — verified by test assertions on JE lines
- Remittance verification JEs use DR 1010, CR 1030
- Variance does not block remittance completion
</verification>
<success_criteria>
- Collection model with FIFO allocation (same pattern as PaymentService but with 1030)
- Zone enforcement on collections (collector can only collect from their zones)
- Remittance two-party verification (collector collects, staff counts and verifies)
- Correct accounting chain: Collection DR 1030/CR 1100, Remittance DR 1010/CR 1030
- Variance recorded but non-blocking
- Daily collection summary report with per-collector totals
- Collector balances derived (no stored balance field)
- All integration tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/03-operational-modules/03-02-SUMMARY.md`
</output>