feat(shoraka): integrate transactions with Shoraka API

This commit is contained in:
nurafrinaalimi16
2026-10-08 12:39:20 +08:00
parent 464d3e8dbb
commit 1d6baf3b1f
25 changed files with 780 additions and 157 deletions
+12 -12
View File
@@ -125,10 +125,10 @@ class AliranTunaiController extends Controller
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan_gadai)->json();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan_gadai)->throw()->json();
$gadai_detail = $gadai[0];
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $teller])->json();
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $teller])->throw()->json();
$keluar = $getTunai2['keluar'];
$bakiakhir = $getTunai2['baki'];
@@ -143,7 +143,7 @@ class AliranTunaiController extends Controller
'keluar' => $keluar,
'masuk' => $masuk,
'bakiakhir' => $bakiakhir
]);
])->throw();
return response()->json($updateTunai,201);
@@ -153,13 +153,13 @@ class AliranTunaiController extends Controller
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->json();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->throw()->json();
$gadai_detail = $gadai[0];
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->json();
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->throw()->json();
$tebus_detail = $tebus[0];
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->json();
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->throw()->json();
$keluar=$getTunai2['keluar'];
$baki=$getTunai2['baki'];
@@ -181,7 +181,7 @@ class AliranTunaiController extends Controller
'keluar'=>$keluar,
'masuk'=>$masuk,
'bakiakhir'=>$bakiakhir
]);
])->throw();
}else{
@@ -199,7 +199,7 @@ class AliranTunaiController extends Controller
'keluar'=>$keluar,
'masuk'=>$masuk,
'bakiakhir'=>$bakiakhir
]);
])->throw();
}
}
@@ -208,13 +208,13 @@ class AliranTunaiController extends Controller
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->json();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->throw()->json();
$gadai_detail = $gadai[0];
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->json();
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->throw()->json();
$tebus_detail = $tebus[0];
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->json();
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->throw()->json();
$keluar=$getTunai2['keluar'];
$baki=$getTunai2['baki'];
@@ -228,7 +228,7 @@ class AliranTunaiController extends Controller
'keluar'=>$keluar,
'masuk'=>$masuk,
'bakiakhir'=>$bakiakhir
]);
])->throw();
}
public function alirantunaiUpahBknTawarruq(Request $request){
+25 -7
View File
@@ -285,6 +285,11 @@ class GadaianController extends Controller
public function store(Request $request)
{
$auth_user = Auth::user();
$shoraka = app(\App\Services\ShorakaFinancingService::class);
if ($shoraka->local($request->norujukan_gadai)) {
return $shoraka->pendingResponse($request, $request->norujukan_gadai);
}
$shoraka->ensureReady();
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
$pinjamanAmt = round((float) $request->pinjaman, 2);
@@ -342,16 +347,20 @@ class GadaianController extends Controller
'nokp' => $request->nokp,
'kadar_rebate' => $this->parseMoney($request->kadar_rebate),
'rebate' => $this->parseMoney($request->rebate),
])->json();
])->throw()->json();
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/komuditi/transaksi', [
'unit_komuditi' => round($request->pinjaman, 2),
'norujukan' => $request->norujukan_gadai,
'kodcaw' => $auth_user['cawangan'],
'teller' => $auth_user['name']
return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [
'cara_terima' => $caraTerima,
'pinjaman' => $pinjamanAmt,
]);
}
public function completeShorakaFinancing(Request $request)
{
$auth_user = Auth::user();
$caraTerima = $request->cara_terima;
$pinjamanAmt = round((float) $request->pinjaman, 2);
if ($caraTerima === 'ONLINE') {
$online = Http::withOptions(['verify' => config('app.api_verify')])
->asForm()
@@ -374,7 +383,7 @@ class GadaianController extends Controller
$msg = is_array($body) && isset($body['message'])
? $body['message']
: 'Gagal daftar pembiayaan online. Sila cuba lagi.';
return redirect()->back()->with('error', $msg);
throw new \RuntimeException($msg);
}
$this->notifyPcPembiayaanOnlinePending($auth_user, $request, $pinjamanAmt);
@@ -594,6 +603,7 @@ class GadaianController extends Controller
public function cetakSAG(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->printData($request->norujukan_modal);
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/cetak/' . $auth_user['cawangan'] . '/' . $request->norujukan_modal);
@@ -614,6 +624,7 @@ class GadaianController extends Controller
}
foreach ($arraynorujukan as $index => $norujukan) {
app(\App\Services\ShorakaFinancingService::class)->printData($norujukan);
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/serah/' . $auth_user['cawangan'] . '/' . $norujukan);
if ($gadai->failed()) {
@@ -671,6 +682,12 @@ class GadaianController extends Controller
// $pembayaran_segera = "https://spare.arrahn.com.my/pembayaransegera";
$pembayaran_segera = "https://pay.arrahn.com.my";
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komoditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komoditi = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/check')
->json();
@@ -678,6 +695,7 @@ class GadaianController extends Controller
->get($api_url . '/api/komuditi/description', ['kodproduk' => $komoditi['kodproduct']])
->json();
$kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai'];
}
if ($gadai_detail['tagbaru'] == 1) {
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
@@ -522,6 +522,12 @@ class PembiayaanOnlineController extends Controller
$pembayaran_segera = 'https://pay.arrahn.com.my';
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komoditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komoditi = $this->api()->get($api_url . '/api/komuditi/check')->json();
$product = $this->api()
->get($api_url . '/api/komuditi/description', [
@@ -531,6 +537,7 @@ class PembiayaanOnlineController extends Controller
$nilai = is_array($komoditi) ? (float) ($komoditi['nilai'] ?? 0) : 0;
$kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0;
}
if (($gadai_detail['tagbaru'] ?? 0) == 1) {
$cawangan_info = $this->api()
+24 -64
View File
@@ -271,6 +271,7 @@ class PenebusanController extends Controller
public function tambahpinjaman(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$api_url = config('api.url');
$current = Carbon::now();
$current = new Carbon();
@@ -405,33 +406,16 @@ class PenebusanController extends Controller
'upah12' => $keuntungan12bulan,
'hargajualan' => $hargajualan,
'kuantiti_marhun' => $request->kuantiti_marhun
])->json();
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/komuditi/transaksi',[
'unit_komuditi' => round($request->pinjaman, 2),
'norujukan' => $norujukanbaru,
'kodcaw' => $auth_user['cawangan'],
'teller' => $auth_user['name']
]);
////////////////////////////
////Update History Gadaian///
////////////////////////////
$updatehistorygadaian = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/gadai/updatehistory/'. $auth_user['cawangan'] .'/' . $norujukanbaru,[
'historygadaian' => $request->norujukan,
'jenistebus' => $request->jenistebus,
])->json();
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$this->AliranTunaiController->alirantunaiTebus($request);
])->throw()->json();
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $norujukanbaru, 'refinance', ['update_history' => true]
);
}
public function tambahpinjaman_latest(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$api_url = config('api.url');
$current = Carbon::now();
$current = new Carbon();
@@ -475,17 +459,18 @@ class PenebusanController extends Controller
])->json();
if($updatetebus['result'] == 'success'){
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$this->AliranTunaiController->alirantunaiTebus($request);
if (($updatetebus['result'] ?? null) === 'success') {
abort_unless(!empty($updatetebus['norujukan_baru']), 502, 'Rujukan gadaian baharu tidak diterima.');
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $updatetebus['norujukan_baru'], 'refinance'
);
}
return response()->json($updatetebus['result'],200);
return response()->json($updatetebus['result'] ?? 'error', 200);
}
public function tebusautotawarruq(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$auth_user = Auth::user();
////////////////////////
@@ -515,17 +500,18 @@ class PenebusanController extends Controller
"noic" => $request->noic,
"jenisAT" => $request->jenisAT,
])->json();
if($updatetebus['result'] == 'success'){
$alirantunai = new AliranTunaiController();
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$alirantunai->alirantunaiTebusAutoTawarruq($request);
if (($updatetebus['result'] ?? null) === 'success') {
abort_unless(!empty($updatetebus['norujukan_baru']), 502, 'Rujukan gadaian baharu tidak diterima.');
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $updatetebus['norujukan_baru'], 'autotawarruq'
);
}
return response()->json($updatetebus['result'],200);
return response()->json($updatetebus['result'] ?? 'error', 200);
}
public function tebusseparuh(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$api_url = config('api.url');
$current = Carbon::now();
$current = new Carbon();
@@ -666,39 +652,13 @@ class PenebusanController extends Controller
'upah12' => $keuntungan12bulan,
'hargajualan' => $hargajualan,
'kuantiti_marhun' => $request->kuantiti_marhun
])->json();
])->throw()->json();
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/komuditi/transaksi',[
'unit_komuditi' => round($request->pinjaman, 2),
'norujukan' => $norujukanbaru,
'kodcaw' => $auth_user['cawangan'],
'teller' => $auth_user['name']
]);
////////////////////////////
////Update History Gadaian///
////////////////////////////
$updatehistorygadaian = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/gadai/updatehistory/'. $auth_user['cawangan'] .'/' . $norujukanbaru,[
'historygadaian' => $request->norujukan,
'jenistebus' => $request->jenistebus,
])->json();
$alirantunai = new AliranTunaiController();
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$alirantunai->alirantunaiTebus($request);
return response()->json($updatemarhun,200);
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $norujukanbaru, 'refinance', ['update_history' => true]
);
}
//Wakil Function
public function getfirstWakil()
{
$auth_user = Auth::user();
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\ShorakaFinancing;
use App\Services\ShorakaFinancingService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Throwable;
class ShorakaController extends Controller
{
public function index()
{
$records = ShorakaFinancing::where('kodcaw', Auth::user()->cawangan)
->orderByDesc('id')->limit(100)->get();
$cawangan_info = ['details_caw' => Auth::user()->cawangan, 'kodcaw' => Auth::user()->cawangan];
return view('shoraka.index', compact('records', 'cawangan_info'));
}
public function show(string $norujukan, ShorakaFinancingService $service)
{
$record = $service->local($norujukan);
$data = null;
$error = null;
try {
$data = $service->api()->get($service->url($norujukan))->throw()->json()['data'];
} catch (Throwable $exception) {
$error = 'Maklumat Shoraka tidak dapat diambil. Bayaran dan SAG masih ditahan.';
}
$cawangan_info = ['details_caw' => Auth::user()->cawangan, 'kodcaw' => Auth::user()->cawangan];
return view('shoraka.show', compact('norujukan', 'record', 'data', 'error', 'cawangan_info'));
}
public function submit(string $norujukan, ShorakaFinancingService $service)
{
$record = $service->local($norujukan);
abort_unless($record && $record->teller === Auth::user()->name, 403);
$response = $service->api()->post($service->url($norujukan, '/submit'));
return redirect()->route('shoraka.show', compact('norujukan'))
->with('shoraka_message', $response->successful()
? 'Order dihantar. Semak status sebelum menyelesaikan pembiayaan.'
: (($response->json()['message'] ?? null) ?: 'Order belum berjaya. Semak status sebelum cuba semula.'));
}
public function refresh(string $norujukan, ShorakaFinancingService $service)
{
try {
$response = $service->api()->post($service->url($norujukan, '/refresh'));
$message = $response->successful() ? 'Status dikemas kini.'
: (($response->json()['message'] ?? null) ?: 'Status tidak dapat dikemas kini.');
} catch (Throwable $exception) {
$message = 'Sambungan terganggu. Jangan hantar order semula.';
}
return redirect()->route('shoraka.show', compact('norujukan'))->with('shoraka_message', $message);
}
public function finish(Request $request, string $norujukan, ShorakaFinancingService $service)
{
return $service->finish($request, $norujukan);
}
public function certificate(string $norujukan, ShorakaFinancingService $service)
{
$response = $service->api()->get($service->url($norujukan, '/certificate'))->throw();
abort_unless(substr($response->body(), 0, 5) === '%PDF-', 502);
return response($response->body(), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="sijil-shoraka.pdf"',
'Cache-Control' => 'private, no-store',
]);
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
namespace App\Services;
use App\ShorakaFinancing;
use App\Http\Controllers\GadaianController;
use App\Http\Controllers\AliranTunaiController;
use App\Events\Dashboard\GadaiNotify;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Throwable;
class ShorakaFinancingService
{
public function ensureReady(): void
{
// Check local migration before any registration side effect.
ShorakaFinancing::where('kodcaw', Auth::user()->cawangan)->exists();
$response = $this->api()->get(rtrim(config('api.url'), '/') . '/api/shoraka/readiness')->throw()->json();
abort_unless(!empty($response['ready']), 503, 'Shoraka belum dikonfigurasi. Pembiayaan baharu ditahan.');
}
public function api()
{
return Http::withOptions(['verify' => config('app.api_verify')])
->acceptJson()->timeout(75);
}
public function url(string $reference, string $suffix = ''): string
{
return rtrim(config('api.url'), '/') . '/api/shoraka/gadai/'
. rawurlencode(Auth::user()->cawangan) . '/' . rawurlencode($reference) . $suffix;
}
public function local(string $reference)
{
return ShorakaFinancing::where('kodcaw', Auth::user()->cawangan)
->where('norujukan', $reference)->first();
}
public function hold(Request $request, string $reference, string $action, array $extra = [])
{
$user = Auth::user();
$record = ShorakaFinancing::firstOrCreate(
['kodcaw' => $user->cawangan, 'norujukan' => $reference],
[
'teller' => $user->name, 'action' => $action, 'state' => 'WAITING',
'context' => array_merge($request->except(['_token', 'password']), $extra),
]
);
// A duplicate request only opens the saved workflow; it never replays registration.
if ($record->wasRecentlyCreated) {
try {
$this->api()->post($this->url($reference, '/submit'));
} catch (Throwable $exception) {
// The persisted workflow remains available after timeouts.
}
}
return $this->pendingResponse($request, $reference);
}
public function pendingResponse(Request $request, string $reference)
{
$url = route('shoraka.show', ['norujukan' => $reference]);
if ($request->ajax() || $request->expectsJson()) {
return response()->json(['shoraka_pending' => true, 'redirect' => $url], 202);
}
return redirect($url);
}
public function finish(Request $request, string $reference)
{
$record = $this->local($reference);
abort_unless($record && $record->teller === Auth::user()->name, 403,
'Pembiayaan perlu diselesaikan oleh teller asal.');
if ($record->state === 'DONE') {
return redirect()->route('shoraka.show', ['norujukan' => $reference]);
}
abort_unless($record->state === 'WAITING', 409,
'Penyelesaian telah diproses atau memerlukan semakan operasi. Jangan ulang bayaran.');
$this->api()->post($this->url($reference, '/refresh'))->throw();
$snapshot = $this->api()->get($this->url($reference))->throw()->json()['data'];
abort_unless(!empty($snapshot['can_complete']), 409,
'Bayaran ditahan sehingga Shoraka Completed dan amaun sepadan.');
abort_unless(
number_format((float) ($record->context['pinjaman'] ?? 0), 2, '.', '')
=== number_format((float) ($snapshot['order_amount'] ?? 0), 2, '.', ''),
409, 'Amaun pembiayaan berubah. Semakan operasi diperlukan sebelum bayaran.'
);
$claimed = ShorakaFinancing::where('id', $record->id)->where('state', 'WAITING')
->update(['state' => 'PROCESSING', 'updated_at' => now()]);
abort_unless($claimed === 1, 409, 'Penyelesaian sedang diproses.');
$saved = new Request($record->context);
$saved->setLaravelSession($request->session());
try {
if ($record->action === 'gadai') {
$result = app(GadaianController::class)->completeShorakaFinancing($saved);
} else {
if (!empty($record->context['update_history'])) {
$this->api()->put(rtrim(config('api.url'), '/') . '/api/gadai/updatehistory/'
. rawurlencode($record->kodcaw) . '/' . rawurlencode($reference), [
'historygadaian' => $saved->norujukan,
'jenistebus' => $saved->jenistebus,
])->throw();
}
$cash = app(AliranTunaiController::class);
if ($record->action === 'autotawarruq') {
$cash->alirantunaiTebusAutoTawarruq($saved);
} else {
$cash->alirantunaiTebus($saved);
}
event(new \App\Events\Dashboard\TebusNotify('lulus', $record->kodcaw));
event(new GadaiNotify('lulus', $record->kodcaw));
$result = redirect()->route('shoraka.show', ['norujukan' => $reference]);
}
$record->update(['state' => 'DONE', 'context' => []]);
return $result;
} catch (Throwable $exception) {
// A failed remote payment response can be ambiguous. Never retry automatically.
$record->update(['state' => 'REVIEW']);
throw $exception;
}
}
public function printData(string $reference): ?array
{
$response = $this->api()->get($this->url($reference))->throw()->json()['data'];
$order = $response['order'] ?? null;
if (!$order) {
abort_if($this->local($reference), 409, 'SAG ditahan: transaksi Shoraka belum tersedia.');
return null; // Historical financing keeps its historical print path.
}
abort_unless(!empty($response['can_complete']), 409, 'SAG ditahan: Shoraka belum Completed.');
$details = $response['details'] ?? [];
$certificate = explode('|', $details['certificateDetails1'] ?? '')[0];
abort_unless($certificate !== '' && isset($details['unit'], $details['volume'], $details['commodityType']),
409, 'Maklumat sijil belum lengkap. Semak status Shoraka dahulu.');
return [
'komoditi' => ['no_sijil' => $certificate],
'product' => [
'kodproduk' => $details['commodityType'],
'description' => $details['commodityType'] === '000-COPPER' ? 'Copper' : 'Plastic resin',
'unit' => $details['unit'],
],
'kuantiti' => $details['volume'],
];
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ShorakaFinancing extends Model
{
protected $guarded = [];
protected $casts = ['context' => 'array'];
}
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateShorakaFinancingsTable extends Migration
{
public function up()
{
Schema::create('shoraka_financings', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('kodcaw', 20);
$table->string('norujukan');
$table->string('teller');
$table->string('action', 30);
$table->string('state', 30)->default('WAITING');
$table->json('context');
$table->timestamps();
$table->unique(['kodcaw', 'norujukan']);
});
}
public function down()
{
Schema::dropIfExists('shoraka_financings');
}
}
@@ -2814,25 +2814,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+7 -7
View File
@@ -461,25 +461,25 @@
}else{
$.ajax({
method:'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success:function(data){
var check_komuditi = Object.keys(data).length;
if(check_komuditi == 0){
if (!data.ready){
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
}else{
if(Number(data['unit_komuditi']) <= min_komoditi){
if (!data.ready){
$("#daftar_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+7 -7
View File
@@ -758,25 +758,25 @@
} else {
$.ajax({
method: 'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success: function(data) {
var check_komuditi = Object.keys(data).length;
if (check_komuditi == 0) {
if (!data.ready) {
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
} else {
if (Number(data['unit_komuditi']) <= min_komoditi) {
if (!data.ready) {
$("#daftar_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+7 -7
View File
@@ -819,25 +819,25 @@
}else{
$.ajax({
method:'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success:function(data){
var check_komuditi = Object.keys(data).length;
if(check_komuditi == 0){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(data['unit_komuditi']) < pinjaman){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+7 -7
View File
@@ -1655,25 +1655,25 @@
}else{
$.ajax({
method:'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success:function(data){
var check_komuditi = Object.keys(data).length;
if(check_komuditi == 0){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(data['unit_komuditi']) < pinjaman){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+1
View File
@@ -405,6 +405,7 @@
@endif
<main class="py-4">
@include('shoraka.navigation')
@yield('content')
</main>
</div>
+7 -7
View File
@@ -2022,25 +2022,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
@@ -2258,25 +2258,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+21 -21
View File
@@ -2264,27 +2264,27 @@
let komuditiblock = false;
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
async:false,
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
@@ -2646,29 +2646,29 @@
let komuditiblock = false;
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
async:false,
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.close();
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.close();
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
@@ -3106,25 +3106,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+8 -1
View File
@@ -556,9 +556,16 @@
</table>
@php
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/check')->json();
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komuditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/check')->json();
$product = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/description',['kodproduk'=>$komuditi['kodproduct']])->json();
$kuantiti = $gadai_detail['pinjaman']/$komuditi['nilai'];
}
@endphp
<table style="margin-bottom:10px;" id="keuntungan">
+8 -1
View File
@@ -654,13 +654,20 @@
@php
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komuditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/check')
->json();
$product = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/description', ['kodproduk' => $komuditi['kodproduct']])
->json();
$kuantiti = $gadai_detail['pinjaman'] / $komuditi['nilai'];
}
@endphp
<table style="margin-bottom:10px;" id="keuntungan">
+8 -1
View File
@@ -713,13 +713,20 @@
</tr>
</table>
@php
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komuditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/check')
->json();
$product = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/description', ['kodproduk' => $komuditi['kodproduct']])
->json();
$kuantiti = $gadai_detail['pinjaman'] / $komuditi['nilai'];
}
@endphp
<table id="keuntungan" style="margin-bottom:10px;">
+21
View File
@@ -0,0 +1,21 @@
@extends('layouts.app')
@section('content')
<div class="container py-3">
<h3>Pembiayaan Shoraka</h3>
<p>Semak transaksi komoditi dan selesaikan pembiayaan selepas status Completed.</p>
<table class="table table-bordered">
<thead><tr><th>No. rujukan</th><th>Teller</th><th>Penyelesaian pembiayaan</th><th></th></tr></thead>
<tbody>
@forelse($records as $record)
<tr>
<td>{{ $record->norujukan }}</td><td>{{ $record->teller }}</td>
<td>{{ ['WAITING' => 'Menunggu Shoraka / penyelesaian', 'PROCESSING' => 'Sedang diproses', 'REVIEW' => 'Perlu semakan operasi', 'DONE' => 'Selesai'][$record->state] ?? $record->state }}</td>
<td><a class="btn btn-primary" href="{{ route('shoraka.show', $record->norujukan) }}">Buka</a></td>
</tr>
@empty
<tr><td colspan="4">Tiada transaksi.</td></tr>
@endforelse
</tbody>
</table>
</div>
@endsection
@@ -0,0 +1,27 @@
@auth
@if(in_array(Auth::user()->roles, ['teller', 'penilai', 'pc', 'ppc', 'admin', 'masteradmin', 'operasi']) || Auth::user()->role_harian === 'teller')
<div class="container-fluid text-right py-1">
<a class="btn btn-sm btn-outline-primary" href="{{ route('shoraka.index') }}">Pembiayaan Shoraka</a>
</div>
@endif
@endauth
<script>
// Existing refinancing AJAX handlers must not print receipts or announce payment
// success when the server has deferred the financial completion.
$.ajaxPrefilter(function (options) {
var success = options.success;
options.success = function (data) {
if (data && data.shoraka_pending && data.redirect) {
window.location.assign(data.redirect);
return;
}
if (typeof success === 'function') {
return success.apply(this, arguments);
}
if (Array.isArray(success)) {
var args = arguments, context = this;
success.forEach(function (callback) { callback.apply(context, args); });
}
};
});
</script>
+57
View File
@@ -0,0 +1,57 @@
@extends('layouts.app')
@section('content')
<div class="container py-3">
<a href="{{ route('shoraka.index') }}">Kembali ke senarai</a>
<h3 class="mt-3">Komoditi Shoraka — {{ $norujukan }}</h3>
@if($error)<div class="alert alert-danger">{{ $error }}</div>@endif
@if(session('shoraka_message'))<div class="alert alert-info">{{ session('shoraka_message') }}</div>@endif
@php
$order = $data['order'] ?? null;
$status = $order['status'] ?? 'Belum dihantar';
$ready = !empty($data['can_complete']);
@endphp
<dl class="row">
<dt class="col-sm-4">Status komoditi</dt><dd class="col-sm-8">{{ $status }}</dd>
<dt class="col-sm-4">Order ID</dt><dd class="col-sm-8">{{ $order['order_id'] ?? '—' }}</dd>
<dt class="col-sm-4">Pembiayaan</dt><dd class="col-sm-8">RM {{ number_format($data['order_amount'] ?? 0, 2) }}</dd>
<dt class="col-sm-4">Harga jualan</dt><dd class="col-sm-8">RM {{ number_format($data['murabaha_amount'] ?? 0, 2) }}</dd>
</dl>
@if(!$ready)
<div class="alert alert-warning">Bayaran pembiayaan dan SAG ditahan sehingga transaksi komoditi Completed.</div>
@endif
@if(in_array($status, ['UNKNOWN', 'FAILED', 'PENDING']))
<p>Jangan hantar order sekali lagi. Hubungi operasi untuk semakan dengan Shoraka.</p>
@endif
@if($record && in_array($record->state, ['REVIEW', 'PROCESSING']))
<div class="alert alert-warning">Penyelesaian sedang diproses atau memerlukan semakan operasi. Jangan ulang bayaran.</div>
@elseif($record && $record->state === 'DONE')
<div class="alert alert-success">Pembiayaan telah diselesaikan.</div>
@endif
<div class="d-flex flex-wrap">
@if($order && !empty($order['order_id']))
<form class="mr-2 mb-2" method="post" action="{{ route('shoraka.refresh', $norujukan) }}">@csrf
<button class="btn btn-info" type="submit">Semak Status</button>
</form>
@elseif($data && !$order && $record && $record->teller === Auth::user()->name)
<form class="mr-2 mb-2" method="post" action="{{ route('shoraka.submit', $norujukan) }}">@csrf
<button class="btn btn-primary" type="submit">Hantar Order</button>
</form>
@endif
@if($ready && $record && $record->state === 'WAITING' && $record->teller === Auth::user()->name)
<form class="mr-2 mb-2" method="post" action="{{ route('shoraka.finish', $norujukan) }}">@csrf
<button class="btn btn-success" type="submit">Selesaikan Pembiayaan</button>
</form>
@endif
@if($ready)
<a class="btn btn-secondary mb-2" href="{{ route('shoraka.certificate', $norujukan) }}">Muat Turun Sijil</a>
@endif
</div>
</div>
<script>
document.querySelectorAll('form').forEach(function (form) {
form.addEventListener('submit', function () {
form.querySelectorAll('button[type="submit"]').forEach(function (button) { button.disabled = true; });
});
});
</script>
@endsection
+9 -1
View File
@@ -605,4 +605,12 @@ Route::post('/pampasan/terima',['as' => 'pampasan.terima', 'uses' => 'KhazanahCo
Route::get(('/pampasan/resit'), ['as' => 'pampasan.resit', 'uses' => 'KhazanahController@resitPampasan'])->middleware('auth', 'khazanah');
//Surat Pampasan
Route::get('/pampasan/surat/{norujukan}', ['as' => 'pampasan.surat', 'uses' => 'KhazanahController@suratPampasan'])->middleware('auth', 'role:teller,khazanah');
Route::get('/pampasan/surat/{norujukan}', ['as' => 'pampasan.surat', 'uses' => 'KhazanahController@suratPampasan'])->middleware('auth', 'role:teller,khazanah');
Route::group(['prefix' => 'shoraka', 'middleware' => ['auth', 'role:teller,penilai,khazanah,admin,masteradmin,operasi']], function () {
Route::get('/', 'ShorakaController@index')->name('shoraka.index');
Route::get('/{norujukan}', 'ShorakaController@show')->name('shoraka.show');
Route::post('/{norujukan}/submit', 'ShorakaController@submit')->name('shoraka.submit');
Route::post('/{norujukan}/refresh', 'ShorakaController@refresh')->name('shoraka.refresh');
Route::post('/{norujukan}/finish', 'ShorakaController@finish')->name('shoraka.finish');
Route::get('/{norujukan}/certificate', 'ShorakaController@certificate')->name('shoraka.certificate');
});
+238
View File
@@ -0,0 +1,238 @@
<?php
namespace Tests\Feature;
use App\ShorakaFinancing;
use App\User;
use App\Services\ShorakaFinancingService;
use App\Http\Controllers\GadaianController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Factory;
use Mockery;
use Tests\TestCase;
class ShorakaFinancingTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'database.default' => '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');
}
}