Skip to content

[Core]: The user shall be able to produce a report with total V.A.T amount that is due by the company according to the sales of products made #73

Open

Description

@nielsdrost7

Context


The user shall be able to produce a report with total V.A.T amount that is due by the company according to the sales of products made


Specific:

As an accountant, I want to generate a VAT report that summarizes total VAT due from product sales, so I can accurately remit taxes.

Measurable:

The report will display the total VAT amount in the company's base currency, accurate to the nearest cent, for a specified date range.

Achievable:

The sales data and VAT calculations are already present in the database, allowing for report generation using existing data structures.

Relevant:

This report directly supports legal compliance by providing the necessary data for VAT remittance.

Time-bound:

The VAT report generation feature will be implemented and tested within the next two sprints (4 weeks).


Arrange:

// Create a user with the accountant role
$accountant = User::factory()->create(['role' => 'accountant']);

// Authenticate the user
$this->actingAs($accountant);

// Define the date range for the report
$startDate = now()->subMonth();
$endDate = now();

// Create some sales transactions with VAT
$product1 = Product::factory()->create(['price' => 100, 'vat_rate' => 0.20]);
$product2 = Product::factory()->create(['price' => 50, 'vat_rate' => 0.10]);

Sale::factory()->create(['product_id' => $product1->id, 'quantity' => 2, 'sale_date' => $startDate->addDays(5)]);
Sale::factory()->create(['product_id' => $product2->id, 'quantity' => 3, 'sale_date' => $endDate->subDays(5)]);

Act:

// Call the endpoint to generate the VAT report
$response = $this->get(route('vat.report', ['start_date' => $startDate->format('Y-m-d'), 'end_date' => $endDate->format('Y-m-d')]));

Assert:

// Assert that the response is successful
$response->assertStatus(200);

// Assert that the response returns a JSON structure
$response->assertJsonStructure(['total_vat']);

// Calculate the expected VAT amount
$expectedVat = (2 * 100 * 0.20) + (3 * 50 * 0.10);

// Assert that the total VAT amount in the response matches the expected value
$response->assertJson(['total_vat' => number_format($expectedVat, 2, '.', '')]);


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

    Rate Limit Exceeded

    @nielsdrost7 have exceeded the limit for the number of chat messages per hour. Please wait 0 minutes and 37 seconds before sending another message.

  4. nielsdrost7 commented on Jul 16, 2026

    @nielsdrost7
    CollaboratorAuthor

    SMART Plan — VAT due report

    Context/Epic: Core

    Specific

    Let a company generate a report of total VAT due, aggregated from actual invoice tax data, for a given period. Current state: the issue body's own Arrange/Act/Assert block references Sale, Product::vat_rate, and a vat.report route that do not exist anywhere in the codebase — it's boilerplate, not a real spec, and is disregarded here in favour of what actually exists. What does exist: Invoice has a real invoice_tax_total column (cast decimal:4, populated by InvoiceFactory/the invoice calculator on every invoice), and a taxRates(): BelongsToMany relation to Modules\Core\Models\TaxRate via the invoice_tax_rates pivot table (with tax_total on the pivot). Modules\Core\Services\InvoiceTaxRateService exists as a class but is currently an empty stub — there is no service that sums tax across invoices for a date range today.

    Measurable

    • A new VatReportService::generate($company, $from, $to) sums invoice_tax_total for the company's invoices within the given period.
      • PHPUnit: it_sums_invoice_tax_totals_within_the_given_period()
        #[Test]
        public function it_sums_invoice_tax_totals_within_the_given_period(): void
        {
            /* Arrange */
            Invoice::factory()->for($this->company)->create([
                'invoiced_at'       => now()->subDays(5)->toDateString(),
                'invoice_tax_total' => 42.5000,
            ]);
    
            /* Act */
            $report = app(VatReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(42.5, $report->totalVat);
        }
    • Invoices outside the requested date range (by invoiced_at) are excluded.
      • PHPUnit: it_excludes_invoices_outside_the_requested_period()
        #[Test]
        public function it_excludes_invoices_outside_the_requested_period(): void
        {
            /* Arrange */
            Invoice::factory()->for($this->company)->create([
                'invoiced_at'       => now()->subYear()->toDateString(),
                'invoice_tax_total' => 100.0,
            ]);
    
            /* Act */
            $report = app(VatReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(0.0, $report->totalVat);
        }
    • Another company's invoices are never included (tenant isolation, via BelongsToCompany's global scope).
      • PHPUnit: it_does_not_include_another_companys_invoice_tax()
        #[Test]
        public function it_does_not_include_another_companys_invoice_tax(): void
        {
            /* Arrange */
            $otherCompany = Company::factory()->create();
            Invoice::factory()->for($otherCompany)->create([
                'invoiced_at'       => now()->toDateString(),
                'invoice_tax_total' => 500.0,
            ]);
    
            /* Act */
            $report = app(VatReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(0.0, $report->totalVat);
        }
    • Soft-deleted invoices are excluded, since Invoice uses SoftDeletes.
      • PHPUnit: it_excludes_soft_deleted_invoices_from_the_vat_total()
        #[Test]
        public function it_excludes_soft_deleted_invoices_from_the_vat_total(): void
        {
            /* Arrange */
            $invoice = Invoice::factory()->for($this->company)->create([
                'invoiced_at'       => now()->toDateString(),
                'invoice_tax_total' => 30.0,
            ]);
            $invoice->delete();
    
            /* Act */
            $report = app(VatReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(0.0, $report->totalVat);
        }
    • A breakdown per TaxRate (via the invoice_tax_rates pivot's tax_total) is available, not just a single grand total.
      • PHPUnit: it_breaks_down_vat_by_tax_rate()
        #[Test]
        public function it_breaks_down_vat_by_tax_rate(): void
        {
            /* Arrange */
            $taxRate = TaxRate::factory()->for($this->company)->create(['percentage' => 21]);
            $invoice = Invoice::factory()->for($this->company)->create(['invoiced_at' => now()->toDateString()]);
            $invoice->taxRates()->attach($taxRate->id, ['tax_total' => 21.0]);
    
            /* Act */
            $report = app(VatReportService::class)->generate($this->company, now()->subMonth(), now());
    
            /* Assert */
            $this->assertSame(21.0, $report->byTaxRate[$taxRate->id] ?? null);
        }

    Achievable

    1. New Modules/Core/Services/VatReportService.php (or implement the currently-empty InvoiceTaxRateService — confirm which is the intended home before starting) with a generate(Company $company, Carbon $from, Carbon $to) method querying Invoice::query()->whereBetween('invoiced_at', [$from, $to]) scoped by the existing BelongsToCompany global scope, summing invoice_tax_total and grouping taxRates() pivot data by tax_rate_id.
    2. Return a simple value object (or array) with totalVat and byTaxRate, following the pattern used by the existing ReportDataSource/ReportFieldService reporting scaffolding in Modules/Core.
    3. Add a Filament page (admin or company panel — confirm which panel this targets) to select a date range and render the totals; there is no existing "VAT report" page to extend.
    4. Add PHPUnit coverage in Modules/Core/Tests/Feature/.
    5. Confirm with the reporter whether "VAT due" should also account for VAT reclaimable on expenses (input VAT) or only output VAT on sales, since the title says "according to the sales of products made" but real-world VAT remittance usually nets the two.

    Relevant

    VAT remittance is a legal obligation for most companies using this software; without an aggregate report, a company would have to manually total tax across every invoice for a filing period.

    Time-bound

    Sprint 6 (confirm output-only vs. net VAT scope before starting, per the Achievable note above).

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