markTestSkipped('SQLite PDO driver is required for application readiness database regression tests.'); } Schema::dropIfExists('applications'); Schema::create('applications', function (Blueprint $table): void { $table->string('id')->primary(); $table->string('application_reference')->nullable(); $table->string('status')->nullable(); $table->string('pathway')->nullable(); $table->string('email')->nullable(); $table->string('phone_number')->nullable(); $table->string('country_of_residence')->nullable(); $table->string('first_name')->nullable(); $table->string('last_name')->nullable(); $table->text('selected_product_codes')->nullable(); $table->text('metadata')->nullable(); $table->timestamp('submitted_at')->nullable(); $table->timestamp('ready_for_verification_at')->nullable(); $table->timestamps(); $table->softDeletes(); }); } public function test_review_search_returns_payment_completed_application_without_submitted_at(): void { $this->insertApplication('ready-from-payment', [ 'status' => 'APPLICATION_COMPLETED', 'submitted_at' => null, 'ready_for_verification_at' => '2026-06-30 10:00:00', ]); $this->insertApplication('in-progress', [ 'status' => 'APPLICATION_IN_PROGRESS', 'submitted_at' => null, 'ready_for_verification_at' => null, ]); $result = (new ApplicationRepository())->searchForReview([]); $this->assertSame(['ready-from-payment'], array_map(fn (object $application): string => $application->id, $result['applications'])); $this->assertSame(1, $result['pagination']['total']); } public function test_review_submitted_date_filters_use_review_readiness_date(): void { $this->insertApplication('inside-window', [ 'status' => 'APPLICATION_COMPLETED', 'submitted_at' => null, 'ready_for_verification_at' => '2026-06-30 10:00:00', ]); $this->insertApplication('outside-window', [ 'status' => 'APPLICATION_COMPLETED', 'submitted_at' => null, 'ready_for_verification_at' => '2026-06-20 10:00:00', ]); $result = (new ApplicationRepository())->searchForReview([ 'submitted_from' => '2026-06-29 00:00:00', 'submitted_to' => '2026-07-01 00:00:00', ]); $this->assertSame(['inside-window'], array_map(fn (object $application): string => $application->id, $result['applications'])); } public function test_payment_completion_sets_submitted_at_when_missing(): void { $this->insertApplication('payment-ready', [ 'status' => 'APPLICATION_IN_PROGRESS', 'submitted_at' => null, 'ready_for_verification_at' => null, ]); $this->invokeCompleteApplicationFeeIfNeeded((object) [ 'application_id' => 'payment-ready', 'fee_type_code' => 'APPLICATION_FEE', ]); $application = DB::table('applications')->where('id', 'payment-ready')->first(); $this->assertSame('APPLICATION_COMPLETED', $application->status); $this->assertNotNull($application->ready_for_verification_at); $this->assertNotNull($application->submitted_at); } public function test_payment_completion_preserves_existing_submitted_at(): void { $this->insertApplication('already-submitted', [ 'status' => 'APPLICATION_IN_PROGRESS', 'submitted_at' => '2026-06-01 08:30:00', 'ready_for_verification_at' => null, ]); $this->invokeCompleteApplicationFeeIfNeeded((object) [ 'application_id' => 'already-submitted', 'fee_type_code' => 'APPLICATION_FEE', ]); $application = DB::table('applications')->where('id', 'already-submitted')->first(); $this->assertSame('2026-06-01 08:30:00', $application->submitted_at); } public function test_approval_payment_url_identifies_membership_payment_type(): void { config(['app.approval_payment_form_url' => 'https://example.test/payment']); $url = $this->invokeApprovalPaymentUrl('approval-token-123'); $parts = parse_url($url); parse_str($parts['query'] ?? '', $query); $this->assertSame('https', $parts['scheme']); $this->assertSame('example.test', $parts['host']); $this->assertSame('/payment', $parts['path']); $this->assertSame('membership_fee', $query['payment_type'] ?? null); $this->assertSame('approval-token-123', $query['approval_token'] ?? null); } public function test_approval_payment_url_derives_from_website_url_when_override_is_missing(): void { config([ 'app.approval_payment_form_url' => null, 'platform.website_url' => 'https://clientportaldev.qhtestingserver.com/diaspora-nexus', ]); $url = $this->invokeApprovalPaymentUrl('approval-token-456'); $parts = parse_url($url); parse_str($parts['query'] ?? '', $query); $this->assertSame('https://clientportaldev.qhtestingserver.com/diaspora-nexus/payment', ($parts['scheme'] ?? '').'://'.($parts['host'] ?? '').($parts['path'] ?? '')); $this->assertSame('membership_fee', $query['payment_type'] ?? null); $this->assertSame('approval-token-456', $query['approval_token'] ?? null); } private function insertApplication(string $id, array $overrides = []): void { DB::table('applications')->insert(array_merge([ 'id' => $id, 'application_reference' => 'APP-'.$id, 'status' => 'APPLICATION_IN_PROGRESS', 'pathway' => 'STANDARD', 'email' => $id.'@example.com', 'phone_number' => '+1000000000', 'country_of_residence' => 'Kenya', 'first_name' => 'Test', 'last_name' => 'Applicant', 'selected_product_codes' => null, 'metadata' => null, 'submitted_at' => null, 'ready_for_verification_at' => null, 'created_at' => '2026-06-01 00:00:00', 'updated_at' => '2026-06-01 00:00:00', 'deleted_at' => null, ], $overrides)); } private function invokeCompleteApplicationFeeIfNeeded(object $payment): void { $reflection = new ReflectionClass(PaymentService::class); $service = $reflection->newInstanceWithoutConstructor(); $method = $reflection->getMethod('completeApplicationFeeIfNeeded'); $method->setAccessible(true); $method->invoke($service, $payment); } private function invokeApprovalPaymentUrl(string $token): string { $reflection = new ReflectionClass(ApplicationService::class); $service = $reflection->newInstanceWithoutConstructor(); $method = $reflection->getMethod('approvalPaymentUrl'); $method->setAccessible(true); return $method->invoke($service, $token); } }