create(['email_verified_at' => now()]); $customer = Customer::factory()->create(['email' => $user->email, 'user_id' => $user->id]); return [$user, $customer]; } function publishedDpa(string $version = '1.0', bool $withMeasures = true): DpaVersion { Storage::fake('local'); return DpaVersion::query()->create([ 'version' => $version, 'agreement_path' => UploadedFile::fake()->create('avv.pdf', 12, 'application/pdf')->store('dpa', 'local'), 'measures_path' => $withMeasures ? UploadedFile::fake()->create('tom.pdf', 12, 'application/pdf')->store('dpa', 'local') : null, 'published_at' => now()->subDay(), ]); } it('shows the customer nothing at all until a version is in force', function () { // A card offering an agreement that does not exist is worse than silence. [$user] = dpaCustomer(); Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract']) ->assertDontSee(__('dpa.title')); }); it('offers the document and records the acceptance with its evidence', function () { $version = publishedDpa(); [$user, $customer] = dpaCustomer(); $page = Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract']); $page->assertSee(__('dpa.title')) ->assertSee(__('dpa.state_open')) ->assertSee(__('dpa.accept_cta')) ->call('acceptProcessingAgreement') ->assertDispatched('notify'); $acceptance = DpaAcceptance::query()->where('customer_id', $customer->id)->sole(); // What makes it evidence rather than a flag: the version, the moment, the // login that pressed it, and where from. expect($acceptance->dpa_version_id)->toBe($version->id) ->and($acceptance->user_id)->toBe($user->id) ->and($acceptance->accepted_at)->not->toBeNull() ->and($acceptance->ip)->not->toBeNull(); // And the card now states it rather than asking again. Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract']) ->assertSee(__('dpa.state_accepted')) ->assertDontSee(__('dpa.accept_cta')); }); it('counts pressing twice as one agreement', function () { publishedDpa(); [$user, $customer] = dpaCustomer(); Livewire::actingAs($user)->test(Settings::class, ['tab' => 'contract']) ->call('acceptProcessingAgreement') ->call('acceptProcessingAgreement'); expect(DpaAcceptance::query()->where('customer_id', $customer->id)->count())->toBe(1); }); it('asks again when a new version comes into force, and keeps the old acceptance', function () { // Acceptance is per version: a new one supersedes the last, and accepting a // superseded document is not accepting the one in force. The old row stays — // it was true when it was made, and the history is the point. $first = publishedDpa('1.0'); [$user, $customer] = dpaCustomer(); app(ProcessingAgreement::class)->accept($customer); expect(app(ProcessingAgreement::class)->accepted($customer))->toBeTrue(); $second = DpaVersion::query()->create([ 'version' => '2.0', 'agreement_path' => $first->agreement_path, 'published_at' => now(), ]); expect(app(ProcessingAgreement::class)->current()->id)->toBe($second->id) ->and(app(ProcessingAgreement::class)->accepted($customer))->toBeFalse() // Not deleted, not overwritten. ->and(DpaAcceptance::query()->where('dpa_version_id', $first->id)->exists())->toBeTrue(); }); it('serves the document to the customer, and only the one in force', function () { publishedDpa(); [$user] = dpaCustomer(); $this->actingAs($user)->get(route('dpa.file', 'agreement'))->assertOk(); $this->actingAs($user)->get(route('dpa.file', 'measures'))->assertOk(); // Which document applies is ours to say, not a parameter somebody can put // in the address. $this->actingAs($user)->get(route('dpa.file', 'sonstwas'))->assertNotFound(); }); it('does not hand the agreement to a stranger', function () { publishedDpa(); $this->get(route('dpa.file', 'agreement'))->assertRedirect(); }); it('answers 404 rather than 500 when nothing is published', function () { [$user] = dpaCustomer(); $this->actingAs($user)->get(route('dpa.file', 'agreement'))->assertNotFound(); }); // ---- The console ---- it('takes an upload as a draft, and only a second act puts it in force', function () { // Publishing leaves every customer who accepted the previous version // outstanding again. That is correct, and expensive to trigger by dropping a // file on a form. Storage::fake('local'); Livewire::actingAs(operator('Owner'), 'operator') ->test(ProcessingAgreements::class) ->set('version', '3.0') ->set('agreement', UploadedFile::fake()->create('avv.pdf', 20, 'application/pdf')) ->call('upload') ->assertHasNoErrors(); $version = DpaVersion::query()->where('version', '3.0')->sole(); expect($version->isPublished())->toBeFalse() ->and(app(ProcessingAgreement::class)->current())->toBeNull() ->and(Storage::disk('local')->exists($version->agreement_path))->toBeTrue(); Livewire::actingAs(operator('Owner'), 'operator') ->test(ProcessingAgreements::class) ->call('publish', $version->uuid); expect(app(ProcessingAgreement::class)->current()?->id)->toBe($version->id); }); it('takes the agreement as a PDF and nothing else', function () { Storage::fake('local'); Livewire::actingAs(operator('Owner'), 'operator') ->test(ProcessingAgreements::class) ->set('version', '4.0') ->set('agreement', UploadedFile::fake()->create('avv.docx', 20, 'application/msword')) ->call('upload') ->assertHasErrors(['agreement']); }); it('refuses two versions under one name', function () { publishedDpa('1.0'); Storage::fake('local'); Livewire::actingAs(operator('Owner'), 'operator') ->test(ProcessingAgreements::class) ->set('version', '1.0') ->set('agreement', UploadedFile::fake()->create('avv.pdf', 20, 'application/pdf')) ->call('upload') ->assertHasErrors(['version']); }); it('keeps the whole thing to operators who may manage it', function () { // Its own capability: whoever keeps the platform running does not thereby // decide what every customer is asked to agree to. Livewire::actingAs(operator('Read-only'), 'operator') ->test(ProcessingAgreements::class) ->assertForbidden(); $this->actingAs(operator('Read-only'), 'operator') ->get(route('admin.dpa')) ->assertForbidden(); }); it('confirms publication in a modal rather than on one click', function () { // R23, and for a reason beyond the rule: this is the click that makes every // customer outstanding again. $page = Illuminate\Support\Facades\File::get(resource_path('views/livewire/admin/processing-agreements.blade.php')); expect($page)->toContain("component: 'admin.confirm-publish-dpa'"); $modal = Illuminate\Support\Facades\File::get(app_path('Livewire/Admin/ConfirmPublishDpa.php')); expect($modal)->toContain("dispatch('dpa-publish-confirmed'") // The modal decides nothing: the permission check stays where it was. ->and($modal)->not->toContain('published_at'); });