plan
Implementation Plan: Funding Stream Balance in Package Sidebar
Feature Code: FRB (Funding Remaining Balance) Epic: TP-2501 (Budget Reloaded) Spec: spec.md Created: 2025-12-09 Status: Draft
Technical Context
Technology Stack
- Backend: Laravel 12 (PHP 8.3.28)
- Frontend: Vue 3 + Inertia.js v2, TypeScript
- Styling: Tailwind CSS v3
- Package Manager: npm, composer
- Testing: Pest v3 (feature & unit tests)
- Database: MySQL (read-only queries for funding data)
Critical Dependencies
- FundingStreamData - Existing DTO with
availableAmount,externalId,colourproperties - FundingStreamBadge.vue - Existing Vue component for badge rendering
- PackageViewData - Distributed to all package tabs via Inertia
- BudgetPlanController - Quarter determination logic (existing)
- Quarter model -
Quarter::getCurrent()static method - BudgetPlan model -
getSelectableQuarters()method - Package model - Relationship to BudgetPlan
- PackageSidebar.vue - Target component for integration
Constraints
- Performance: No new database queries; data pre-fetched with package view data
- Latency: Must load within standard package view latency (<500ms)
- Accessibility: Badge descriptions required; colors not sole identifier
- Compatibility: Must work on all package view tabs (11 tabs total)
- Styling: Match existing funding UI patterns; support dark mode if sidebar has it
- Data Accuracy: Funding amounts must match Service Plan view exactly
Known Limitations
- Quarter determination reuses existing Service Plan logic (no new quarter calculations needed)
- No real-time updates; relies on Inertia re-renders during tab navigation
- Empty state requires explicit styling (not just hidden)
Design Decisions
1. Data Architecture
Decision: Extend PackageViewData to include funding summary, calculated once per page load.
Rationale:
- Avoids N+1 queries; data already loaded for Service Plan
- Reuses existing quarter determination logic from BudgetPlanController
- Single source of truth: funding data matches Service Plan view
- No new database access patterns needed
Implementation:
- Add
fundingSummaryproperty toPackageViewDataDTO - Populate in
PackageService::getTabData()method - Include full
FundingStreamDatacollection for current quarter
2. Quarter Determination Strategy
Decision: Mirror Service Plan’s quarter selection logic exactly.
Rationale:
- Coordinator sees consistent funding across all tabs
- No duplicate logic; reuse existing quarter determination
- Handles edge cases (between quarters, future quarters, etc.)
- Automatically respects
?quarter=XURL parameter
Implementation Flow:
1. Check URL parameter (?quarter=X)2. If not set, use Quarter::getCurrent()3. If current not selectable, use package commencement quarter4. Fallback to budget plan commencement quarterKey Integration Points:
- Extract quarter selection from
BudgetPlanController::show()into reusable service - Or: Call same logic from both BudgetPlanController and PackageService
3. Component Architecture
Decision: Create new lightweight FundingStreamBalanceSidebar.vue component that wraps existing FundingStreamBadge.vue.
Rationale:
- Keeps sidebar logic modular and testable
- Reuses battle-tested badge component
- Easier to maintain and extend
- Separates presentation from data logic
File Structure:
resources/js/Components/Package/Sidebar/├── FundingStreamBalanceSidebar.vue (NEW - wrapper component)├── PackageSidebar.vue (EXISTING - parent component)└── ...4. Empty State Handling
Decision: Display section with “No Funding Streams” message instead of hiding.
Rationale:
- Users understand feature exists but no budget plan yet
- Consistent sidebar layout across packages
- Clear signal action needed (create budget)
- Professional, non-error presentation
Styling Approach:
- Centered text with subtle color (muted gray)
- Same section container as funding display
- Uses existing empty state patterns from app
5. Badge Layout Strategy
Decision: Two-column horizontal grid layout with responsive wrapping.
Rationale:
- Matches design version B mockup exactly
- Compact sidebar footprint (2 badges per row)
- Responsive: wraps to 1 column on mobile
- Scales gracefully (no limit on stream count)
- Clean visual hierarchy
Tailwind Implementation:
- Section title: “This Quarter’s Funding”
- Grid container:
grid grid-cols-2 gap-3(2 columns, responsive gap) - Mobile breakpoint:
md:grid-cols-2 grid-cols-1(1 column below medium breakpoint) - Each badge:
[Code] $[Amount]format only - Tooltip on badge: Full stream name + quarter range
6. Tooltip Content Strategy
Decision: Hide detailed information in tooltips; badge shows code + amount only.
Rationale:
- Clean, minimal sidebar display
- Full context available without clutter
- Consistent with design mockup (code + amount visible, rest in tooltip)
- Improves sidebar readability
Tooltip Information:
- Full funding stream name (e.g., “Home support ongoing”)
- Quarter display (e.g., “Q3 2025: July - September 2025”)
- Additional context as needed
Data Model
Funding Summary DTO
New File: domain/Budget/Data/PackageFundingSummaryData.php
class PackageFundingSummaryData extends Data{ public function __construct( public ?string $currentQuarterDisplay, // "Q3 2025" for tooltip public Collection $fundingStreams, // Collection of FundingStreamData ) {}}Where It Lives:
- In
PackageViewData, add property:public ?PackageFundingSummaryData $fundingSummary - Populated during
PackageService::getTabData()call
Modified DTOs
PackageViewData (domain/Package/Data/PackageViewData.php):
- Add property:
public ?PackageFundingSummaryData $fundingSummary = null - Populated by new action:
GetPackageFundingSummary
No Changes Needed:
FundingStreamData- Already has all required fieldsBudgetPlanmodel - Already has quarter relationshipsQuartermodel - Already hasgetCurrent()method
API Contracts
No New Endpoints Required
Rationale: Funding data packaged with existing PackageViewData response.
Updated Response Structure
Route: All package view routes (e.g., GET /packages/{package})
Response Change:
{ "package": { ... }, "viewLayoutData": { "cash_balance": "...", "funding_summary": { "currentQuarterDisplay": "Q3 2025", "fundingStreams": [ { "externalId": "ON", "name": "Office of the National Coordinator", "colour": "#1f2937", "availableAmount": 15302.45, "totalFunded": 100000.00, "spent": 84697.55, ... }, { "externalId": "CM", "name": "Care Management", ... } ] } }, ...}Backward Compatibility
fundingSummaryoptional (can be null)- All existing response fields unchanged
- Safe to add without breaking clients
UI Components
Component Hierarchy
PackageLayout (parent)└── PackageSidebar ├── RecipientInformation (EXISTING) ├── FundingStreamBalanceSidebar (NEW) │ ├── Section Title │ ├── Empty State (conditional) OR │ └── FundingStreamBadges (collection) │ └── FundingStreamBadge (EXISTING - reused) │ ├── Badge Icon/Color │ ├── Stream Code (ON, CM) │ └── Amount ($15,302.45) └── ActionButtons (EXISTING)New Component: FundingStreamBalanceSidebar.vue
Location: resources/js/Components/Package/Sidebar/FundingStreamBalanceSidebar.vue
Props:
interface Props { fundingStreams?: FundingStreamData[] quarterDisplay?: string // e.g., "Q3 2025" for tooltip context}Layout Structure:
- Section title: “This Quarter’s Funding”
- If no funding streams: “No Funding Streams” empty state (centered)
- If funding streams: 2-column grid layout with badges
Grid Layout:
<div class="grid grid-cols-2 gap-3 md:grid-cols-2 md:gap-4"> <!-- Badges render here, 2 per row --></div>Features:
- Badge format:
[StreamCode] $[Amount]only - Tooltip on each badge showing:
- Full funding stream name
- Quarter range (e.g., “Q3 2025: July - September 2025”)
- Any additional context
- Accessible descriptions for screen readers
- Dark mode support (inherit from sidebar)
- Responsive: Wraps to 1 column on mobile, 2 columns on desktop
Integration with PackageSidebar.vue
File: resources/js/Components/Package/Sidebar/PackageSidebar.vue
Changes:
- Import new component:
import FundingStreamBalanceSidebar from './FundingStreamBalanceSidebar.vue' - Add to template after RecipientInformation section:
<FundingStreamBalanceSidebar :funding-streams="viewLayoutData.funding_summary?.fundingStreams" :quarter-display="viewLayoutData.funding_summary?.currentQuarterDisplay"/>- Conditional render: Only if
viewLayoutData.funding_summaryexists
Implementation Phases
Phase 0: Research & Preparation
Duration: 2-3 hours
- Review existing FundingStreamData structure and usage
- Examine BudgetPlanController quarter selection logic in detail
- Check PackageService::getTabData() flow and extension points
- Review FundingStreamBadge.vue component API and styling
- Determine optimal place to extract quarter logic (new service or inline)
- Verify no side effects from adding fundingSummary to PackageViewData
Deliverables:
- Design decision memo on quarter logic extraction
- DTO design finalized
- Component API specification
Phase 1: Backend Foundation
Duration: 4-6 hours
Tasks:
-
Create PackageFundingSummaryData DTO
- File:
domain/Budget/Data/PackageFundingSummaryData.php - Properties:
currentQuarterDisplay,fundingStreams(Collection) - Validation: Ensure fundingStreams is non-empty or null
- File:
-
Create GetPackageFundingSummary Action
- File:
domain/Budget/Actions/GetPackageFundingSummary.php - Input:
Packagemodel - Logic:
- Get latest BudgetPlan for package
- If no plan, return null
- Determine current quarter using Service Plan logic
- Extract FundingStreamData for that quarter
- Format quarter display string (e.g., “Q3 2025”)
- Output:
?PackageFundingSummaryData
- File:
-
Extract/Refactor Quarter Selection Logic (if needed)
- Option A: Create reusable
GetCurrentQuarterForBudgetPlanservice - Option B: Add method to BudgetPlan model
- Option C: Duplicate logic (acceptable if isolated)
- Selected: Option A (reusable service)
- Option A: Create reusable
-
Update PackageViewData DTO
- File:
domain/Package/Data/PackageViewData.php - Add property:
public ?PackageFundingSummaryData $fundingSummary = null;
- File:
-
Modify PackageService::getTabData()
- File:
domain/Package/Services/PackageService.php - Call
GetPackageFundingSummary::run($package) - Assign result to
packageViewData->fundingSummary - Handle null gracefully
- File:
Testing:
- Unit test:
GetPackageFundingSummarywith various package states - Feature test: Package view returns correct funding summary
- Edge cases: No budget plan, single stream, multiple streams, zero balances
Phase 2: Frontend Components
Duration: 3-4 hours
Tasks:
-
Create FundingStreamBalanceSidebar.vue
- File:
resources/js/Components/Package/Sidebar/FundingStreamBalanceSidebar.vue - Props:
fundingStreams,quarterDisplay - Template:
- Section heading: “Remaining balance by funding stream”
- Conditional render:
- If no streams: Empty state “No Funding Streams”
- If streams: Loop and render FundingStreamBadge for each
- Styling: Tailwind flex/gap, dark mode support
- Features:
- Tooltip with full stream name on hover
- Badge descriptions for accessibility
- Responsive: Stack all sizes, no breaks
- File:
-
Integrate into PackageSidebar.vue
- File:
resources/js/Components/Package/Sidebar/PackageSidebar.vue - Import new component
- Add to template with conditional render
- Position: Below RecipientInformation section
- Props: Map from
viewLayoutData.funding_summary
- File:
-
Styling & Theming
- Use existing FundingStreamBadge colors
- Apply Tailwind gap utilities (gap-3 or gap-4)
- Empty state: Center text, muted color (#6B7280 or similar)
- Support dark mode (check if sidebar has dark mode class)
Testing:
- Component tests: Props, conditional rendering, empty states
- Snapshot tests: Verify badge layout
- Visual tests: Dark mode, multiple streams, long names
- Accessibility tests: Screen reader descriptions, color contrast
Phase 3: Integration & Refinement
Duration: 3-4 hours
Tasks:
-
End-to-End Testing
- Navigate all package tabs, verify funding visible everywhere
- Modify budget in Service Plan, navigate back, verify update
- Test with zero balances, negative balances
- Test with long funding stream names (tooltip)
- Test on packages without budget plans
-
Performance Testing
- Measure page load time (sidebar should not add latency)
- Verify no N+1 queries
- Profile funding data retrieval
-
Code Formatting & Standards
- Run
vendor/bin/pint --dirtyfor PHP - Verify Prettier formatting for Vue
- Check ESLint rules
- Ensure no unused imports
- Run
-
Documentation
- Add inline comments to GetPackageFundingSummary action
- Document DTO structure
- Update any relevant README or docs
Testing:
- Full test suite:
php artisan test - Manual QA across all package types
- Browser testing: Chrome, Safari, Firefox
- Mobile testing: Responsive sidebar layout
Phase 4: Deployment Preparation
Duration: 1-2 hours
Tasks:
-
Final Review
- Code review: Check for edge cases, error handling
- Spec compliance: Verify all requirements met
- Performance: Confirm no regressions
-
Create Changeset Summary
- List all files modified
- Document breaking changes (none expected)
- Note any configuration changes needed (none)
-
Prepare Migration Plan (if any DB changes needed)
- Confirm: No migrations needed for this feature
- Data already exists in existing schema
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Quarter logic mismatch | Medium | High | Extract shared quarter service; mirror BudgetPlanController exactly; test all quarter edge cases |
| Stale data on concurrent tabs | Low | Medium | Document limitation; users expect re-render on tab change; can add focus event listener if needed |
| Performance regression | Low | Medium | Pre-fetch all data; no new queries; load test with 1000+ packages |
| Funding data calculation error | Low | High | Inherit existing FundingStreamData logic (proven); extensive test coverage; compare with Service Plan view |
| Missing null safety | Low | Medium | Explicit null checks in template; fundingSummary optional in DTO; null coalescing in Vue |
| Accessibility issues | Low | Medium | Tooltip text for screen readers; color + text in badge; test with accessible name checker |
| UI layout breaks | Low | Medium | Test on small screens; Tailwind flex utilities scale well; responsive badge sizing |
| Empty state appearance | Very Low | Low | Simple centered text; test styling matches other empty states in app |
Mitigation Strategy:
- Comprehensive test suite with edge case coverage
- Code review focusing on data accuracy
- Performance profiling before merge
- Manual QA on all 11 package tabs
Testing Strategy
Unit Tests
File: tests/Unit/Budget/GetPackageFundingSummaryTest.php
Test Cases:
- ✅ Package with no budget plan returns null
- ✅ Package with budget plan returns PackageFundingSummaryData
- ✅ Funding streams collection non-empty when plan exists
- ✅ Quarter display formatted correctly
- ✅ Single funding stream renders correctly
- ✅ Multiple funding streams all included
- ✅ Zero balance streams included
- ✅ Negative balance streams included
Feature Tests
File: tests/Feature/Package/PackageFundingSidebarTest.php
Test Cases:
- ✅ Overview tab shows funding summary
- ✅ Care Circle tab shows funding summary
- ✅ All 11 tabs show funding summary
- ✅ Funding matches Service Plan view exactly
- ✅ Empty state shows “No Funding Streams”
- ✅ Funding updates after budget modification
- ✅ Quarter parameter respected (?quarter=X)
- ✅ Access control: Users can view package funding if they can view package
Component Tests (Vue)
File: tests/Unit/Components/FundingStreamBalanceSidebarTest.ts (Vitest or similar)
Test Cases:
- ✅ Component renders when fundingStreams provided
- ✅ Empty state renders when no fundingStreams
- ✅ Badge rendered for each stream
- ✅ Tooltip contains full stream name
- ✅ Amount formatted correctly with currency
- ✅ Accessible descriptions present
- ✅ Dark mode class applied
- ✅ Responsive layout works on mobile
Manual QA Checklist
- Funding visible on Overview tab
- Funding visible on all 11 package tabs
- Empty state shows for package without budget
- Modify budget in Service Plan, check sidebar updates
- Test with 1-8+ funding streams
- Test long funding stream names (tooltip)
- Test zero and negative balances
- Test on mobile (responsive layout)
- Test in dark mode
- Test with screen reader (NVDA/JAWS on Windows, VoiceOver on Mac)
- Test currency formatting edge cases
Implementation Files & Changes
New Files
| File | Type | Lines | Purpose |
|---|---|---|---|
domain/Budget/Data/PackageFundingSummaryData.php | PHP DTO | ~20 | Data structure for funding summary |
domain/Budget/Actions/GetPackageFundingSummary.php | PHP Action | ~50 | Business logic to fetch funding |
resources/js/Components/Package/Sidebar/FundingStreamBalanceSidebar.vue | Vue | ~100 | Funding display component |
tests/Unit/Budget/GetPackageFundingSummaryTest.php | Pest Test | ~80 | Unit tests |
tests/Feature/Package/PackageFundingSidebarTest.php | Pest Test | ~120 | Feature tests |
Modified Files
| File | Changes | Impact |
|---|---|---|
domain/Package/Data/PackageViewData.php | Add fundingSummary property | Optional; backward compatible |
domain/Package/Services/PackageService.php | Call GetPackageFundingSummary in getTabData() | Low impact; data already available |
resources/js/Components/Package/Sidebar/PackageSidebar.vue | Import and render FundingStreamBalanceSidebar | Low impact; conditional render |
Total Code Impact
- New Code: ~350 lines (PHP + Vue)
- Modified Code: ~20 lines (imports + one method call)
- Test Code: ~200 lines
- No Breaking Changes
Dependencies & Prerequisites
Code Dependencies
- ✅
FundingStreamData- Already exists - ✅
FundingStreamBadge.vue- Already exists - ✅
BudgetPlanmodel with quarter methods - Already exists - ✅
Quarter::getCurrent()- Already exists - ✅
PackageSidebar.vue- Already exists - ✅ Inertia.js v2 - Already in use
External Dependencies
- None new required
- Tailwind CSS v3 (already used)
- Vue 3 (already used)
Knowledge Requirements
- Understanding of Laravel DTOs (Data Transfer Objects)
- Understanding of Action pattern (already used in codebase)
- Vue 3 template syntax
- Tailwind CSS utilities
Constitution Alignment
Status: ✅ Aligned with project principles
| Principle | Status | Notes |
|---|---|---|
| Follow Laravel conventions | ✅ | Uses Models, Services, Actions, DTOs per codebase patterns |
| Reuse existing components | ✅ | Extends PackageSidebar, reuses FundingStreamBadge, follows existing patterns |
| No new dependencies | ✅ | Uses only existing packages (Laravel, Vue, Tailwind) |
| Comprehensive testing | ✅ | Unit, feature, and component test coverage planned |
| Performance conscious | ✅ | No new queries; pre-fetched data; leverages Inertia caching |
| Accessible UX | ✅ | Tooltips, badge descriptions, color + text, dark mode support |
| Code quality | ✅ | Follows PSR-12, uses type hints, consistent naming |
Deployment Strategy
Pre-Deployment
- Run full test suite:
php artisan test - Code review: Check data accuracy, edge cases
- Performance test: Measure page load times
- Browser testing: Chrome, Safari, Firefox
Deployment Steps
- Merge to main/dev branch
- Run migrations (none needed)
- Clear cache:
php artisan cache:clear - Deploy to staging
- Smoke test: Verify funding appears on all package tabs
- Deploy to production
- Monitor: Check for errors, performance issues
Rollback Plan
- Simple revert: Feature is additive (new component in sidebar)
- No data changes: Can safely rollback without data loss
- Time to rollback: <5 minutes (git revert)
Next Steps
-
Code Review of Plan
- User approval of architecture and phasing
- Confirm technology choices and data model design
-
Generate Task List
- Run
/speckit.tasksto break plan into actionable tasks - Estimate story points per task
- Prioritize by dependency order
- Run
-
Design Mockups (if not already done)
- Run
/trilogy.mockupto visualize component layout - Confirm empty state styling
- Verify responsive design on mobile
- Run
-
Begin Implementation
- Start with Phase 0 (research)
- Then Phase 1 (backend) in parallel with Phase 2 (frontend)
- Phase 3 (integration) after components ready
- Phase 4 (deployment) after full testing
-
Sync to Jira (if needed)
- Run
/trilogy.jira-syncto link tasks to epic TP-2501 - Assign to team members
- Track progress
- Run
Success Criteria
✅ Implementation Success when:
- All unit and feature tests pass
- Funding visible on all 11 package tabs
- Sidebar data matches Service Plan view exactly
- No performance regression (page load time unchanged)
- Empty state renders beautifully for packages without budgets
- Tooltips show full funding stream names
- Responsive on mobile and desktop
- Accessible to screen readers
- Dark mode fully supported
- Code formatted and linted
- Full test coverage (>80% of new code)
- Zero breaking changes to existing APIs
References
- Specification: spec.md
- Quality Checklist: checklists/spec-quality.md
- Existing Components:
- FundingStreamBadge.vue:
resources/js/Components/ServicePlan/Funding/FundingStreamBadge.vue - PackageSidebar.vue:
resources/js/Components/Package/Sidebar/PackageSidebar.vue - BudgetPlanController:
domain/Budget/Http/Controllers/BudgetPlanController.php - Quarter model:
app/Models/Quarter.php
- FundingStreamBadge.vue: