mdn-backend / tests /Unit /ClientOnboardingSetupTest.php
internationalscholarsprogram's picture
feat(backend): replace backend with peter branch + Emergency Care integration
6773345
Raw
History Blame Contribute Delete
10.4 kB
<?php
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(),
]);
}
}