feat(shoraka): defer gadai persistence until commodity confirmation and add simulation support

This commit is contained in:
2026-10-19 13:02:13 +08:00
parent d0f08545d1
commit a8888e66d1
12 changed files with 429 additions and 30 deletions
+47 -12
View File
@@ -55,6 +55,10 @@ class GadaiController extends Controller
$cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$cawangan_code)->first();
$gadaiTransaction = Gadai::on($cawangan['mysql'])->where('t_gadai','=',$date)->orderBy('sirig', 'desc')->first();
$serial = app(\App\Services\ShorakaNewGadaiService::class)->latestSerial($cawangan_code, $date);
if ($serial > (int) ($gadaiTransaction->sirig ?? 0)) {
$gadaiTransaction = ['sirig' => $serial];
}
}catch(\Exception $e){
Log::debug($e);
Log::debug($cawangan_code);
@@ -111,7 +115,7 @@ class GadaiController extends Controller
$gadai_all = [];
foreach($cawangan_all as $index=>$cawangan){
$gadai = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get();
$gadai = collect(array_filter([app(\App\Services\ShorakaNewGadaiService::class)->find($cawangan->kodcaw, $norujukan)]));
if(sizeof($gadai) != 0){
array_push($gadai_all,$gadai);
}
@@ -130,7 +134,7 @@ class GadaiController extends Controller
$gadai_all = [];
foreach($cawangan_all as $index=>$cawangan){
$gadai = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get();
$gadai = collect(array_filter([app(\App\Services\ShorakaNewGadaiService::class)->find($cawangan->kodcaw, $norujukan)]));
if(sizeof($gadai) != 0){
array_push($gadai_all,$gadai);
}
@@ -152,13 +156,22 @@ class GadaiController extends Controller
public function showOneGadaibyRujukanAndCawangan($kodcaw,$norujukan){
$cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first();
$gadai = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get();
$gadai = collect(array_filter([app(\App\Services\ShorakaNewGadaiService::class)->find($cawangan->kodcaw, $norujukan)]));
return response()->json($gadai);
}
public function create($kodcaw,Request $request){
if ($request->boolean('shoraka_new')) {
$this->validate($request, ['nokp' => 'required|string', 'nopelanggan' => 'required']);
return response()->json(app(\App\Services\ShorakaNewGadaiService::class)->reserve(
$kodcaw, $request->only(['nokp', 'nopelanggan'])
), 201);
}
if (app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, (string) $request->norujukan)) {
abort(409, 'Nombor rujukan sedang digunakan. Sila dapatkan nombor baharu.');
}
$master = new MasterServices($kodcaw);
$response = $master->createGadai($request);
@@ -228,14 +241,18 @@ class GadaiController extends Controller
}
public function daftarGadaiMarhun($norujukan,Request $request){
if (\App\ShorakaTransaction::where('norujukan', $request->norujukan)->exists()) {
public function daftarGadaiMarhun($kodcaw, $norujukan, Request $request)
{
if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()) {
abort(409, 'Kontrak telah dihantar ke Shoraka dan tidak boleh diubah.');
}
// dd("ggwp");
$master = new MasterServices($request->kodcaw);
$response = $master->createMarhunGadai($request->norujukan,$request);
$temporary = app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan);
if ($temporary && (string) $temporary['gadai']['nokp'] !== (string) $request->nokp) {
abort(409, 'Pelanggan tidak sepadan dengan borang gadaian sementara.');
}
$request->merge(['kodcaw' => $kodcaw, 'norujukan' => $norujukan]);
$master = new MasterServices($kodcaw);
$response = $master->createMarhunGadai($norujukan, $request);
return response()->json($response, 200);
}
@@ -249,7 +266,13 @@ class GadaiController extends Controller
public function checkNorujukan(Request $request){
$cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$request->kodcaw)->first();
$gadai = Gadai::on($cawangan['mysql'])->where([['norujukan','=',$request->norujukan],['nokp','=',$request->nokp]])->first();
$gadai = app(\App\Services\ShorakaNewGadaiService::class)->find($request->kodcaw, $request->norujukan);
if (!$gadai && \App\ShorakaTransaction::where('norujukan', $request->norujukan)->exists()) {
abort(409, 'Transaksi Shoraka wujud tetapi maklumat sementara tiada. Hubungi operasi; jangan daftar semula.');
}
if ($gadai && $gadai->nokp !== $request->nokp) {
$gadai = null;
}
return response()->json($gadai);
}
@@ -372,6 +395,16 @@ class GadaiController extends Controller
}
public function batalGadai($kodcaw, $norujukan,Request $request){
$temporary = app(\App\Services\ShorakaNewGadaiService::class);
$order = \App\ShorakaTransaction::where('norujukan', $norujukan)->first();
if ($order && !empty($order->request_payload['_new_gadai'])) {
abort(409, 'Transaksi Shoraka telah dihantar. Semakan operasi diperlukan sebelum pembatalan.');
}
if ($temporary->pending($kodcaw, $norujukan)) {
$temporary->abandon($kodcaw, $norujukan);
return response('Batal Berjaya', 200);
}
$current = Carbon::now();
$current = new Carbon();
@@ -426,7 +459,8 @@ class GadaiController extends Controller
}
public function cetakSAG($kodcaw,$norujukan,Request $request){
if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()) {
if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()
|| app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan)) {
app(\App\Services\ShorakaGadaiService::class)->assertCompleted($kodcaw, $norujukan);
}
@@ -454,7 +488,8 @@ class GadaiController extends Controller
}
public function serahMarhun($kodcaw,$norujukan){
if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()) {
if (\App\ShorakaTransaction::where('norujukan', $norujukan)->exists()
|| app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan)) {
app(\App\Services\ShorakaGadaiService::class)->assertCompleted($kodcaw, $norujukan);
}
+50
View File
@@ -26,6 +26,9 @@ class MarhunController extends Controller
$marhun_all = [];
foreach($cawangan_all as $index=>$cawangan){
$marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->get();
if ($marhun->isEmpty()) {
$marhun = app(\App\Services\ShorakaNewGadaiService::class)->items($cawangan->kodcaw, $norujukan);
}
if(sizeof($marhun) != 0){
array_push($marhun_all,$marhun);
}
@@ -40,6 +43,9 @@ class MarhunController extends Controller
$marhun = Marhun::on($cawangan['mysql'])->where('norujukan',$request->norujukan)->distinct()->pluck('karat')->toArray();
if (app(\App\Services\ShorakaNewGadaiService::class)->pending($request->kodcaw, $request->norujukan)) {
$marhun = app(\App\Services\ShorakaNewGadaiService::class)->items($request->kodcaw, $request->norujukan)->pluck('karat')->unique()->all();
}
$mutu = HargaEmas::on('mysql7')->whereIn('karat',$marhun)->pluck('keterangan')->toArray();
return response()->json($mutu);
@@ -59,6 +65,11 @@ class MarhunController extends Controller
public function create($kodcaw,Request $request)
{
$order = \App\ShorakaTransaction::where('norujukan', $request->norujukan)->first();
if ($order && !empty($order->request_payload['_new_gadai'])) {
abort(409, 'Kontrak Shoraka telah dihantar dan tidak boleh diubah.');
}
$current = Carbon::now();
$current = new Carbon();
@@ -96,6 +107,10 @@ class MarhunController extends Controller
};
$cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first();
$temporary = app(\App\Services\ShorakaNewGadaiService::class);
if ($temporary->pending($kodcaw, $request->norujukan)) {
return response()->json($temporary->changeItem($kodcaw, $request->norujukan, $marhunToCreate), 201);
}
$marhun = Marhun::on($cawangan['mysql'])->create($marhunToCreate);
$audit = new Audit();
@@ -127,6 +142,17 @@ class MarhunController extends Controller
public function deletemarhun($kodcaw,$norujukan,Request $request)
{
$order = \App\ShorakaTransaction::where('norujukan', $norujukan)->first();
if ($order && !empty($order->request_payload['_new_gadai'])) {
abort(409, 'Kontrak Shoraka telah dihantar dan tidak boleh diubah.');
}
$temporary = app(\App\Services\ShorakaNewGadaiService::class);
if ($temporary->pending($kodcaw, $norujukan)) {
$temporary->changeItem($kodcaw, $norujukan, [], $request->recno, true);
return response('Deleted Successfully', 200);
}
$current = Carbon::now();
$current = new Carbon();
@@ -152,6 +178,11 @@ class MarhunController extends Controller
public function editmarhun($kodcaw,$norujukan,Request $request)
{
$order = \App\ShorakaTransaction::where('norujukan', $norujukan)->first();
if ($order && !empty($order->request_payload['_new_gadai'])) {
abort(409, 'Kontrak Shoraka telah dihantar dan tidak boleh diubah.');
}
$current = Carbon::now();
$current = new Carbon();
@@ -159,6 +190,17 @@ class MarhunController extends Controller
$harga = HargaEmas::on('mysql7')->where('karat',$request->karat)->first();
$nilai_marhun = $request->berat * $harga['harga'];
$temporary = app(\App\Services\ShorakaNewGadaiService::class);
if ($temporary->pending($kodcaw, $norujukan)) {
$temporary->changeItem($kodcaw, $norujukan, [
'karat' => $request->karat, 'marhun' => $request->marhun, 'nota' => $request->nota,
'tag_permata' => $request->tag_permata,
'berat_batu_permata' => $request->berat_batu_permata ?: null,
'berat' => $request->berat, 'harga' => $harga['harga'], 'n_marhun' => $nilai_marhun,
'bil' => $request->kuantiti,
], $request->recno);
return response()->json(true);
}
$cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first();
// Lock the parent first so concurrent edits to this SAG are serialized.
@@ -257,6 +299,9 @@ class MarhunController extends Controller
$marhuns = Marhun::on($cawangan['mysql'])->where('norujukan',$request->norujukan)->get();
if ($marhuns->isEmpty()) {
$marhuns = app(\App\Services\ShorakaNewGadaiService::class)->items($request->kodcaw, $request->norujukan);
}
$totalnilaimarhun = 0;
foreach($marhuns as $index=>$marhun){
@@ -270,6 +315,11 @@ class MarhunController extends Controller
public function getMarhunCaw($kodcaw,$norujukan)
{
$temporary = app(\App\Services\ShorakaNewGadaiService::class);
if ($temporary->pending($kodcaw, $norujukan)) {
return response()->json($temporary->items($kodcaw, $norujukan)->sum('n_marhun'));
}
$cawangan = Cawangan::on('mysql7')->where('kodcaw','=',$kodcaw)->first();
$data = Marhun::on($cawangan['mysql'])
->where('norujukan','=', $norujukan)
+9 -3
View File
@@ -48,7 +48,7 @@ class ShorakaController extends Controller
try {
// The unique norujukan index is the atomic claim for concurrent submissions.
$transaction = ShorakaTransaction::create([
$attributes = [
'norujukan' => $validated['norujukan'],
'kodcaw' => $request->attributes->get('shoraka_kodcaw'),
'status' => 'PENDING',
@@ -58,9 +58,15 @@ class ShorakaController extends Controller
'ownership_doc_no' => $payload['ownership_doc_no'],
'order_amount' => $payload['order_amount'],
'murabaha_amount' => $payload['murabaha_amount'],
'request_payload' => $payload,
'request_payload' => $request->attributes->get('shoraka_new')
? array_merge($payload, ['_new_gadai' => true]) : $payload,
'attempt_count' => 1,
]);
];
$create = function () use ($attributes) { return ShorakaTransaction::create($attributes); };
$transaction = $request->attributes->get('shoraka_new')
? app(\App\Services\ShorakaNewGadaiService::class)->claim(
$request->attributes->get('shoraka_kodcaw'), $validated['norujukan'], $payload, $create
) : $create();
} catch (QueryException $exception) {
$existing = ShorakaTransaction::where('norujukan', $validated['norujukan'])->first();
if ($existing) {
@@ -25,7 +25,8 @@ class ShorakaGadaiController extends ShorakaController
'order_amount' => $gadai->pinjaman, 'murabaha_amount' => $gadai->hargajualan,
'order' => $order ? $this->orderData($order) : null,
'details' => $order ? $order->response_payload : null,
'can_complete' => $order && $order->status === 'Completed'
'simulation' => $order && strpos((string) $order->order_id, 'SIM-') === 0,
'can_complete' => $gadai->exists && $order && $order->status === 'Completed'
&& number_format((float) $gadai->pinjaman, 2, '.', '') === $order->order_amount
&& number_format((float) $gadai->hargajualan, 2, '.', '') === $order->murabaha_amount,
],
@@ -44,8 +45,16 @@ class ShorakaGadaiController extends ShorakaController
|| $gadai->cetak || $gadai->sttebus) {
abort(409, 'Gadaian lama atau telah selesai tidak boleh dihantar sebagai order baharu.');
}
$temporary = app(\App\Services\ShorakaNewGadaiService::class)->pending($kodcaw, $norujukan);
if ($temporary && $gadai->exists) {
abort(409, 'Rujukan sementara bertindih dengan gadaian sedia ada. Semakan operasi diperlukan.');
}
if ($temporary && !$temporary['prepared']) {
abort(409, 'Sila sahkan semula amaun gadaian selepas perubahan marhun.');
}
$request = new Request($gadaiService->payload($gadai));
$request->attributes->set('shoraka_kodcaw', $kodcaw);
$request->attributes->set('shoraka_new', !$gadai->exists);
return $this->submit($request, $shoraka);
}
+7 -2
View File
@@ -13,7 +13,8 @@ class ShorakaGadaiService
public function gadai(string $kodcaw, string $norujukan): Gadai
{
$branch = Cawangan::on('mysql7')->where('kodcaw', $kodcaw)->firstOrFail();
return Gadai::on($branch->mysql)->where('norujukan', $norujukan)->where('kodcaw', $kodcaw)->firstOrFail();
return app(ShorakaNewGadaiService::class)->find($kodcaw, $norujukan)
?? abort(404, 'Maklumat gadaian tidak tersedia. Semakan diperlukan.');
}
public function payload(Gadai $gadai): array
@@ -52,7 +53,11 @@ class ShorakaGadaiService
if (!$order->kodcaw) {
return;
}
app(ShorakaNewGadaiService::class)->promote($order);
$gadai = $this->gadai($order->kodcaw, $order->norujukan);
if (!$gadai->exists) {
return;
}
$sameAmounts = number_format((float) $gadai->pinjaman, 2, '.', '') === $order->order_amount
&& number_format((float) $gadai->hargajualan, 2, '.', '') === $order->murabaha_amount;
$gadai->getConnection()->table('gadai')->where('norujukan', $order->norujukan)->update([
@@ -64,7 +69,7 @@ class ShorakaGadaiService
{
$gadai = $this->gadai($kodcaw, $norujukan);
$order = $this->transaction($kodcaw, $norujukan);
if (!$order || $order->status !== 'Completed'
if (!$gadai->exists || !$order || $order->status !== 'Completed'
|| number_format((float) $gadai->pinjaman, 2, '.', '') !== $order->order_amount
|| number_format((float) $gadai->hargajualan, 2, '.', '') !== $order->murabaha_amount) {
abort(409, 'Pembiayaan dan SAG ditahan sehingga transaksi Shoraka selesai dan amaun sepadan.');
+223
View File
@@ -0,0 +1,223 @@
<?php
namespace App\Services;
use App\Cawangan;
use App\Gadai;
use App\ShorakaTransaction;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
/** Temporary new financing only. Existing/refinancing contracts retain their database path. */
class ShorakaNewGadaiService
{
const TTL = 604800; // Seven days; loss/expiry requires reconciliation, never resubmission.
private function cache()
{
return Cache::store(config('services.shoraka.pending_cache_store', 'file'));
}
private function key(string $branch, string $reference): string
{
return 'shoraka:new:' . hash('sha256', $branch . ':' . $reference);
}
public function pending(string $branch, string $reference): ?array
{
return $this->cache()->get($this->key($branch, $reference));
}
private function put(string $branch, string $reference, array $data): void
{
if (!$this->cache()->put($this->key($branch, $reference), $data, self::TTL)) {
abort(503, 'Maklumat sementara tidak dapat disimpan.');
}
}
// File cache and these locks must be shared by every API worker serving a branch.
public function locked(string $key, callable $callback)
{
$directory = storage_path('framework/cache/shoraka-locks');
if (!is_dir($directory) && !@mkdir($directory, 0700, true) && !is_dir($directory)) {
abort(503, 'Kunci transaksi tidak tersedia.');
}
$handle = fopen($directory . '/' . hash('sha256', $key) . '.lock', 'c');
if (!$handle) {
abort(503, 'Kunci transaksi tidak tersedia.');
}
if (!flock($handle, LOCK_EX | LOCK_NB)) {
fclose($handle);
abort(409, 'Transaksi sedang diproses. Sila tunggu.');
}
try {
return $callback();
} finally {
flock($handle, LOCK_UN);
fclose($handle);
}
}
public function reserve(string $branch, array $customer): Gadai
{
return $this->locked('reserve:' . $branch, function () use ($branch, $customer) {
$cawangan = Cawangan::on('mysql7')->where('kodcaw', $branch)->firstOrFail();
$date = Carbon::now()->toDateString();
$counter = 'shoraka:serial:' . $branch . ':' . $date;
$latest = Gadai::on($cawangan->mysql)->where('t_gadai', $date)->max('sirig');
$serial = max((int) $latest, (int) $this->cache()->get($counter, 0));
do {
++$serial;
$reference = strtoupper($branch) . Carbon::now()->format('ymd') . '-' . sprintf('%04d', $serial);
} while (ShorakaTransaction::where('norujukan', $reference)->exists()
|| Gadai::on($cawangan->mysql)->where('norujukan', $reference)->exists()
|| $this->pending($branch, $reference));
if (!$this->cache()->forever($counter, $serial)) {
abort(503, 'Nombor rujukan tidak dapat ditempah.');
}
$data = array_merge($customer, [
'kodcaw' => $branch, 'norujukan' => $reference, 'sirig' => $serial,
't_gadai' => $date, 'masa' => Carbon::now()->toTimeString(), 'sttebus' => '',
'cara_terima' => 'TUNAI', 'cetak' => 0, 'tagserah' => 0, 'tagkomuditi' => 0,
]);
if ($cawangan->version === 'v2') {
$data['tagbaru'] = 1;
}
$this->put($branch, $reference, ['gadai' => $data, 'items' => [], 'next_item' => 1, 'revision' => 0, 'prepared' => false]);
return $this->find($branch, $reference);
});
}
public function abandon(string $branch, string $reference): void
{
$this->locked($branch . ':' . $reference, function () use ($branch, $reference) {
$this->mutable($branch, $reference);
$this->cache()->forget($this->key($branch, $reference));
});
}
public function latestSerial(string $branch, string $date): int
{
return (int) $this->cache()->get('shoraka:serial:' . $branch . ':' . $date, 0);
}
public function claim(string $branch, string $reference, array $payload, callable $create)
{
return $this->locked($branch . ':' . $reference, function () use ($branch, $reference, $payload, $create) {
$pending = $this->mutable($branch, $reference);
if (!$pending['prepared']
|| number_format((float) ($pending['gadai']['pinjaman'] ?? 0), 2, '.', '') !== $payload['order_amount']
|| number_format((float) ($pending['gadai']['hargajualan'] ?? 0), 2, '.', '') !== $payload['murabaha_amount']) {
abort(409, 'Maklumat gadaian berubah. Sila sahkan semula sebelum penghantaran.');
}
return $create(); // The unique transaction reference freezes further edits.
});
}
public function find(string $branch, string $reference)
{
$cawangan = Cawangan::on('mysql7')->where('kodcaw', $branch)->firstOrFail();
$saved = Gadai::on($cawangan->mysql)->where('kodcaw', $branch)->where('norujukan', $reference)->first();
if ($saved) {
return $saved;
}
$pending = $this->pending($branch, $reference);
if (!$pending) {
return null;
}
$model = (new Gadai())->setConnection($cawangan->mysql);
$model->setRawAttributes($pending['gadai']);
return $model;
}
private function mutable(string $branch, string $reference): array
{
if (ShorakaTransaction::where('norujukan', $reference)->exists()) {
abort(409, 'Kontrak telah dihantar ke Shoraka dan tidak boleh diubah.');
}
$pending = $this->pending($branch, $reference);
if (!$pending) {
abort(409, 'Maklumat sementara tamat. Semakan diperlukan.');
}
return $pending;
}
public function prepare(string $branch, string $reference, array $fields, int $revision): void
{
$this->locked($branch . ':' . $reference, function () use ($branch, $reference, $fields, $revision) {
$pending = $this->mutable($branch, $reference);
if (($pending['revision'] ?? 0) !== $revision) {
abort(409, 'Maklumat marhun berubah semasa pengiraan. Sila sahkan semula.');
}
$pending['gadai'] = array_merge($pending['gadai'], $fields);
$pending['prepared'] = true;
$this->put($branch, $reference, $pending);
});
}
public function items(string $branch, string $reference)
{
return collect(array_values($this->pending($branch, $reference)['items'] ?? []));
}
public function changeItem(string $branch, string $reference, array $fields, $id = null, bool $delete = false)
{
return $this->locked($branch . ':' . $reference, function () use ($branch, $reference, $fields, $id, $delete) {
$pending = $this->mutable($branch, $reference);
if ($id !== null && !isset($pending['items'][$id])) {
abort(404, 'Marhun tidak dijumpai.');
}
if ($id === null) {
$id = $pending['next_item']++;
}
$item = array_merge($pending['items'][$id] ?? [], $fields, ['recno' => (int) $id]);
if ($delete) {
unset($pending['items'][$id]);
} else {
$pending['items'][$id] = $item;
}
$pending['revision'] = ($pending['revision'] ?? 0) + 1;
$pending['prepared'] = false; // Reprice the contract after any collateral edit.
$this->put($branch, $reference, $pending);
return $item;
});
}
public function promote(ShorakaTransaction $order): void
{
if ($order->status !== 'Completed' || !$this->pending($order->kodcaw, $order->norujukan)) {
return;
}
$this->locked($order->kodcaw . ':' . $order->norujukan, function () use ($order) {
$pending = $this->pending($order->kodcaw, $order->norujukan);
$data = $pending['gadai'];
if (!$pending['prepared']
|| number_format((float) ($data['pinjaman'] ?? 0), 2, '.', '') !== $order->order_amount
|| number_format((float) ($data['hargajualan'] ?? 0), 2, '.', '') !== $order->murabaha_amount) {
abort(409, 'Amaun gadaian tidak sepadan dengan Shoraka.');
}
$branch = Cawangan::on('mysql7')->where('kodcaw', $order->kodcaw)->firstOrFail();
$connection = (new Gadai())->setConnection($branch->mysql)->getConnection();
$connection->transaction(function () use ($connection, $pending, $data, $order) {
$existing = $connection->table('gadai')->where('norujukan', $order->norujukan)->lockForUpdate()->first();
if ($existing) {
if ($existing->kodcaw !== $order->kodcaw
|| number_format((float) $existing->pinjaman, 2, '.', '') !== $order->order_amount
|| number_format((float) $existing->hargajualan, 2, '.', '') !== $order->murabaha_amount) {
abort(409, 'Rujukan gadaian bertindih. Semakan diperlukan.');
}
return;
}
$connection->table('gadai')->insert(array_merge($data, ['tagkomuditi' => 1]));
foreach ($pending['items'] as $item) {
unset($item['recno']);
$connection->table('marhun')->insert($item);
}
});
\App\Audit::firstOrCreate([
'kodcaw' => $order->kodcaw, 'norujukan' => $order->norujukan, 'actions' => 'Daftar Gadai',
], ['users' => $data['teller'] ?? '', 'new_data' => json_encode($data)]);
// Retain the cache briefly for idempotent reconciliation after a lost response.
});
}
}
+54
View File
@@ -32,11 +32,50 @@ class ShorakaService
public function configured(): bool
{
if ($this->simulationRequested()) {
return $this->simulationAllowed() && in_array($this->simulationMode(), ['failed', 'pending'], true);
}
return $this->apiId !== '' && $this->secretKey !== '';
}
protected function simulationAllowed(): bool
{
return app()->environment('local', 'testing');
}
protected function simulationMode(): string
{
return strtolower(trim((string) config('services.shoraka.simulation', 'off')));
}
protected function simulationRequested(): bool
{
return !in_array($this->simulationMode(), ['', 'off'], true);
}
private function simulatedResponse(string $orderId): array
{
if (!$this->simulationAllowed()) {
throw new RuntimeException('Shoraka simulation is restricted to local/testing.');
}
if (strpos($orderId, 'SIM-FAILED-') === 0) {
return ['orderId' => $orderId, 'status' => 'Cancelled'];
}
if (strpos($orderId, 'SIM-PENDING-') === 0) {
return ['orderId' => $orderId, 'status' => 'Active'];
}
throw new RuntimeException('Invalid simulation order ID.');
}
public function submitOrder(array $data): array
{
if ($this->simulationRequested()) {
if (!$this->simulationAllowed() || !in_array($this->simulationMode(), ['failed', 'pending'], true)) {
throw new RuntimeException('Shoraka simulation configuration is not allowed.');
}
return $this->simulatedResponse('SIM-' . strtoupper($this->simulationMode()) . '-' . bin2hex(random_bytes(12)));
}
$payload = ['api_id' => $this->apiId];
foreach ([
'product_type', 'commodity_type', 'ownership', 'value_date',
@@ -61,6 +100,13 @@ class ShorakaService
public function orderStatus(string $orderId): array
{
if (strpos($orderId, 'SIM-') === 0) {
return $this->simulatedResponse($orderId); // Never send simulated references to the real provider.
}
if ($this->simulationRequested()) {
throw new RuntimeException('Real order status is unavailable while simulation is enabled.');
}
return $this->orderResponse($this->request()->post(
$this->baseUrl . '/api/orderstatus', $this->orderPayload($orderId)
), $orderId);
@@ -68,6 +114,10 @@ class ShorakaService
public function certificate(string $orderId): string
{
if ($this->simulationRequested() || strpos($orderId, 'SIM-') === 0) {
throw new RuntimeException('Certificates are unavailable for simulation.');
}
// Section 5 specifies POST, despite GET in the document history.
$response = $this->request()->accept('application/pdf')->post(
$this->baseUrl . '/api/certificate', $this->orderPayload($orderId)
@@ -83,6 +133,10 @@ class ShorakaService
public function validCallback(array $payload): bool
{
if ($this->simulationRequested() || strpos((string) ($payload['orderId'] ?? ''), 'SIM-') === 0) {
return false;
}
if (!$this->configured()
|| !isset($payload['apiId'], $payload['signature'])
|| !is_string($payload['apiId'])
+13 -7
View File
@@ -73,10 +73,13 @@ class GadaiV1
$old_data = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->first();
}
$bil_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->count();
$temporary = app(\App\Services\ShorakaNewGadaiService::class);
$temporaryData = $temporary->pending($request->kodcaw, $norujukan);
$isTemporary = $temporaryData !== null;
$bil_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->count() : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->count());
$total_nilai_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun');
$total_berat = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat');
$total_nilai_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('n_marhun') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun'));
$total_berat = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('berat') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat'));
$kadar_raw = KadarUpah::where('min','<=',$total_nilai_marhun)->where('max','>=',$total_nilai_marhun)->first();
$tawarruqservices = new Calculation();
@@ -88,9 +91,7 @@ class GadaiV1
$hargajualan = $request->pinjaman + $upah12;
$gadai = Gadai::on($cawangan['mysql'])
->where('norujukan','=',$norujukan)
->update(array(
$gadaiFields = array(
'nilaimarhun' => $total_nilai_marhun,
'berat'=> $total_berat,
'pinjaman' => $request->pinjaman,
@@ -115,7 +116,12 @@ class GadaiV1
'upahdibayar' => 0.00,
'tempohbayar' => 0.00,
'lelong_tawarruq' => 1
));
);
if ($isTemporary) {
$temporary->prepare($request->kodcaw, $norujukan, $gadaiFields, $temporaryData['revision'] ?? 0);
return response()->json(true);
}
$gadai = Gadai::on($cawangan['mysql'])->where('norujukan', $norujukan)->update($gadaiFields);
$new_data = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->first();
+2 -1
View File
@@ -946,7 +946,8 @@ class Penebusan
$gadaiTransaction = Gadai::on($cawangan['mysql'])->where('t_gadai','=',$current->toDateString())->orderBy('sirig', 'desc')->first();
$gadaiTransaction = optional($gadaiTransaction)->toArray();
$new_sirig = ($gadaiTransaction) ? $gadaiTransaction['sirig'] + 1 : 1;
$new_sirig = max((int) ($gadaiTransaction['sirig'] ?? 0),
app(\App\Services\ShorakaNewGadaiService::class)->latestSerial($kodcaw, $current->toDateString())) + 1;
$curent_year = substr($current->year,2,2);
$curent_month = sprintf("%02d", $current->month);
$current_day = sprintf("%02d", $current->day);
+10 -3
View File
@@ -107,10 +107,13 @@ class GadaiV2
$old_data = Gadai::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->first();
}
$bil_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('bil');
$temporary = app(\App\Services\ShorakaNewGadaiService::class);
$temporaryData = $temporary->pending($request->kodcaw, $norujukan);
$isTemporary = $temporaryData !== null;
$bil_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('bil') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('bil'));
$total_nilai_marhun = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun');
$total_berat = Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat');
$total_nilai_marhun = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('n_marhun') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('n_marhun'));
$total_berat = ($isTemporary ? $temporary->items($request->kodcaw, $norujukan)->sum('berat') : Marhun::on($cawangan['mysql'])->where('norujukan','=',$norujukan)->sum('berat'));
$kadar_raw = $this->getKadarUpahAnggota($request->nokp, $total_nilai_marhun, $cawangan['group_id']);
@@ -190,6 +193,10 @@ if (is_null($kadar_raw)) {
}
}
if ($isTemporary) {
$temporary->prepare($request->kodcaw, $norujukan, $gadaiFields, $temporaryData['revision'] ?? 0);
return 'true';
}
$gadai = Gadai::on($cawangan['mysql'])
->where('norujukan','=',$norujukan)
->update($gadaiFields);
+2 -1
View File
@@ -1224,7 +1224,8 @@ class Penebusan
$gadaiTransaction = Gadai::on($cawangan['mysql'])->where('t_gadai','=',$current->toDateString())->orderBy('sirig', 'desc')->first();
$gadaiTransaction = optional($gadaiTransaction)->toArray();
$new_sirig = ($gadaiTransaction) ? $gadaiTransaction['sirig'] + 1 : 1;
$new_sirig = max((int) ($gadaiTransaction['sirig'] ?? 0),
app(\App\Services\ShorakaNewGadaiService::class)->latestSerial($kodcaw, $current->toDateString())) + 1;
$curent_year = substr($current->year,2,2);
$curent_month = sprintf("%02d", $current->month);
$current_day = sprintf("%02d", $current->day);
+2
View File
@@ -3,6 +3,8 @@
return [
'shoraka' => [
// Local UI tests only: off, failed or pending. Success is deliberately excluded.
'simulation' => env('SHORAKA_SIMULATION', 'off'),
'base_url' => env(
'SHORAKA_BASE_URL',
'https://uat2.shorakagroup.com'