Spaces:
Runtime error
Runtime error
| namespace Tests\Unit; | |
| use App\Domain\Identity\ClientOnboardingService; | |
| use App\Shared\Exceptions\ApiException; | |
| use Illuminate\Database\Schema\Blueprint; | |
| use Illuminate\Support\Facades\DB; | |
| use Illuminate\Support\Facades\Schema; | |
| use PDO; | |
| use Tests\TestCase; | |
| class ClientOnboardingSetupTest extends TestCase | |
| { | |
| protected function setUp(): void | |
| { | |
| parent::setUp(); | |
| if (! in_array('sqlite', PDO::getAvailableDrivers(), true)) { | |
| $this->markTestSkipped('SQLite PDO driver is required for client onboarding database regression tests.'); | |
| } | |
| foreach ([ | |
| 'communication_delivery_attempts', | |
| 'communication_messages', | |
| 'audit_events', | |
| 'user_roles', | |
| 'roles', | |
| 'users', | |
| 'client_account_invitations', | |
| 'contracts', | |
| 'payments', | |
| 'invoices', | |
| 'applications', | |
| ] as $table) { | |
| Schema::dropIfExists($table); | |
| } | |
| $this->createSchema(); | |
| $this->seedEligibleApplication(); | |
| } | |
| public function test_contract_acceptance_invitation_uses_client_setup_url_and_sends_email(): void | |
| { | |
| config(['app.client_portal_setup_url' => 'https://example.test']); | |
| $result = app(ClientOnboardingService::class)->inviteAfterContractAcceptance('app-1'); | |
| $this->assertSame('INVITED', $result['status']); | |
| $this->assertSame('applicant@example.com', $result['email']); | |
| $this->assertTrue($result['email_sent']); | |
| $this->assertStringStartsWith('https://example.test/client/setup/', $result['setup_url']); | |
| $this->assertSame(1, DB::table('client_account_invitations')->where('status', 'INVITED')->count()); | |
| $message = DB::table('communication_messages')->where('event_type', 'CLIENT_PORTAL_SETUP_INVITED')->first(); | |
| $this->assertNotNull($message); | |
| $this->assertStringContainsString($result['setup_url'], $message->body); | |
| $this->assertStringNotContainsString('internal My Diaspora Nexus email address', $message->body); | |
| } | |
| public function test_setup_token_creates_client_account_and_cannot_be_reused(): void | |
| { | |
| config(['app.client_portal_setup_url' => 'https://example.test']); | |
| $invitation = app(ClientOnboardingService::class)->inviteAfterContractAcceptance('app-1'); | |
| $token = basename(parse_url($invitation['setup_url'], PHP_URL_PATH)); | |
| $setup = app(ClientOnboardingService::class)->getSetupInvitation($token); | |
| $this->assertSame('applicant@example.com', $setup['invitation']['email']); | |
| $result = app(ClientOnboardingService::class)->completeSetup($token, 'StrongPass123!'); | |
| $this->assertSame('applicant@example.com', $result['user']['email']); | |
| $this->assertSame('CLIENT', $result['user']['type']); | |
| $storedInvitation = DB::table('client_account_invitations')->where('id', $invitation['invitation_id'])->first(); | |
| $application = DB::table('applications')->where('id', 'app-1')->first(); | |
| $this->assertSame('ACCEPTED', $storedInvitation->status); | |
| $this->assertNotNull($storedInvitation->user_id); | |
| $this->assertSame($storedInvitation->user_id, $application->user_id); | |
| $this->expectException(ApiException::class); | |
| app(ClientOnboardingService::class)->getSetupInvitation($token); | |
| } | |
| private function createSchema(): void | |
| { | |
| Schema::create('applications', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('application_reference')->nullable(); | |
| $table->string('status'); | |
| $table->string('first_name')->nullable(); | |
| $table->string('last_name')->nullable(); | |
| $table->string('email')->nullable(); | |
| $table->string('user_id')->nullable(); | |
| $table->timestamp('approved_at')->nullable(); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('invoices', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('application_id')->nullable(); | |
| $table->string('fee_type_code'); | |
| $table->string('status'); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('payments', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('invoice_id'); | |
| $table->string('status'); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('contracts', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('application_id')->nullable(); | |
| $table->string('status'); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('client_account_invitations', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('application_id'); | |
| $table->string('user_id')->nullable(); | |
| $table->string('email'); | |
| $table->string('token_hash')->unique(); | |
| $table->string('status'); | |
| $table->timestamp('expires_at'); | |
| $table->timestamp('accepted_at')->nullable(); | |
| $table->string('invited_by_user_id')->nullable(); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('users', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('email'); | |
| $table->string('email_normalized')->nullable(); | |
| $table->string('password_hash')->nullable(); | |
| $table->string('user_status')->nullable(); | |
| $table->string('user_type')->nullable(); | |
| $table->string('portal_status')->nullable(); | |
| $table->timestamp('email_verified_at')->nullable(); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('roles', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('code'); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('user_roles', function (Blueprint $table): void { | |
| $table->string('user_id'); | |
| $table->string('role_id'); | |
| $table->timestamp('effective_from')->nullable(); | |
| $table->timestamp('effective_to')->nullable(); | |
| $table->string('assigned_by_user_id')->nullable(); | |
| $table->string('assignment_reason')->nullable(); | |
| }); | |
| Schema::create('audit_events', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('event_type'); | |
| $table->string('event_category'); | |
| $table->string('actor_user_id')->nullable(); | |
| $table->string('actor_role_code')->nullable(); | |
| $table->string('subject_type'); | |
| $table->string('subject_id')->nullable(); | |
| $table->string('affected_owner_user_id')->nullable(); | |
| $table->string('result'); | |
| $table->timestamp('occurred_at'); | |
| $table->string('correlation_id')->nullable(); | |
| $table->string('ip_address')->nullable(); | |
| $table->text('user_agent')->nullable(); | |
| $table->text('metadata')->nullable(); | |
| }); | |
| Schema::create('communication_messages', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('template_id')->nullable(); | |
| $table->string('channel'); | |
| $table->string('recipient_user_id')->nullable(); | |
| $table->string('recipient_email')->nullable(); | |
| $table->string('recipient_phone')->nullable(); | |
| $table->string('event_type'); | |
| $table->string('subject')->nullable(); | |
| $table->text('body'); | |
| $table->string('status'); | |
| $table->timestamp('queued_at')->nullable(); | |
| $table->timestamp('sent_at')->nullable(); | |
| $table->string('correlation_id')->nullable(); | |
| $table->text('context')->nullable(); | |
| $table->timestamps(); | |
| $table->softDeletes(); | |
| }); | |
| Schema::create('communication_delivery_attempts', function (Blueprint $table): void { | |
| $table->string('id')->primary(); | |
| $table->string('message_id'); | |
| $table->integer('attempt_number'); | |
| $table->string('provider_code'); | |
| $table->string('provider_reference')->nullable(); | |
| $table->string('status'); | |
| $table->timestamp('attempted_at'); | |
| $table->string('error_code')->nullable(); | |
| $table->text('error_message')->nullable(); | |
| $table->timestamps(); | |
| }); | |
| } | |
| private function seedEligibleApplication(): void | |
| { | |
| DB::table('applications')->insert([ | |
| 'id' => 'app-1', | |
| 'application_reference' => 'APP-1', | |
| 'status' => 'APPROVED', | |
| 'first_name' => 'Test', | |
| 'last_name' => 'Applicant', | |
| 'email' => 'applicant@example.com', | |
| 'approved_at' => now(), | |
| 'created_at' => now(), | |
| 'updated_at' => now(), | |
| ]); | |
| DB::table('invoices')->insert([ | |
| 'id' => 'invoice-1', | |
| 'application_id' => 'app-1', | |
| 'fee_type_code' => 'ANNUAL_MEMBERSHIP_FEE', | |
| 'status' => 'PAID', | |
| 'created_at' => now(), | |
| 'updated_at' => now(), | |
| ]); | |
| DB::table('payments')->insert([ | |
| 'id' => 'payment-1', | |
| 'invoice_id' => 'invoice-1', | |
| 'status' => 'PAID', | |
| 'created_at' => now(), | |
| 'updated_at' => now(), | |
| ]); | |
| DB::table('contracts')->insert([ | |
| 'id' => 'contract-1', | |
| 'application_id' => 'app-1', | |
| 'status' => 'ACCEPTED', | |
| 'created_at' => now(), | |
| 'updated_at' => now(), | |
| ]); | |
| DB::table('roles')->insert([ | |
| 'id' => 'role-client', | |
| 'code' => 'CLIENT', | |
| 'created_at' => now(), | |
| 'updated_at' => now(), | |
| ]); | |
| } | |
| } | |