Skip to main content

Service Unit Tests

Unit tests for domain services. These test business logic in isolation — no HTTP, no Filament, no real database in most cases (use factories + RefreshDatabase where needed).


RateResolutionServiceTest.php

RateResolutionService::resolve(Matter $matter, FirmUserProfile $lawyer, string $activityCode)

✓ returns matter-level rate when CASE_RATE_PLAN_SELECTIONS is set for this matter
✓ falls back to client rate plan when no matter-level rate is set
✓ falls back to lawyer's default_billing_rate when no client rate plan
✓ returns 0 for a non-billable activity code (is_billable = false)
✓ respects effective_from: rate plan not yet in effect returns null
✓ respects effective_to: expired rate plan is skipped, fallback used
✓ returns the correct rate when multiple rate plan versions exist (uses latest valid)
✓ partner attorney rate uses PARTNER_RATE_PLANS not LAWYER_RATE_PLANS

InvoiceGeneratorServiceTest.php

InvoiceGeneratorService::generate(Matter $matter)

✓ creates an Invoice record in DRAFT status
✓ creates InvoiceLineItems for each APPROVED time entry
✓ groups line items by activity_code (one line per code, sum of hours)
✓ applies correct tax rate from FIRM_TAX_RATES for the firm
✓ skips time entries that are already linked to an existing invoice
✓ marks generated time entries with invoiced_at timestamp
✓ throws InvoiceGenerationException when matter has no APPROVED entries
✓ throws InvoiceGenerationException when matter already has a DRAFT invoice
✓ total amount = sum of (hours × rate) + tax
✓ invoice is scoped to the correct law_firm_id
✓ does not modify any existing rows (append-only: new Invoice, new InvoiceLineItems)

RbacCheckerServiceTest.php

RbacCheckerService::check(User $user, string $permissionCode, int $firmId, ?int $matterId)

✓ returns true when user has a role with the matching permission
✓ returns false when user's role does not have the permission
✓ resource-level ALLOW grant overrides role denial
✓ resource-level DENY grant overrides role grant
✓ temporal: role assignment with ends_at in the past is ignored
✓ temporal: role assignment with starts_at in the future is ignored
✓ firm boundary: user with role in firm A returns false for firm B check
✓ matter-scoped role applies only to the specified matter, not all matters
✓ GLOBAL scope role applies across all matters in the firm

RbacCheckerService::visibleFields(User $user, string $resourceType, int $firmId)

✓ returns all fields when role has fields_mode = ALL
✓ returns only listed fields when role has fields_mode = ONLY
✓ returns all except listed fields when role has fields_mode = EXCEPT
✓ SSN field not included for non-BILLING_ADMIN roles
✓ SSN field included for BILLING_ADMIN role

AppendOnlyModelTest.php

AppendOnlyObserver (applied to Invoice, Payment, LawyerTimeEntry, ExpenseEntry, PaymentEvent)

✓ Invoice::create() succeeds (INSERT allowed)
✓ Invoice::update() throws AppendOnlyViolationException
✓ Invoice::delete() throws AppendOnlyViolationException
✓ Invoice::forceDelete() throws AppendOnlyViolationException
✓ Payment::create() succeeds
✓ Payment::update() throws AppendOnlyViolationException
✓ LawyerTimeEntry::create() succeeds
✓ LawyerTimeEntry::update() throws AppendOnlyViolationException (after SUBMITTED)
✓ LawyerTimeEntry in DRAFT status CAN be updated (update allowed while draft)
✓ ExpenseEntry::create() succeeds
✓ ExpenseEntry::update() throws AppendOnlyViolationException