Files
crewli/api/tests/Feature/Artist/BumaVatCalculationTest.php
bert.hausmans 996dedc11d test(timetable): Phase C — 57 new tests covering session 2 surface
Nine test files under tests/Feature/Artist/ exercising:

  ArtistEngagementStateMachineTest    8 tests — terminal blocks, conditional
                                       gates (Option/Contracted), full happy
                                       path, cancel cascade
  LaneCascadeServiceTest              5 tests — simple move, cascade-bump,
                                       version mismatch, park, unpark
  BumaVatCalculationTest              6 tests — D26 formula coverage:
                                       Organisation/BookingAgency/NotApplicable,
                                       VAT off, breakdown sum, zero fee
  DemoteExpiredOptionsTest            4 tests — expired demote, future
                                       untouched, non-Option untouched, run
                                       twice → single option_expired entry
  IdempotencyKey60sRedisTest          4 tests — missing header 400, first
                                       cache, replay header, failed not cached
  ArtistControllerTest                8 tests — index/create/destroy + cross-
                                       tenant + duplicate detection + restore
  StageControllerTest                 7 tests — create + uniqueness, destroy
                                       cascade-park, reorder permutation,
                                       replaceDays orphan 409 + force_orphan
  ArtistEngagementControllerTest      5 tests — index/create/update/destroy +
                                       422 on invalid status transition
  TimetableMoveControllerTest         3 tests — happy path with idempotency
                                       header, missing header → 400, version
                                       mismatch → 409
  ArtistPolicyTest                    6 tests — role checks, cross-tenant
                                       denial, super_admin bypass, D27 active-
                                       engagement gate
  ActivityLogShapeTest                4 tests — performance.moved cascade
                                       props, status_changed vs cancelled,
                                       stage.day_added subject + props,
                                       stage.reordered on Event subject

Bug fixes surfaced by Phase C:

  Schema reality: events table uses `start_date`/`end_date` (date), not
  `start_at`/`end_at`. Updated WithinEventBounds rule and the two stage_day
  resolvers (LaneCascadeService + MoveTimetablePerformanceRequest) to
  query the actual columns. ArtistResource.engagements_summary upcoming
  filter likewise.

  performances table has no organisation_id column (FK-chain via
  engagement_id). Removed the org-id filter from the Rule::exists in
  MoveTimetablePerformanceRequest; cross-tenant is caught by the policy
  in TimetableMoveController.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:07:29 +02:00

156 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Artist;
use App\Enums\Artist\BumaHandledBy;
use App\Http\Resources\Api\V1\Artist\ArtistEngagementResource;
use App\Models\Artist;
use App\Models\ArtistEngagement;
use App\Models\Event;
use App\Models\Organisation;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Tests\TestCase;
/**
* RFC v0.2 D26 — Buma + VAT computation.
*/
final class BumaVatCalculationTest extends TestCase
{
use RefreshDatabase;
private Organisation $org;
private Event $event;
private Artist $artist;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleSeeder::class);
$this->org = Organisation::factory()->create();
$this->event = Event::factory()->create(['organisation_id' => $this->org->id]);
$this->artist = Artist::factory()->create(['organisation_id' => $this->org->id]);
}
private function compute(array $attrs): array
{
$eng = ArtistEngagement::factory()->create(array_merge([
'artist_id' => $this->artist->id,
'event_id' => $this->event->id,
], $attrs));
$req = Request::create('/');
$payload = (new ArtistEngagementResource($eng))->toArray($req);
return $payload['computed'];
}
public function test_organisation_handles_buma_includes_buma_in_vat_grondslag(): void
{
$c = $this->compute([
'fee_amount' => 1000.00,
'buma_applicable' => true,
'buma_percentage' => 7.00,
'buma_handled_by' => BumaHandledBy::Organisation,
'vat_applicable' => true,
'vat_percentage' => 21.00,
'deal_breakdown' => [],
]);
$this->assertSame(70.0, $c['buma_amount']);
$this->assertSame(1070.0, $c['vat_grondslag']);
$this->assertSame(224.7, $c['vat_amount']);
$this->assertSame(1294.7, $c['total_cost']);
}
public function test_booking_agency_handles_buma_excludes_from_vat_grondslag(): void
{
$c = $this->compute([
'fee_amount' => 1000.00,
'buma_applicable' => true,
'buma_percentage' => 7.00,
'buma_handled_by' => BumaHandledBy::BookingAgency,
'vat_applicable' => true,
'vat_percentage' => 21.00,
'deal_breakdown' => [],
]);
$this->assertSame(0.0, $c['buma_amount']);
$this->assertSame(1000.0, $c['vat_grondslag']);
$this->assertSame(210.0, $c['vat_amount']);
$this->assertSame(1210.0, $c['total_cost']);
}
public function test_not_applicable_buma_yields_zero_buma(): void
{
$c = $this->compute([
'fee_amount' => 1000.00,
'buma_applicable' => false,
'buma_percentage' => 7.00,
'buma_handled_by' => BumaHandledBy::NotApplicable,
'vat_applicable' => true,
'vat_percentage' => 21.00,
'deal_breakdown' => [],
]);
$this->assertSame(0.0, $c['buma_amount']);
$this->assertSame(1000.0, $c['vat_grondslag']);
}
public function test_vat_disabled_yields_zero_vat(): void
{
$c = $this->compute([
'fee_amount' => 1000.00,
'buma_applicable' => true,
'buma_percentage' => 7.00,
'buma_handled_by' => BumaHandledBy::Organisation,
'vat_applicable' => false,
'vat_percentage' => 21.00,
'deal_breakdown' => [],
]);
$this->assertSame(70.0, $c['buma_amount']);
$this->assertSame(0.0, $c['vat_amount']);
}
public function test_breakdown_summed_into_total_cost(): void
{
$c = $this->compute([
'fee_amount' => 500.00,
'buma_applicable' => false,
'buma_handled_by' => BumaHandledBy::NotApplicable,
'vat_applicable' => false,
'deal_breakdown' => [
['label' => 'Hospitality', 'amount' => 50.00],
['label' => 'Hotel', 'amount' => 120.00],
],
]);
$this->assertSame(170.0, $c['breakdown_total']);
$this->assertSame(670.0, $c['total_cost']);
}
public function test_zero_fee_yields_zero_components(): void
{
$c = $this->compute([
'fee_amount' => 0,
'buma_applicable' => true,
'buma_percentage' => 7.00,
'buma_handled_by' => BumaHandledBy::Organisation,
'vat_applicable' => true,
'vat_percentage' => 21.00,
'deal_breakdown' => [],
]);
$this->assertSame(0.0, $c['buma_amount']);
$this->assertSame(0.0, $c['vat_amount']);
$this->assertSame(0.0, $c['total_cost']);
}
}