Skip to content

[Core]: The user shall be able to produce a complete Income vs Expenses report for a given period of time #71

Open

Description

@nielsdrost7

Context


Per financial year. or every three or six months, according to the needs of the company


Specific:

The system should generate financial reports at predefined intervals.

Measurable:

Report generation frequency can be configured to occur either annually, bi-annually, or quarterly.

Achievable:

The existing reporting module can be extended with configuration options to support different reporting intervals.

Relevant:

Regular financial reports are essential for effective business performance monitoring and decision-making.

Time-bound:

This functionality will be implemented and deployed within the next sprint (2 weeks).


### Arrange:
// Create a configuration setting for report generation frequency.
// Define the possible values as 'annually', 'bi-annually', and 'quarterly'.
// Set the initial report generation frequency to 'annually'.
### Act:
// Trigger the report generation process.
// The system should determine the current time and compare it against the configured report generation frequency.
// If the current time aligns with the scheduled interval (e.g., end of the year for 'annually'), a report should be generated.
### Assert:
// Verify that a financial report file was created.
// Verify that the report file contains the expected data based on the report generation frequency and reporting period (e.g., data for the entire year if 'annually').
// Verify that the report generation timestamp is recorded.

Activity

  1. added this to the v2.0.0-alpha milestone on Dec 28, 2025
  2. nielsdrost7 commented on Jan 2, 2026

    @nielsdrost7
    CollaboratorAuthor

    @coderabbitai make pull-request with AAA test Scenario's: Arrange, Act, Assert. And also make sure you test edge cases

  3. coderabbitai commented on Jan 2, 2026

    @coderabbitai
    Contributor

    Coding Plan

    Approach

    Implement automated financial report generation that produces Income vs Expenses summaries at configurable intervals (annually, bi-annually, quarterly), with scheduled execution, file storage, and administrative management interface. The solution will create a new FinancialReports module following the existing Laravel modular architecture, leveraging established patterns for exports, background jobs, and Filament admin interfaces.

    Observations

    This Laravel application uses a modular architecture with business logic organized into Modules/ directories. The system has established patterns for export services (CSV/XLSX via Maatwebsite/Excel), background jobs (database queue), console commands, and Filament admin resources. Financial data comes from the Invoices, Payments, and Expenses modules. A ReportBuilder module exists but is designed for document templates (invoices/quotes) rather than aggregated financial summaries. The scheduler infrastructure is prepared but currently inactive across service providers.

    Assumptions
    Assumption 1: Income Calculation Method

    Options Considered:

    • Use invoice totals - represents billed amounts regardless of payment status
    • Use payment amounts - represents actual cash received during the period
    • Support both as separate line items in the report

    Chosen Option: 3

    Rationale: Include both invoice totals and payment amounts as separate metrics, giving users a complete picture of billed vs received income.

    Assumption 2: Report Scope

    Options Considered:

    • System-wide reports aggregating all companies
    • Per-company reports with company-scoped data
    • Configurable per schedule

    Chosen Option: 2

    Rationale: Per-company reports, aligning with the existing multi-tenant architecture where all financial entities are company-scoped.

    Assumption 3: Schedule Storage

    Options Considered:

    • Add frequency columns to existing report_templates table
    • Create dedicated report_schedules table
    • Use the global settings table with JSON configuration

    Chosen Option: 2

    Rationale: Dedicated report_schedules table provides clear separation between template design (ReportBuilder) and automated generation (FinancialReports), supports multiple active schedules per company, and follows the modular pattern.

    💡 User Tips

    Regenerate the plan with different choices with @coderabbitai <feedback>.

    Implementation Steps

    Phase 1: Module Foundation & Data Structure

    Establish the FinancialReports module with database schema for storing report schedules and generated report metadata.

    Task 1: Create FinancialReports Module Structure

    Create the module foundation with standard Laravel module directories following the existing modular pattern.

    • Generate module structure in Modules/FinancialReports/ with subdirectories: Models/, Services/, DTOs/, Enums/, Jobs/, Console/, Filament/, Database/Migrations/, Tests/
    • Create module service provider in Modules/FinancialReports/Providers/FinancialReportsServiceProvider.php to register routes, migrations, and translations
    • Register the service provider in config/modules.php
    Task 2: Create Database Schema for Report Schedules

    Define the schema for storing report generation frequency configuration per company.

    • Create migration in Modules/FinancialReports/Database/Migrations/ for financial_report_schedules table
    • Include columns: id, company_id (foreign key), frequency (enum: annually, bi-annually, quarterly), is_active (boolean), last_generated_at (timestamp), next_scheduled_at (timestamp), timestamps
    • Add unique constraint on company_id to enforce one active schedule per company
    • Add index on next_scheduled_at for efficient schedule checking
    Task 3: Create Database Schema for Generated Reports

    Define the schema for tracking generated report files and metadata.

    • Create migration in Modules/FinancialReports/Database/Migrations/ for financial_reports table
    • Include columns: id, company_id (foreign key), schedule_id (foreign key, nullable), report_type (enum: income_vs_expenses), period_start_date, period_end_date, file_path, file_format (enum: pdf, xlsx), generated_at (timestamp), timestamps
    • Add indexes on company_id and generated_at for efficient querying
    • Store file paths relative to the storage disk for portability
    Task 4: Create Models and Enums

    Implement Eloquent models and supporting enums following existing codebase patterns.

    • Create Modules/FinancialReports/Models/FinancialReportSchedule.php with relationships to Company and FinancialReports
    • Create Modules/FinancialReports/Models/FinancialReport.php with relationships to Company and FinancialReportSchedule
    • Create Modules/FinancialReports/Enums/ReportFrequency.php enum with cases: Annually, BiAnnually, Quarterly
    • Create Modules/FinancialReports/Enums/FinancialReportType.php enum with case: IncomeVsExpenses (extensible for future report types)
    • Add scope methods to FinancialReportSchedule for finding schedules due for generation
    🤖 Prompt for AI agents
    Set up the FinancialReports module foundation with database schema for report
    schedules and generated reports. Create the module structure, service provider,
    and database migrations.
    
    Create module structure:
    - Generate `Modules/FinancialReports/` directory with subdirectories: `Models/`,
    `Services/`, `DTOs/`, `Enums/`, `Jobs/`, `Console/`, `Filament/`,
    `Database/Migrations/`, `Tests/`
    - Create `FinancialReportsServiceProvider.php` in `Providers/` subdirectory to
    register migrations and translations
    - Register provider in `config/modules.php`
    
    Create database migrations:
    - `financial_report_schedules` table with: `id`, `company_id` (FK), `frequency`
    (enum), `is_active` (boolean), `last_generated_at`, `next_scheduled_at`,
    timestamps
    - Add unique constraint on `company_id` and index on `next_scheduled_at`
    - `financial_reports` table with: `id`, `company_id` (FK), `schedule_id` (FK
    nullable), `report_type` (enum), `period_start_date`, `period_end_date`,
    `file_path`, `file_format` (enum), `generated_at`, timestamps
    - Add indexes on `company_id` and `generated_at`
    
    Create models and enums:
    - `FinancialReportSchedule` model with Company and FinancialReport
    relationships, plus scope for due schedules
    - `FinancialReport` model with Company and FinancialReportSchedule relationships
    - `ReportFrequency` enum with Annually, BiAnnually, Quarterly cases
    - `FinancialReportType` enum with IncomeVsExpenses case
    

    Phase 2: Financial Data Aggregation Services

    Build services to query and aggregate income and expense data from existing modules for specified date ranges.

    Task 1: Create Financial Data Aggregation Service

    Implement service to aggregate income and expense data from Invoices, Payments, and Expenses modules.

    • Create Modules/FinancialReports/Services/FinancialDataAggregator.php
    • Implement method to calculate total invoiced amount for period by querying Invoices module (filter by invoiced_at date and company_id, sum invoice_total)
    • Implement method to calculate total payments received for period by querying Payments module (filter by paid_at date and company_id, sum payment_amount)
    • Implement method to calculate total expenses for period by querying Expenses module (filter by expensed_at date and company_id, sum expense_amount)
    • Group expenses by category and return category breakdowns
    • Handle edge cases: empty results, null dates, company without data
    Task 2: Create Date Range Calculator

    Implement service to calculate period start/end dates based on report frequency.

    • Create Modules/FinancialReports/Services/ReportPeriodCalculator.php
    • Implement methods to calculate period dates for annually (fiscal year), bi-annually (6 months), quarterly (3 months)
    • Use company's fiscal year settings if available, otherwise default to calendar year
    • Return period start and end dates as Carbon instances
    • Handle edge cases: leap years, partial periods for new companies
    Task 3: Create Financial Summary DTO

    Define data transfer object to structure aggregated financial data.

    • Create Modules/FinancialReports/DTOs/FinancialSummaryDTO.php
    • Include properties: period_start, period_end, total_invoiced, total_payments_received, total_expenses, expense_breakdown (array of category => amount), net_income (calculated)
    • Implement calculation method for net_income (total_payments_received - total_expenses)
    • Add formatting methods for currency display using the FormatsCurrency trait pattern
    • Include metadata: company_id, generated_at, currency_code
    🤖 Prompt for AI agents
    Build services to aggregate financial data from Invoices, Payments, and Expenses
    modules for specified date ranges. Create period calculator and data transfer
    object.
    
    Create FinancialDataAggregator service:
    - Query Invoices by `invoiced_at` date and `company_id`, sum `invoice_total`
    - Query Payments by `paid_at` date and `company_id`, sum `payment_amount`
    - Query Expenses by `expensed_at` date and `company_id`, sum `expense_amount`
    - Group expenses by category for breakdown
    - Handle empty results and null dates
    
    Create ReportPeriodCalculator service:
    - Calculate period dates for annually (fiscal year), bi-annually (6 months),
    quarterly (3 months)
    - Use company fiscal year settings or default to calendar year
    - Return Carbon instances for start/end dates
    - Handle leap years and partial periods
    
    Create FinancialSummaryDTO:
    - Properties: `period_start`, `period_end`, `total_invoiced`,
    `total_payments_received`, `total_expenses`, `expense_breakdown`, `net_income`
    - Calculate net income as total_payments_received minus total_expenses
    - Add currency formatting methods using FormatsCurrency trait
    - Include metadata: `company_id`, `generated_at`, `currency_code`
    

    Phase 3: Report File Generation

    Create services to generate report files in PDF and Excel formats, storing them in the filesystem.

    Task 1: Create Report File Generator Service

    Implement service to transform financial summary data into downloadable files.

    • Create Modules/FinancialReports/Services/FinancialReportGenerator.php
    • Follow export service patterns from Modules/Invoices/Services/InvoiceExportService.php
    • Implement generatePDF() method that creates formatted PDF using existing libraries (consider using Laravel DomPDF or similar)
    • Implement generateExcel() method that creates XLSX file using Maatwebsite/Excel with summary sheet and expense breakdown sheet
    • Store generated files in storage/app/financial_reports/ using Storage facade
    • Return file path for database storage
    • Include proper error handling and logging for file generation failures
    Task 2: Create Excel Export Class

    Build Excel export class for structured financial data following Maatwebsite/Excel patterns.

    • Create Modules/FinancialReports/Exports/FinancialSummaryExport.php implementing FromCollection and WithHeadings interfaces
    • Structure export with summary section (period, income, expenses, net) and detailed expense breakdown by category
    • Follow formatting patterns from existing export classes in other modules
    • Include currency formatting and column width configuration
    • Add chart/visualization if feasible within Excel export capabilities
    Task 3: Create Background Job for Report Generation

    Implement queueable job for asynchronous report generation following existing job patterns.

    • Create Modules/FinancialReports/Jobs/GenerateFinancialReportJob.php implementing ShouldQueue
    • Accept FinancialReportSchedule model and optional format parameter in constructor
    • In handle() method: calculate period dates, aggregate data via FinancialDataAggregator, generate file via FinancialReportGenerator, create FinancialReport record
    • Update schedule's last_generated_at and next_scheduled_at timestamps
    • Implement retry logic with exponential backoff following Modules/Invoices/Jobs/Peppol/SendInvoiceToPeppolJob.php pattern
    • Add proper error handling and logging for failed generations
    🤖 Prompt for AI agents
    Create services to generate PDF and Excel reports from financial data and store
    them in the filesystem. Build background job for asynchronous generation.
    
    Create FinancialReportGenerator service:
    - Follow patterns from `InvoiceExportService.php`
    - Implement `generatePDF()` using DomPDF or similar library
    - Implement `generateExcel()` using Maatwebsite/Excel with summary and breakdown
    sheets
    - Store files in `storage/app/financial_reports/` using Storage facade
    - Return file path for database storage
    - Add error handling and logging
    
    Create FinancialSummaryExport class:
    - Implement FromCollection and WithHeadings interfaces
    - Structure with summary section (period, income, expenses, net) and expense
    breakdown by category
    - Follow existing export formatting patterns
    - Add currency formatting and column widths
    
    Create GenerateFinancialReportJob:
    - Implement ShouldQueue interface
    - Accept FinancialReportSchedule and optional format parameter
    - In `handle()`: calculate dates, aggregate data, generate file, create
    FinancialReport record
    - Update schedule timestamps: `last_generated_at` and `next_scheduled_at`
    - Implement retry logic with exponential backoff following existing job patterns
    - Add error handling and logging
    

    Phase 4: Scheduling & Automation

    Implement console command and activate Laravel scheduler for automated report generation.

    Task 1: Create Report Generation Console Command

    Build console command to check schedules and trigger report generation.

    • Create Modules/FinancialReports/Console/Commands/GenerateScheduledReportsCommand.php
    • Implement signature 'financial-reports:generate-scheduled' with optional --force flag
    • Query FinancialReportSchedule model for schedules where next_scheduled_at <= now() and is_active = true
    • Dispatch GenerateFinancialReportJob for each due schedule
    • Include dry-run option to preview what would be generated without executing
    • Add verbose output for logging which schedules are being processed
    Task 2: Activate Laravel Task Scheduler

    Enable the Laravel scheduler in the service provider to run the command automatically.

    • Uncomment and configure the scheduler callback in Modules/FinancialReports/Providers/FinancialReportsServiceProvider.php
    • Schedule the GenerateScheduledReportsCommand to run daily at a configured time (e.g., 2 AM)
    • Follow the commented-out scheduler pattern found in existing service providers
    • Add scheduler registration to the boot() method of the provider
    • Document that the Laravel scheduler cron job must be configured on the server (* * * * * php artisan schedule:run)
    Task 3: Implement Schedule Calculation Logic

    Add methods to calculate next scheduled run date based on frequency.

    • Add calculateNextScheduledDate() method to FinancialReportSchedule model or create dedicated service
    • For Annually: add 1 year to last_generated_at (or use fiscal year end date)
    • For BiAnnually: add 6 months to last_generated_at
    • For Quarterly: add 3 months to last_generated_at
    • Handle initial scheduling when last_generated_at is null (set to next period end based on current date)
    • Consider timezone handling based on company settings or default to application timezone
    🤖 Prompt for AI agents
    Implement console command for scheduled report generation and activate Laravel
    scheduler. Add schedule calculation logic.
    
    Create GenerateScheduledReportsCommand:
    - Signature: `financial-reports:generate-scheduled` with optional `--force` flag
    - Query FinancialReportSchedule where `next_scheduled_at <= now()` and
    `is_active = true`
    - Dispatch GenerateFinancialReportJob for each due schedule
    - Add dry-run option to preview without executing
    - Include verbose output for logging
    
    Activate Laravel scheduler:
    - Configure scheduler callback in `FinancialReportsServiceProvider.php`
    - Schedule command to run daily at 2 AM
    - Follow existing scheduler patterns from other service providers
    - Register in provider's `boot()` method
    - Document cron job requirement: `* * * * * php artisan schedule:run`
    
    Implement schedule calculation:
    - Add `calculateNextScheduledDate()` method to FinancialReportSchedule model
    - Annually: add 1 year to `last_generated_at` or use fiscal year end
    - BiAnnually: add 6 months
    - Quarterly: add 3 months
    - Handle null `last_generated_at` by setting to next period end from current
    date
    - Use company timezone or application default
    

    Phase 5: Administrative Interface

    Create Filament resources for managing report schedules and viewing/downloading generated reports.

    Task 1: Create Report Schedule Management Resource

    Build Filament resource for configuring report generation schedules.

    • Create Modules/FinancialReports/Filament/Admin/Resources/FinancialReportScheduleResource.php
    • Implement form with fields: frequency (select), is_active (toggle), next_scheduled_at (read-only display)
    • Restrict to one schedule per company using validation rules
    • Add table displaying existing schedules with columns: frequency, status, last generated, next scheduled
    • Include custom action to manually trigger report generation immediately
    • Set navigation group to 'Reports' to group with existing ReportBuilder resources
    Task 2: Create Generated Reports Management Resource

    Build Filament resource for viewing and downloading generated reports.

    • Create Modules/FinancialReports/Filament/Admin/Resources/FinancialReportResource.php
    • Implement table displaying reports with columns: report type, period, format, generated date
    • Add download action following Filament's downloadable action pattern using Storage::download()
    • Add bulk delete action for old reports
    • Add filter by period (date range) and format
    • Include summary card showing total reports generated and storage usage
    • Set navigation group to 'Reports'
    Task 3: Add Manual Report Generation Action

    Create action to generate reports on-demand from the admin interface.

    • In FinancialReportScheduleResource, add custom "Generate Now" action in table or view page
    • Action should open modal to select report format (PDF/Excel) and optionally override date range
    • Dispatch GenerateFinancialReportJob with custom parameters
    • Show success notification when job is dispatched
    • Follow Filament action patterns from existing resources
    🤖 Prompt for AI agents
    Create Filament admin resources for managing report schedules and viewing
    generated reports. Add manual generation action.
    
    Create FinancialReportScheduleResource:
    - Form fields: frequency (select), is_active (toggle), next_scheduled_at
    (read-only)
    - Validation to restrict one schedule per company
    - Table columns: frequency, status, last generated, next scheduled
    - Custom action to manually trigger report generation
    - Set navigation group to 'Reports'
    
    Create FinancialReportResource:
    - Table columns: report type, period, format, generated date
    - Download action using `Storage::download()`
    - Bulk delete action for old reports
    - Filters by period (date range) and format
    - Summary card showing total reports and storage usage
    - Set navigation group to 'Reports'
    
    Add manual generation action:
    - "Generate Now" action in FinancialReportScheduleResource table/view page
    - Modal to select format (PDF/Excel) and optional date range override
    - Dispatch GenerateFinancialReportJob with custom parameters
    - Success notification on dispatch
    - Follow Filament action patterns
    

    Phase 6: Testing

    Implement comprehensive PHPUnit tests covering data aggregation, report generation, scheduling logic, and edge cases.

    Task 1: Unit Tests for Data Aggregation

    Test financial data calculation accuracy and edge cases.

    • Create Modules/FinancialReports/Tests/Unit/Services/FinancialDataAggregatorTest.php
    • Test income calculation from invoices (Arrange: create invoices with various totals, Act: aggregate for period, Assert: correct sum)
    • Test payment aggregation (Arrange: create payments across date range, Act: aggregate, Assert: correct sum)
    • Test expense aggregation with category breakdown (Arrange: create expenses in multiple categories, Act: aggregate, Assert: correct totals and breakdown)
    • Test edge case: no data in period (Assert: zero totals returned)
    • Test edge case: date boundary conditions (data exactly on period start/end dates)
    • Test filtering by company_id (Assert: only company's data included)
    • Follow AAA pattern with #[Test] and #[Group('unit')] attributes
    Task 2: Unit Tests for Period Calculation

    Test date range calculations for different frequencies.

    • Create Modules/FinancialReports/Tests/Unit/Services/ReportPeriodCalculatorTest.php
    • Test annual period calculation (Assert: correct start/end dates for fiscal year)
    • Test bi-annual period calculation (Assert: correct 6-month periods)
    • Test quarterly period calculation (Assert: correct 3-month periods)
    • Test edge case: leap year handling
    • Test edge case: fiscal year vs calendar year differences
    • Test next_scheduled_date calculation for each frequency
    • Use Carbon::setTestNow() to control time in tests
    Task 3: Feature Tests for Report Generation

    Test end-to-end report generation workflow.

    • Create Modules/FinancialReports/Tests/Feature/GenerateFinancialReportTest.php
    • Test complete report generation (Arrange: create schedule, income, expenses; Act: dispatch job; Assert: report file created, database record exists, data correct)
    • Test PDF generation format (Assert: file exists at correct path, file is valid PDF)
    • Test Excel generation format (Assert: file exists, contains expected sheets and data)
    • Test schedule timestamp updates (Assert: last_generated_at and next_scheduled_at updated correctly)
    • Test job retry on failure (Arrange: mock service to fail, Act: dispatch job, Assert: job retries)
    • Use feature test base class and database transactions
    Task 4: Feature Tests for Scheduled Execution

    Test the console command and scheduling logic.

    • Create Modules/FinancialReports/Tests/Feature/ScheduledReportGenerationTest.php
    • Test command execution (Arrange: create due schedule, Act: run command, Assert: job dispatched)
    • Test that only due schedules are processed (Arrange: mix of due and future schedules, Act: run command, Assert: only due ones processed)
    • Test inactive schedules are skipped (Arrange: create inactive schedule, Act: run command, Assert: not processed)
    • Test edge case: multiple schedules for different companies execute correctly
    • Test dry-run mode (Assert: no jobs dispatched, but output shows what would run)
    • Use Artisan::call() to execute command in tests
    Task 5: Edge Case and Integration Tests

    Test boundary conditions and error scenarios.

    • Create Modules/FinancialReports/Tests/Feature/FinancialReportEdgeCasesTest.php
    • Test report generation for brand new company with no financial data (Assert: valid report with zero totals)
    • Test handling of missing or null date values in source data
    • Test currency handling with mixed currencies (if supported) or single currency enforcement
    • Test storage disk failures (Assert: proper error handling and logging)
    • Test concurrent job execution (Assert: no race conditions in schedule updates)
    • Test timezone edge cases (Assert: correct period calculation across timezone boundaries)
    • Test database transaction rollback on partial failures
    🤖 Prompt for AI agents
    Implement comprehensive PHPUnit tests for data aggregation, report generation,
    scheduling, and edge cases.
    
    Create FinancialDataAggregatorTest:
    - Test invoice income calculation with various totals
    - Test payment aggregation across date ranges
    - Test expense aggregation with category breakdowns
    - Test edge cases: no data, date boundaries, company filtering
    - Use AAA pattern with #[Test] and #[Group('unit')] attributes
    
    Create ReportPeriodCalculatorTest:
    - Test annual, bi-annual, and quarterly period calculations
    - Test leap year handling
    - Test fiscal vs calendar year differences
    - Test next_scheduled_date calculation
    - Use `Carbon::setTestNow()` for time control
    
    Create GenerateFinancialReportTest:
    - Test complete report generation workflow
    - Test PDF and Excel format generation
    - Test schedule timestamp updates
    - Test job retry on failure
    - Use feature test base and database transactions
    
    Create ScheduledReportGenerationTest:
    - Test command execution with due schedules
    - Test filtering: only due and active schedules processed
    - Test multiple companies
    - Test dry-run mode
    - Use `Artisan::call()` to execute command
    
    Create FinancialReportEdgeCasesTest:
    - Test new company with no data
    - Test null date handling
    - Test currency handling
    - Test storage failures
    - Test concurrent execution and race conditions
    - Test timezone edge cases
    - Test transaction rollbacks
    

    🚀 Next Steps

    🤖 All AI agent prompts combined
    Task: 1
    
    Set up the FinancialReports module foundation with database schema for report
    schedules and generated reports. Create the module structure, service provider,
    and database migrations.
    
    Create module structure:
    - Generate `Modules/FinancialReports/` directory with subdirectories: `Models/`,
    `Services/`, `DTOs/`, `Enums/`, `Jobs/`, `Console/`, `Filament/`,
    `Database/Migrations/`, `Tests/`
    - Create `FinancialReportsServiceProvider.php` in `Providers/` subdirectory to
    register migrations and translations
    - Register provider in `config/modules.php`
    
    Create database migrations:
    - `financial_report_schedules` table with: `id`, `company_id` (FK), `frequency`
    (enum), `is_active` (boolean), `last_generated_at`, `next_scheduled_at`,
    timestamps
    - Add unique constraint on `company_id` and index on `next_scheduled_at`
    - `financial_reports` table with: `id`, `company_id` (FK), `schedule_id` (FK
    nullable), `report_type` (enum), `period_start_date`, `period_end_date`,
    `file_path`, `file_format` (enum), `generated_at`, timestamps
    - Add indexes on `company_id` and `generated_at`
    
    Create models and enums:
    - `FinancialReportSchedule` model with Company and FinancialReport
    relationships, plus scope for due schedules
    - `FinancialReport` model with Company and FinancialReportSchedule relationships
    - `ReportFrequency` enum with Annually, BiAnnually, Quarterly cases
    - `FinancialReportType` enum with IncomeVsExpenses case
    ===============================================================================
    
    Task: 2
    
    Build services to aggregate financial data from Invoices, Payments, and Expenses
    modules for specified date ranges. Create period calculator and data transfer
    object.
    
    Create FinancialDataAggregator service:
    - Query Invoices by `invoiced_at` date and `company_id`, sum `invoice_total`
    - Query Payments by `paid_at` date and `company_id`, sum `payment_amount`
    - Query Expenses by `expensed_at` date and `company_id`, sum `expense_amount`
    - Group expenses by category for breakdown
    - Handle empty results and null dates
    
    Create ReportPeriodCalculator service:
    - Calculate period dates for annually (fiscal year), bi-annually (6 months),
    quarterly (3 months)
    - Use company fiscal year settings or default to calendar year
    - Return Carbon instances for start/end dates
    - Handle leap years and partial periods
    
    Create FinancialSummaryDTO:
    - Properties: `period_start`, `period_end`, `total_invoiced`,
    `total_payments_received`, `total_expenses`, `expense_breakdown`, `net_income`
    - Calculate net income as total_payments_received minus total_expenses
    - Add currency formatting methods using FormatsCurrency trait
    - Include metadata: `company_id`, `generated_at`, `currency_code`
    ===============================================================================
    
    Task: 3
    
    Create services to generate PDF and Excel reports from financial data and store
    them in the filesystem. Build background job for asynchronous generation.
    
    Create FinancialReportGenerator service:
    - Follow patterns from `InvoiceExportService.php`
    - Implement `generatePDF()` using DomPDF or similar library
    - Implement `generateExcel()` using Maatwebsite/Excel with summary and breakdown
    sheets
    - Store files in `storage/app/financial_reports/` using Storage facade
    - Return file path for database storage
    - Add error handling and logging
    
    Create FinancialSummaryExport class:
    - Implement FromCollection and WithHeadings interfaces
    - Structure with summary section (period, income, expenses, net) and expense
    breakdown by category
    - Follow existing export formatting patterns
    - Add currency formatting and column widths
    
    Create GenerateFinancialReportJob:
    - Implement ShouldQueue interface
    - Accept FinancialReportSchedule and optional format parameter
    - In `handle()`: calculate dates, aggregate data, generate file, create
    FinancialReport record
    - Update schedule timestamps: `last_generated_at` and `next_scheduled_at`
    - Implement retry logic with exponential backoff following existing job patterns
    - Add error handling and logging
    ===============================================================================
    
    Task: 4
    
    Implement console command for scheduled report generation and activate Laravel
    scheduler. Add schedule calculation logic.
    
    Create GenerateScheduledReportsCommand:
    - Signature: `financial-reports:generate-scheduled` with optional `--force` flag
    - Query FinancialReportSchedule where `next_scheduled_at <= now()` and
    `is_active = true`
    - Dispatch GenerateFinancialReportJob for each due schedule
    - Add dry-run option to preview without executing
    - Include verbose output for logging
    
    Activate Laravel scheduler:
    - Configure scheduler callback in `FinancialReportsServiceProvider.php`
    - Schedule command to run daily at 2 AM
    - Follow existing scheduler patterns from other service providers
    - Register in provider's `boot()` method
    - Document cron job requirement: `* * * * * php artisan schedule:run`
    
    Implement schedule calculation:
    - Add `calculateNextScheduledDate()` method to FinancialReportSchedule model
    - Annually: add 1 year to `last_generated_at` or use fiscal year end
    - BiAnnually: add 6 months
    - Quarterly: add 3 months
    - Handle null `last_generated_at` by setting to next period end from current
    date
    - Use company timezone or application default
    ===============================================================================
    
    Task: 5
    
    Create Filament admin resources for managing report schedules and viewing
    generated reports. Add manual generation action.
    
    Create FinancialReportScheduleResource:
    - Form fields: frequency (select), is_active (toggle), next_scheduled_at
    (read-only)
    - Validation to restrict one schedule per company
    - Table columns: frequency, status, last generated, next scheduled
    - Custom action to manually trigger report generation
    - Set navigation group to 'Reports'
    
    Create FinancialReportResource:
    - Table columns: report type, period, format, generated date
    - Download action using `Storage::download()`
    - Bulk delete action for old reports
    - Filters by period (date range) and format
    - Summary card showing total reports and storage usage
    - Set navigation group to 'Reports'
    
    Add manual generation action:
    - "Generate Now" action in FinancialReportScheduleResource table/view page
    - Modal to select format (PDF/Excel) and optional date range override
    - Dispatch GenerateFinancialReportJob with custom parameters
    - Success notification on dispatch
    - Follow Filament action patterns
    ===============================================================================
    
    Task: 6
    
    Implement comprehensive PHPUnit tests for data aggregation, report generation,
    scheduling, and edge cases.
    
    Create FinancialDataAggregatorTest:
    - Test invoice income calculation with various totals
    - Test payment aggregation across date ranges
    - Test expense aggregation with category breakdowns
    - Test edge cases: no data, date boundaries, company filtering
    - Use AAA pattern with #[Test] and #[Group('unit')] attributes
    
    Create ReportPeriodCalculatorTest:
    - Test annual, bi-annual, and quarterly period calculations
    - Test leap year handling
    - Test fiscal vs calendar year differences
    - Test next_scheduled_date calculation
    - Use `Carbon::setTestNow()` for time control
    
    Create GenerateFinancialReportTest:
    - Test complete report generation workflow
    - Test PDF and Excel format generation
    - Test schedule timestamp updates
    - Test job retry on failure
    - Use feature test base and database transactions
    
    Create ScheduledReportGenerationTest:
    - Test command execution with due schedules
    - Test filtering: only due and active schedules processed
    - Test multiple companies
    - Test dry-run mode
    - Use `Artisan::call()` to execute command
    
    Create FinancialReportEdgeCasesTest:
    - Test new company with no data
    - Test null date handling
    - Test currency handling
    - Test storage failures
    - Test concurrent execution and race conditions
    - Test timezone edge cases
    - Test transaction rollbacks
    
    💡 Iterate on the plan with: @coderabbitai <feedback>
    Example Feedback
    - @coderabbitai You can skip phase 3. Add a simple unit test case for phase 2.
    - @coderabbitai For assumption 1 go ahead with option 3 and replan.
    

    💬 Have feedback or questions? Drop into our discord!

  4. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    SMART Plan — Periodic Income vs. Expenses report

    Context/Epic: Core (Reporting)

    Specific

    Let a company generate a financial summary report (total income from invoices/payments vs. total expenses) for a configurable period — annually, bi-annually, or quarterly. Current state: Modules\Core\Enums\ReportDataSource already lists EXPENSE and INVOICE as report data sources, and ReportFieldService exposes their available fields (getFieldsBySource()), but this only feeds a generic report-template designer page. No aggregation service and no scheduling/period configuration for an Income vs. Expenses summary exists yet — this needs new code, not just wiring into the designer (the designer's ReportTemplateService class referenced by its Blade view doesn't currently exist in the codebase either, so it isn't a safe integration point yet).

    Measurable

    • A company can set a report interval (annually, bi-annually, quarterly) in company settings.
      • PHPUnit: it_saves_the_configured_income_expense_report_interval()
        #[Test]
        public function it_saves_the_configured_income_expense_report_interval(): void
        {
            /* Arrange */
            $this->company->update(['income_expense_report_interval' => null]);
    
            /* Act */
            Livewire::actingAs($this->user)
                ->test(Settings::class, ['tenant' => Str::lower($this->company->search_code)])
                ->fillForm(['income_expense_report_interval' => 'quarterly'])
                ->call('save')
                ->assertHasNoFormErrors();
    
            /* Assert */
            $this->assertSame('quarterly', $this->company->fresh()->income_expense_report_interval);
        }
    • IncomeExpenseReportService::generate($company, $from, $to) sums invoice/payment income and expense totals within the given period, scoped to the company.
      • PHPUnit: it_sums_income_and_expenses_within_the_given_period()
        #[Test]
        public function it_sums_income_and_expenses_within_the_given_period(): void
        {
            /* Arrange */
            Payment::factory()->for($this->company)->create(['amount' => 500, 'paid_at' => now()->subDays(5)]);
            Expense::factory()->for($this->company)->create(['amount' => 200, 'expensed_at' => now()->subDays(3)]);
    
            /* Act */
            $report = app(IncomeExpenseReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(500.0, $report->income);
            $this->assertSame(200.0, $report->expenses);
        }
    • Data outside the requested period is excluded.
      • PHPUnit: it_excludes_transactions_outside_the_requested_period()
        #[Test]
        public function it_excludes_transactions_outside_the_requested_period(): void
        {
            /* Arrange */
            Payment::factory()->for($this->company)->create(['amount' => 500, 'paid_at' => now()->subYear()]);
    
            /* Act */
            $report = app(IncomeExpenseReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(0.0, $report->income);
        }
    • Data from another company is never included (tenant isolation).
      • PHPUnit: it_does_not_include_another_companys_income_or_expenses()
        #[Test]
        public function it_does_not_include_another_companys_income_or_expenses(): void
        {
            /* Arrange */
            $otherCompany = Company::factory()->create();
            Payment::factory()->for($otherCompany)->create(['amount' => 1000, 'paid_at' => now()]);
    
            /* Act */
            $report = app(IncomeExpenseReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(0.0, $report->income);
        }
    • A report can be triggered on demand from the UI and downloaded/rendered without waiting for the schedule.
      • Frontend/Livewire: As a company admin I want to generate the Income vs Expenses report on demand so that I don't have to wait for the next scheduled interval.
        #[Test]
        public function user_can_generate_the_report_on_demand(): void
        {
            /* Arrange */
            Payment::factory()->for($this->company)->create(['amount' => 500, 'paid_at' => now()]);
    
            /* Act */
            $component = Livewire::actingAs($this->user)
                ->test(IncomeExpenseReportPage::class, ['tenant' => Str::lower($this->company->search_code)])
                ->call('generate', ['from' => now()->subMonth()->toDateString(), 'to' => now()->toDateString()]);
    
            /* Assert */
            $component->assertSuccessful()->assertSet('income', 500.0);
        }

    Achievable

    1. New IncomeExpenseReportService in Modules/Core/Services/ with a generate(Company $company, Carbon $from, Carbon $to): IncomeExpenseReportData method, querying Payment (income) and Expense (expenses), both already scoped via BelongsToCompany.
    2. Add income_expense_report_interval to company settings (migration + Filament settings form field), reusing the existing Settings page pattern.
    3. Build a dedicated Filament page (not the incomplete report-template designer) that calls the service on demand and renders totals; this is a new page rather than the broken ReportTemplateService integration.
    4. If scheduled generation is required (not just on-demand), add a console command + scheduler entry that runs per the configured interval and stores/exports the result (format — email, stored PDF — to be confirmed with the reporter).
    5. Add PHPUnit coverage in Modules/Core/Tests/Feature/.

    Relevant

    Company admins need periodic visibility into profitability without manually cross-referencing invoices, payments, and expenses — this is core financial reporting, not a nice-to-have.

    Time-bound

    Sprint 7 (report interval/output format to be confirmed with the original reporter before implementation starts).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    refinedRefined with SMART plan / test suggestions

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions