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>
This commit is contained in:
kevin-asprec
2026-03-05 07:11:44 +08:00
parent 9af86c54c3
commit d54b517e2e
5 changed files with 748 additions and 630 deletions

View File

@@ -7,11 +7,13 @@ 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
@@ -21,45 +23,56 @@ autonomous: true
must_haves:
truths:
- "A collector can log a cash payment against a subscriber in the field and the system applies FIFO allocation to outstanding invoices"
- "Total collected and total remitted per collector are derived from the transaction log — no stored balance field"
- "Office staff can verify a remittance by entering their own counted total; variance is recorded but does not block completion"
- "Verified remittance creates a double-entry journal entry (DR Cash on Hand, CR Cash in Transit)"
- "Daily collection summary shows totals per collector: collected, remitted, variance, number of collections"
- "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: "Field collection logging with FIFO allocation reusing PaymentService pattern"
exports: ["CollectorService"]
provides: "Collection recording with FIFO allocation and zone enforcement"
exports: ["recordCollection", "voidCollection", "getCollectionHistory"]
- path: "src/lib/services/remittance-service.ts"
provides: "Remittance creation and two-party verification with JE posting"
exports: ["RemittanceService"]
provides: "Remittance creation and verification with JE"
exports: ["createRemittance", "verifyRemittance"]
- path: "src/lib/services/collection-report-service.ts"
provides: "Daily collection summary per collector"
exports: ["CollectionReportService"]
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/services/payment-service.ts"
via: "Reuses FIFO allocation pattern for invoice payment"
pattern: "PaymentService|recordPayment"
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: "Creates JE on verified remittance (DR 1010 Cash on Hand, CR 1030 Cash in Transit)"
pattern: "JournalEntryService|createEntry"
- from: "src/lib/services/collection-report-service.ts"
to: "prisma/schema.prisma"
via: "Aggregates Collection and Remittance records for daily summary"
pattern: "collection\\.(findMany|aggregate)"
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>
Build the collector field collection and remittance workflow with audit trail.
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 collector workflow — collectors log cash received in the field, the system tracks what they've collected, office staff verify remittances with independent counts, and the accounting ledger records verified cash movements. The daily summary report gives management visibility into collection operations.
Output: Collection model (lump-sum field payment with FIFO allocation), Remittance model (two-party verification), journal entry on verified remittance, daily collection summary report, integration tests.
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>
@@ -72,180 +85,198 @@ Output: Collection model (lump-sum field payment with FIFO allocation), Remittan
@.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/services/payment-service.ts
@src/lib/accounting/journal-entry-service.ts
@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 and Remittance Prisma models + new COA account</name>
<files>prisma/schema.prisma, src/lib/prisma-tenant.ts, src/lib/accounting/chart-of-accounts.ts, src/lib/accounting/seed-coa.ts</files>
<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>
**New enum:**
- `RemittanceStatus { PENDING, VERIFIED }`
- `CollectionStatus { COMPLETED, VOIDED }` (mirroring PaymentStatus pattern)
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.
**Collection model** — a field cash collection logged by a collector:
- id (uuid PK), tenantId
- collectorId (FK to User — the collector who collected)
- subscriberId (FK to Subscriber)
- amount (Decimal 10,2) — lump sum received from subscriber
- collectionDate (DateTime) — when cash was received in the field
- notes (String?)
- status (CollectionStatus default COMPLETED)
- remittanceId (String? FK to Remittance — linked when included in a remittance batch)
- paymentId (String? FK to Payment — the underlying Payment record created via PaymentService)
- createdAt, updatedAt
- @@index([tenantId]), @@index([tenantId, collectorId]), @@index([tenantId, collectionDate])
2. Add enums to schema.prisma:
- `enum CollectionStatus { COMPLETED VOIDED }`
- `enum RemittanceStatus { PENDING VERIFIED }`
**Remittance model** — a batch handoff of collected cash from collector to office:
- id (uuid PK), tenantId
- collectorId (FK to User)
- remittanceDate (DateTime) — when the collector handed over cash
- collectedTotal (Decimal 10,2) — sum of Collection amounts in this batch (system-calculated)
- verifiedTotal (Decimal 10,2?) — amount counted by office staff (null until verified)
- variance (Decimal 10,2?) — collectedTotal - verifiedTotal (system-calculated on verification)
- status (RemittanceStatus default PENDING)
- verifiedById (String? FK to User — office staff who verified)
- verifiedAt (DateTime?)
- journalEntryId (String?) — JE created on verification
- notes (String?)
- createdAt, updatedAt
- @@index([tenantId]), @@index([tenantId, collectorId]), @@index([tenantId, remittanceDate])
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])
**Update relations:**
- User: add `collections Collection[]`, `remittancesAsCollector Remittance[] @relation("RemittanceCollector")`, `remittancesVerified Remittance[] @relation("RemittanceVerifiedBy")`
- Subscriber: add `collections Collection[]`
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])
**New COA account:**
- Add account 1030 "Cash in Transit" (ASSET, DEBIT normal balance) to ISP_CHART_OF_ACCOUNTS in chart-of-accounts.ts
- This is a child of 1000 (Cash and Cash Equivalents)
- Update seed-coa.ts if needed to include it
- Purpose: When collector collects cash, it's in transit until verified remittance moves it to Cash on Hand (1010)
5. Add reverse relations on User: `collections Collection[]`, `verifiedRemittances Remittance[]`
Add reverse relation on Subscriber: `collections Collection[]`
**Add to TENANT_SCOPED_MODELS:** "collection", "remittance"
6. Run `npx prisma migrate dev --name add-collections-remittances`
Run `npx prisma migrate dev --name add-collections-remittances`
**Journal Entry Pattern for Collection:**
When collector logs a collection, it creates a Payment via PaymentService (reusing FIFO allocation) AND creates a Collection record linking the collector. The Payment JE is: DR 1030 Cash in Transit, CR 1100 AR. Note: use 1030 (not 1010) because cash is with the collector, not yet in the office.
**Journal Entry Pattern for Verified Remittance:**
DR 1010 Cash on Hand (verified amount)
CR 1030 Cash in Transit (verified amount)
This moves the cash from "in transit" to "on hand" upon office verification.
IMPORTANT: The collector collection payment must debit 1030 Cash in Transit (not 1010 Cash on Hand). This means CollectorService needs to create the Payment with a custom account override, or create its own JE pattern. The cleanest approach: CollectorService creates the Payment record directly (reusing the FIFO allocation logic from PaymentService but with 1030 as the debit account instead of 1010/1020). Extract the FIFO allocation logic into a shared helper if needed, or have CollectorService call PaymentService.recordPayment with a parameter indicating collector collection (which uses 1030 instead of 1010).
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` completes without errors
- `npx prisma generate` succeeds
- Schema has Collection, Remittance models with correct relations
- chart-of-accounts.ts includes 1030 Cash in Transit
- `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 and Remittance models exist, 1030 Cash in Transit added to COA, TENANT_SCOPED_MODELS updated, migration applied.</done>
<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: CollectorService, RemittanceService, CollectionReportService + APIs + 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/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>
<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>
**CollectorService** (`src/lib/services/collector-service.ts`):
- `recordCollection(db, { collectorId, subscriberId, amount, collectionDate, notes })`:
1. Verify collector is assigned to subscriber's zone (via ZoneService.getCollectorSubscribers or direct zone check)
2. Create a Payment record using PaymentService.recordPayment (or equivalent FIFO logic) — but with 1030 Cash in Transit as debit account instead of 1010/1020. Generate idempotencyKey as `coll-{collectorId}-{subscriberId}-{timestamp}`.
3. Create a Collection record linking collectorId, subscriberId, paymentId
4. Return the collection with payment allocation details
- `getCollectorCollections(db, collectorId, { dateFrom, dateTo })` — list collections for a collector in date range
- `voidCollection(db, collectionId)` — void the collection and its underlying payment (via PaymentService.voidPayment)
- `getUnremittedCollections(db, collectorId)` — collections not yet linked to a remittance
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
**RemittanceService** (`src/lib/services/remittance-service.ts`):
- `createRemittance(db, { collectorId, collectionIds, remittanceDate, notes })`:
1. Validate all collectionIds belong to this collector and are unremitted
2. Calculate collectedTotal as sum of collection amounts
3. Create Remittance record with status PENDING
4. Link collections to remittance (update collection.remittanceId)
5. Return remittance
- `verifyRemittance(db, { remittanceId, verifiedById, verifiedTotal, notes })`:
1. Load remittance, verify status is PENDING
2. Calculate variance = collectedTotal - verifiedTotal
3. Create journal entry via JournalEntryService: DR 1010 Cash on Hand (verifiedTotal), CR 1030 Cash in Transit (verifiedTotal). Reference type "Remittance".
4. Update remittance: verifiedTotal, variance, verifiedById, verifiedAt, journalEntryId, status = VERIFIED
5. Variance is recorded but does NOT block — remittance completes regardless
6. Return remittance with variance info
- `getRemittances(db, { collectorId?, dateFrom?, dateTo?, status? })` — list remittances with filters
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
**CollectionReportService** (`src/lib/services/collection-report-service.ts`):
- `getDailyCollectionSummary(db, { date, collectorId? })`:
1. Query collections for the date (or all collectors if no collectorId)
2. Query remittances for the date
3. Return per-collector summary: { collectorId, collectorName, totalCollected, totalRemitted, variance, collectionCount }
4. Include drill-down data: per-subscriber detail (subscriberName, amount, collectionDate)
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
**API Routes:**
- `POST /api/collections` — collector logs a collection. COLLECTOR role. Body: { subscriberId, amount, collectionDate, notes? }. Extracts collectorId from session.
- `GET /api/collections` — list collections. COLLECTOR sees own; ADMIN/OFFICE_STAFF see all or filter by collectorId query param.
- `GET /api/collections/[id]` — get collection detail with payment allocation info
- `POST /api/collections/[id]/void` void a collection. ADMIN, OFFICE_STAFF.
- `POST /api/remittances` — create remittance batch. COLLECTOR role. Body: { collectionIds, remittanceDate, notes? }
- `GET /api/remittances` — list remittances with filters. ADMIN, OFFICE_STAFF, COLLECTOR (own only).
- `POST /api/remittances/[id]/verify` — verify remittance. ADMIN, OFFICE_STAFF only. Body: { verifiedTotal, notes? }
- `GET /api/reports/collections` — daily collection summary. ADMIN, OFFICE_STAFF. Query params: date, collectorId?
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
**Integration Tests:**
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
`collector-service.test.ts`:
- Collector can log collection against subscriber in their zone
- Collector cannot collect from subscriber outside their zone
- Collection creates Payment with FIFO allocation (reuses PaymentService pattern)
- Collection uses 1030 Cash in Transit (not 1010)
- Void collection voids underlying payment
- getUnremittedCollections returns only collections not linked to remittance
- Collector balances derived from transactions (no stored balance field) — query collections sum vs remittances sum
`remittance-service.test.ts`:
- Create remittance batch from unremitted collections
- Cannot include already-remitted collections
- Verify remittance with matching total (variance = 0)
- Verify remittance with different total (variance recorded, not blocking)
- Verification creates JE: DR 1010, CR 1030
- Cannot verify already-verified remittance
- Daily collection summary returns correct totals per collector
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 vitest run` — full suite passes (no regressions)
- `npx tsc --noEmit` passes
</verify>
<done>Collectors can log field collections with FIFO allocation via 1030 Cash in Transit, create remittance batches, office staff verify with independent count, variance tracked, JE posted on verification, daily collection summary report working. All derived from transaction log — no stored balance fields.</done>
<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>
- Collector logs collection -> Payment created with FIFO allocation -> debit goes to 1030 Cash in Transit
- Collector creates remittance batch from unremitted collections
- Office staff verifies remittance -> JE posted (DR 1010 Cash on Hand, CR 1030 Cash in Transit)
- Variance tracked but does not block verification
- Daily summary shows per-collector totals: collected, remitted, variance, count
- Collector balances are DERIVED (sum of collections minus sum of verified remittances) — no stored balance
- Zone scoping enforced (collector can only collect from their assigned subscribers)
- All existing tests pass (no regressions)
- `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 and Remittance models with proper relations and migration
- 1030 Cash in Transit account added to COA
- CollectorService handles field collection with FIFO and zone scoping
- RemittanceService handles batch creation and two-party verification with JE
- CollectionReportService produces daily summary per collector
- Integration tests prove the full collection-to-remittance-to-JE flow
- No stored balance fields — all collector totals derived from transaction log
- 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>