'shoraka_test', 'database.connections.shoraka_test' => ['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => ''], 'api.url' => 'https://api.test', 'session.driver' => 'array', ]); DB::purge('shoraka_test'); require_once database_path('migrations/2026_10_05_120000_create_shoraka_financings_table.php'); (new \CreateShorakaFinancingsTable())->up(); $this->actingAs(new User(['name' => 'teller1', 'cawangan' => 'KB2', 'roles' => 'teller'])); Http::swap(new Factory()); } protected function record(): ShorakaFinancing { return ShorakaFinancing::create([ 'kodcaw' => 'KB2', 'norujukan' => 'SAG-001', 'teller' => 'teller1', 'action' => 'gadai', 'state' => 'WAITING', 'context' => ['norujukan_gadai' => 'SAG-001', 'pinjaman' => 1000, 'cara_terima' => 'TUNAI'], ]); } protected function request(): Request { $request = new Request(['norujukan_gadai' => 'SAG-001', 'pinjaman' => 1000, '_token' => 'private']); $request->setLaravelSession(app('session.store')); return $request; } public function testHoldPersistsAndNeverSubmitsTwice(): void { Http::fake(function ($request) { $this->assertSame('https://api.test/api/shoraka/gadai/KB2/SAG-001/submit', $request->url()); return Http::response(['success' => true, 'data' => ['status' => 'Active']]); }); $service = new ShorakaFinancingService(); $service->hold($this->request(), 'SAG-001', 'gadai'); $service->hold($this->request(), 'SAG-001', 'gadai'); $this->assertSame(1, ShorakaFinancing::count()); $this->assertSame('WAITING', ShorakaFinancing::first()->state); $this->assertArrayNotHasKey('_token', ShorakaFinancing::first()->context); Http::assertSentCount(1); } public function testPendingStatusCannotTriggerPayment(): void { $record = $this->record(); Http::fake(function () { return Http::response(['data' => ['can_complete' => false]]); }); $controller = Mockery::mock(GadaianController::class); $controller->shouldNotReceive('completeShorakaFinancing'); $this->app->instance(GadaianController::class, $controller); try { (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); $this->fail('Pending trade must block payment.'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { $this->assertSame(409, $exception->getStatusCode()); $this->assertSame('WAITING', $record->fresh()->state); } } public function testCompletedTradePaysOnlyOnce(): void { $record = $this->record(); Http::fake(function () { return Http::response(['data' => ['can_complete' => true, 'order_amount' => 1000]]); }); $controller = Mockery::mock(GadaianController::class); $controller->shouldReceive('completeShorakaFinancing')->once()->andReturn(redirect('/')); $this->app->instance(GadaianController::class, $controller); $service = new ShorakaFinancingService(); $service->finish($this->request(), 'SAG-001'); $service->finish($this->request(), 'SAG-001'); $this->assertSame('DONE', $record->fresh()->state); $this->assertSame([], $record->fresh()->context); Http::assertSentCount(2); } public function testAmbiguousPaymentFailureRequiresReview(): void { $record = $this->record(); Http::fake(function () { return Http::response(['data' => ['can_complete' => true, 'order_amount' => 1000]]); }); $controller = Mockery::mock(GadaianController::class); $controller->shouldReceive('completeShorakaFinancing')->once()->andThrow(new \RuntimeException('Timeout')); $this->app->instance(GadaianController::class, $controller); try { (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); $this->fail('Expected payment failure.'); } catch (\RuntimeException $exception) { $this->assertSame('REVIEW', $record->fresh()->state); } $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); } public function testDifferentTellerCannotCompletePayment(): void { $record = $this->record(); $record->update(['teller' => 'someone-else']); Http::fake(); try { (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); $this->fail('Expected teller restriction.'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { $this->assertSame(403, $exception->getStatusCode()); } Http::assertNothingSent(); } public function testPendingTradeCannotPrintSag(): void { $this->record(); Http::fake(function () { return Http::response(['data' => [ 'can_complete' => false, 'order' => ['status' => 'Active'], ]]); }); $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); (new ShorakaFinancingService())->printData('SAG-001'); } public function testPrintUsesOrderCertificateAndVolume(): void { Http::fake(function () { return Http::response(['data' => [ 'can_complete' => true, 'order' => ['status' => 'Completed'], 'details' => ['certificateDetails1' => 'CERT-01|timestamp|seller', 'unit' => 'Tonnes', 'volume' => '1.25', 'commodityType' => '000-COPPER'], ]]); }); $data = (new ShorakaFinancingService())->printData('SAG-001'); $this->assertSame('CERT-01', $data['komoditi']['no_sijil']); $this->assertSame('1.25', $data['kuantiti']); } public function testBranchComesFromAuthenticatedUser(): void { $service = new ShorakaFinancingService(); $this->assertSame('https://api.test/api/shoraka/gadai/KB2/SAG-001', $service->url('SAG-001')); $record = $this->record(); $record->update(['kodcaw' => 'KB3']); $this->assertNull($service->local('SAG-001')); } public function testRegistrationDefersCashAndOnlinePayment(): void { \Illuminate\Support\Facades\Event::fake(); Http::fake(function ($request) { if (strpos($request->url(), '/shoraka/readiness') !== false) { return Http::response(['ready' => true]); } if (strpos($request->url(), '/marhun/gadai/') !== false) { return Http::response('2000.00'); } if (strpos($request->url(), '/gadai/norujukan/') !== false) { return Http::response('true'); } if (strpos($request->url(), '/shoraka/gadai/') !== false) { return Http::response(['success' => true, 'data' => ['status' => 'Active']]); } $this->fail('Unexpected payment request: ' . $request->url()); }); $request = $this->request(); $request->merge([ 'cara_terima' => 'TUNAI', 'keuntungan_sebulan' => 10, 'kadarkeuntungan' => 1, 'total_berat' => 5, 'nokp' => 'IC-1', ]); $result = app(GadaianController::class)->store($request); $this->assertStringContainsString('/shoraka/SAG-001', $result->getTargetUrl()); $this->assertSame('WAITING', ShorakaFinancing::first()->state); app(GadaianController::class)->store($request); Http::assertSentCount(4); \Illuminate\Support\Facades\Event::assertNotDispatched(\App\Events\Dashboard\GadaiNotify::class); } public function testReadinessFailureBlocksRegistrationBeforeSave(): void { Http::fake(function () { return Http::response(['ready' => false]); }); try { app(GadaianController::class)->store($this->request()); $this->fail('Registration should be blocked.'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { $this->assertSame(503, $exception->getStatusCode()); } Http::assertSentCount(1); $this->assertSame(0, ShorakaFinancing::count()); } public function testRefinancingUsesNewReferenceAndDefersCash(): void { \Illuminate\Support\Facades\Event::fake(); Http::fake(function ($request) { if (strpos($request->url(), '/shoraka/readiness') !== false) { return Http::response(['ready' => true]); } if (strpos($request->url(), '/updatetambahpinjaman/') !== false) { return Http::response(['result' => 'success', 'norujukan_baru' => 'NEW-SAG']); } if (strpos($request->url(), '/shoraka/gadai/KB2/NEW-SAG/submit') !== false) { return Http::response(['success' => true]); } $this->fail('Unexpected cash/legacy commodity request: ' . $request->url()); }); $request = $this->request(); $request->merge(['norujukan' => 'OLD-SAG']); $request->headers->set('X-Requested-With', 'XMLHttpRequest'); $result = app(\App\Http\Controllers\PenebusanController::class)->tambahpinjaman_latest($request); $this->assertSame(202, $result->getStatusCode()); $this->assertSame('NEW-SAG', ShorakaFinancing::first()->norujukan); $this->assertSame('refinance', ShorakaFinancing::first()->action); \Illuminate\Support\Facades\Event::assertNotDispatched(\App\Events\Dashboard\GadaiNotify::class); } public function testChangedSavedPayoutAmountCannotBePaid(): void { $this->record(); Http::fake(function () { return Http::response(['data' => [ 'can_complete' => true, 'order_amount' => 2000, ]]); }); $controller = Mockery::mock(GadaianController::class); $controller->shouldNotReceive('completeShorakaFinancing'); $this->app->instance(GadaianController::class, $controller); $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class); (new ShorakaFinancingService())->finish($this->request(), 'SAG-001'); } }