DONE: drop table shoraka financings, and improve UI checking on shoraka order status
This commit is contained in:
@@ -12,6 +12,61 @@ use Throwable;
|
|||||||
|
|
||||||
class ShorakaController extends Controller
|
class ShorakaController extends Controller
|
||||||
{
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'kodcaw' => 'nullable|string|max:30',
|
||||||
|
'norujukan' => 'nullable|string|max:100',
|
||||||
|
'status' => 'nullable|string|max:30',
|
||||||
|
'date_from' => 'nullable|date_format:Y-m-d',
|
||||||
|
'date_to' => 'nullable|date_format:Y-m-d',
|
||||||
|
'per_page' => 'nullable|integer|min:1|max:100',
|
||||||
|
'page' => 'nullable|integer|min:1',
|
||||||
|
'branch_scope' => 'nullable|string|max:30',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$query = ShorakaTransaction::query();
|
||||||
|
foreach (['kodcaw', 'norujukan', 'status'] as $field) {
|
||||||
|
if ($request->filled($field)) {
|
||||||
|
$query->where($field, $request->input($field));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($request->filled('date_from')) {
|
||||||
|
$query->where('created_at', '>=', $request->input('date_from') . ' 00:00:00');
|
||||||
|
}
|
||||||
|
if ($request->filled('date_to')) {
|
||||||
|
$query->where('created_at', '<', Carbon::parse($request->input('date_to'))->addDay()->format('Y-m-d') . ' 00:00:00');
|
||||||
|
}
|
||||||
|
$records = $query->orderByDesc('created_at')->orderByDesc('id')
|
||||||
|
->paginate(min(100, max(1, (int) $request->input('per_page', 25))));
|
||||||
|
$payload = [
|
||||||
|
'data' => $records->getCollection()->map(function (ShorakaTransaction $record) {
|
||||||
|
return [
|
||||||
|
'id' => $record->id,
|
||||||
|
'kodcaw' => $record->kodcaw,
|
||||||
|
'norujukan' => $record->norujukan,
|
||||||
|
'teller' => $record->teller,
|
||||||
|
'status' => $record->status,
|
||||||
|
'order_id' => $record->order_id,
|
||||||
|
'created_at' => optional($record->created_at)->toDateTimeString(),
|
||||||
|
'updated_at' => optional($record->updated_at)->toDateTimeString(),
|
||||||
|
];
|
||||||
|
})->values(),
|
||||||
|
'total' => $records->total(),
|
||||||
|
'current_page' => $records->currentPage(),
|
||||||
|
'per_page' => $records->perPage(),
|
||||||
|
'last_page' => $records->lastPage(),
|
||||||
|
];
|
||||||
|
if ($request->input('with_branches')) {
|
||||||
|
$branches = ShorakaTransaction::query()->whereNotNull('kodcaw');
|
||||||
|
if ($request->filled('branch_scope')) {
|
||||||
|
$branches->where('kodcaw', $request->input('branch_scope'));
|
||||||
|
}
|
||||||
|
$payload['branches'] = $branches->distinct()->orderBy('kodcaw')->pluck('kodcaw')->values();
|
||||||
|
}
|
||||||
|
return response()->json($payload);
|
||||||
|
}
|
||||||
|
|
||||||
public function submit(Request $request, ShorakaService $shoraka)
|
public function submit(Request $request, ShorakaService $shoraka)
|
||||||
{
|
{
|
||||||
$validated = $this->validate($request, [
|
$validated = $this->validate($request, [
|
||||||
@@ -20,6 +75,7 @@ class ShorakaController extends Controller
|
|||||||
'order_amount' => 'required|numeric|min:0.01|max:9999999999999.99',
|
'order_amount' => 'required|numeric|min:0.01|max:9999999999999.99',
|
||||||
'murabaha_amount' => 'required|numeric|min:0.01|max:9999999999999.99',
|
'murabaha_amount' => 'required|numeric|min:0.01|max:9999999999999.99',
|
||||||
'tenor_other' => 'required|integer|min:1',
|
'tenor_other' => 'required|integer|min:1',
|
||||||
|
'teller' => 'nullable|string|max:255',
|
||||||
]);
|
]);
|
||||||
if (!$shoraka->configured()) {
|
if (!$shoraka->configured()) {
|
||||||
return $this->unconfigured();
|
return $this->unconfigured();
|
||||||
@@ -51,6 +107,7 @@ class ShorakaController extends Controller
|
|||||||
$attributes = [
|
$attributes = [
|
||||||
'norujukan' => $validated['norujukan'],
|
'norujukan' => $validated['norujukan'],
|
||||||
'kodcaw' => $request->attributes->get('shoraka_kodcaw'),
|
'kodcaw' => $request->attributes->get('shoraka_kodcaw'),
|
||||||
|
'teller' => $validated['teller'] ?? null,
|
||||||
'status' => 'PENDING',
|
'status' => 'PENDING',
|
||||||
'product_type' => $payload['product_type'],
|
'product_type' => $payload['product_type'],
|
||||||
'commodity_type' => $payload['commodity_type'],
|
'commodity_type' => $payload['commodity_type'],
|
||||||
@@ -185,6 +242,7 @@ class ShorakaController extends Controller
|
|||||||
return [
|
return [
|
||||||
'norujukan' => $transaction->norujukan,
|
'norujukan' => $transaction->norujukan,
|
||||||
'kodcaw' => $transaction->kodcaw,
|
'kodcaw' => $transaction->kodcaw,
|
||||||
|
'teller' => $transaction->teller,
|
||||||
'order_id' => $transaction->order_id,
|
'order_id' => $transaction->order_id,
|
||||||
'status' => $transaction->status,
|
'status' => $transaction->status,
|
||||||
'certificate_url' => $transaction->certificate_url,
|
'certificate_url' => $transaction->certificate_url,
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\ShorakaFinancing;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\QueryException;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ShorakaFinancingController extends Controller
|
|
||||||
{
|
|
||||||
public function index(Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'kodcaw' => 'nullable|string|max:30',
|
|
||||||
'norujukan' => 'nullable|string|max:100',
|
|
||||||
'state' => 'nullable|in:WAITING,PROCESSING,REVIEW,DONE',
|
|
||||||
'date_from' => 'nullable|date_format:Y-m-d',
|
|
||||||
'date_to' => 'nullable|date_format:Y-m-d',
|
|
||||||
'per_page' => 'nullable|integer|min:1|max:100',
|
|
||||||
'page' => 'nullable|integer|min:1',
|
|
||||||
'branch_scope' => 'nullable|string|max:30',
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$query = ShorakaFinancing::query();
|
|
||||||
foreach (['kodcaw', 'norujukan', 'state'] as $field) {
|
|
||||||
if ($request->filled($field)) {
|
|
||||||
$query->where($field, $request->input($field));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($request->filled('date_from')) {
|
|
||||||
$query->where('created_at', '>=', $request->input('date_from') . ' 00:00:00');
|
|
||||||
}
|
|
||||||
if ($request->filled('date_to')) {
|
|
||||||
$query->where('created_at', '<', Carbon::parse($request->input('date_to'))->addDay()->format('Y-m-d') . ' 00:00:00');
|
|
||||||
}
|
|
||||||
$records = $query->orderByDesc('created_at')->orderByDesc('id')
|
|
||||||
->paginate(min(100, max(1, (int) $request->input('per_page', 25))));
|
|
||||||
$payload = [
|
|
||||||
'data' => $records->getCollection()->map(function (ShorakaFinancing $record) {
|
|
||||||
return $this->present($record, false);
|
|
||||||
})->values(),
|
|
||||||
'total' => $records->total(),
|
|
||||||
'current_page' => $records->currentPage(),
|
|
||||||
'per_page' => $records->perPage(),
|
|
||||||
'last_page' => $records->lastPage(),
|
|
||||||
];
|
|
||||||
if ($request->input('with_branches')) {
|
|
||||||
$branches = ShorakaFinancing::query();
|
|
||||||
if ($request->filled('branch_scope')) {
|
|
||||||
$branches->where('kodcaw', $request->input('branch_scope'));
|
|
||||||
}
|
|
||||||
$payload['branches'] = $branches->distinct()->orderBy('kodcaw')->pluck('kodcaw')->values();
|
|
||||||
}
|
|
||||||
return response()->json($payload);
|
|
||||||
} catch (QueryException $exception) {
|
|
||||||
return $this->unavailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(string $kodcaw, string $norujukan)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$record = $this->find($kodcaw, $norujukan);
|
|
||||||
} catch (QueryException $exception) {
|
|
||||||
return $this->unavailable();
|
|
||||||
}
|
|
||||||
if (!$record) {
|
|
||||||
return response()->json(['message' => 'Rekod pembiayaan tidak dijumpai.'], 404);
|
|
||||||
}
|
|
||||||
return response()->json(['data' => $this->present($record)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(string $kodcaw, string $norujukan, Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'teller' => 'required|string|max:255',
|
|
||||||
'action' => 'required|string|max:30',
|
|
||||||
'context' => 'present|array',
|
|
||||||
]);
|
|
||||||
try {
|
|
||||||
$existing = $this->find($kodcaw, $norujukan);
|
|
||||||
if ($existing) {
|
|
||||||
return response()->json(['created' => false, 'data' => $this->present($existing)]);
|
|
||||||
}
|
|
||||||
$record = ShorakaFinancing::create([
|
|
||||||
'kodcaw' => $kodcaw,
|
|
||||||
'norujukan' => $norujukan,
|
|
||||||
'teller' => $request->input('teller'),
|
|
||||||
'action' => $request->input('action'),
|
|
||||||
'state' => 'WAITING',
|
|
||||||
'context' => $request->input('context'),
|
|
||||||
]);
|
|
||||||
return response()->json(['created' => true, 'data' => $this->present($record)], 201);
|
|
||||||
} catch (QueryException $exception) {
|
|
||||||
try {
|
|
||||||
$existing = $this->find($kodcaw, $norujukan);
|
|
||||||
} catch (QueryException $again) {
|
|
||||||
return $this->unavailable();
|
|
||||||
}
|
|
||||||
if ($existing) {
|
|
||||||
return response()->json(['created' => false, 'data' => $this->present($existing)]);
|
|
||||||
}
|
|
||||||
return $this->unavailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(string $kodcaw, string $norujukan, Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'state' => 'required|in:WAITING,PROCESSING,REVIEW,DONE',
|
|
||||||
'expected_state' => 'nullable|in:WAITING,PROCESSING,REVIEW,DONE',
|
|
||||||
'context' => 'nullable|array',
|
|
||||||
]);
|
|
||||||
try {
|
|
||||||
$record = $this->find($kodcaw, $norujukan);
|
|
||||||
if (!$record) {
|
|
||||||
return response()->json(['message' => 'Rekod pembiayaan tidak dijumpai.'], 404);
|
|
||||||
}
|
|
||||||
if ($request->filled('expected_state')) {
|
|
||||||
$claimed = ShorakaFinancing::where('id', $record->id)
|
|
||||||
->where('state', $request->input('expected_state'))
|
|
||||||
->update(['state' => $request->input('state'), 'updated_at' => Carbon::now()]);
|
|
||||||
return response()->json([
|
|
||||||
'claimed' => $claimed === 1,
|
|
||||||
'data' => $this->present($record->fresh()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$fields = ['state' => $request->input('state')];
|
|
||||||
if ($request->exists('context')) {
|
|
||||||
$fields['context'] = $request->input('context') ?? [];
|
|
||||||
}
|
|
||||||
$record->update($fields);
|
|
||||||
return response()->json(['claimed' => true, 'data' => $this->present($record->fresh())]);
|
|
||||||
} catch (QueryException $exception) {
|
|
||||||
return $this->unavailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function find(string $kodcaw, string $norujukan): ?ShorakaFinancing
|
|
||||||
{
|
|
||||||
return ShorakaFinancing::where('kodcaw', $kodcaw)->where('norujukan', $norujukan)->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function present(ShorakaFinancing $record, bool $withContext = true): array
|
|
||||||
{
|
|
||||||
$data = [
|
|
||||||
'id' => $record->id,
|
|
||||||
'kodcaw' => $record->kodcaw,
|
|
||||||
'norujukan' => $record->norujukan,
|
|
||||||
'teller' => $record->teller,
|
|
||||||
'action' => $record->action,
|
|
||||||
'state' => $record->state,
|
|
||||||
'created_at' => optional($record->created_at)->toDateTimeString(),
|
|
||||||
'updated_at' => optional($record->updated_at)->toDateTimeString(),
|
|
||||||
];
|
|
||||||
if ($withContext) {
|
|
||||||
$data['context'] = $record->context ?? [];
|
|
||||||
}
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function unavailable()
|
|
||||||
{
|
|
||||||
return response()->json(['message' => 'Rekod pembiayaan Shoraka tidak tersedia.'], 503);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ class ShorakaGadaiController extends ShorakaController
|
|||||||
public function readiness(ShorakaService $shoraka)
|
public function readiness(ShorakaService $shoraka)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
\App\ShorakaFinancing::query()->limit(1)->exists();
|
\App\ShorakaTransaction::query()->limit(1)->exists();
|
||||||
$storeReady = true;
|
$storeReady = true;
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$storeReady = false;
|
$storeReady = false;
|
||||||
@@ -39,7 +39,7 @@ class ShorakaGadaiController extends ShorakaController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function submitGadai(string $kodcaw, string $norujukan, ShorakaGadaiService $gadaiService, ShorakaService $shoraka)
|
public function submitGadai(string $kodcaw, string $norujukan, Request $request, ShorakaGadaiService $gadaiService, ShorakaService $shoraka)
|
||||||
{
|
{
|
||||||
$gadai = $gadaiService->gadai($kodcaw, $norujukan);
|
$gadai = $gadaiService->gadai($kodcaw, $norujukan);
|
||||||
$existing = $gadaiService->transaction($kodcaw, $norujukan);
|
$existing = $gadaiService->transaction($kodcaw, $norujukan);
|
||||||
@@ -51,9 +51,14 @@ class ShorakaGadaiController extends ShorakaController
|
|||||||
|| $gadai->cetak || $gadai->sttebus) {
|
|| $gadai->cetak || $gadai->sttebus) {
|
||||||
abort(409, 'Gadaian lama atau telah selesai tidak boleh dihantar sebagai order baharu.');
|
abort(409, 'Gadaian lama atau telah selesai tidak boleh dihantar sebagai order baharu.');
|
||||||
}
|
}
|
||||||
$request = new Request($gadaiService->payload($gadai));
|
$payload = $gadaiService->payload($gadai);
|
||||||
$request->attributes->set('shoraka_kodcaw', $kodcaw);
|
$teller = $request->input('teller');
|
||||||
return $this->submit($request, $shoraka);
|
if (is_string($teller) && $teller !== '') {
|
||||||
|
$payload['teller'] = $teller;
|
||||||
|
}
|
||||||
|
$submission = new Request($payload);
|
||||||
|
$submission->attributes->set('shoraka_kodcaw', $kodcaw);
|
||||||
|
return $this->submit($submission, $shoraka);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refreshGadai(string $kodcaw, string $norujukan, ShorakaGadaiService $gadaiService, ShorakaService $shoraka)
|
public function refreshGadai(string $kodcaw, string $norujukan, ShorakaGadaiService $gadaiService, ShorakaService $shoraka)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class ShorakaFinancing extends Model
|
|
||||||
{
|
|
||||||
protected $connection = 'mysql7';
|
|
||||||
|
|
||||||
protected $table = 'shoraka_financings';
|
|
||||||
|
|
||||||
protected $guarded = [];
|
|
||||||
|
|
||||||
protected $casts = ['context' => 'array'];
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ class ShorakaTransaction extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'norujukan',
|
'norujukan',
|
||||||
'kodcaw',
|
'kodcaw',
|
||||||
|
'teller',
|
||||||
'order_id',
|
'order_id',
|
||||||
'status',
|
'status',
|
||||||
'product_type',
|
'product_type',
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
class CreateMasterShorakaFinancingsTable extends Migration
|
|
||||||
{
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
Schema::connection('mysql7')->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::connection('mysql7')->dropIfExists('shoraka_financings');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddTellerToShorakaTransactions extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('shoraka_transactions', function (Blueprint $table) {
|
||||||
|
$table->string('teller')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('shoraka_transactions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('teller');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
# Shoraka STP integration and kaunter workflow
|
|
||||||
|
|
||||||
Based on Shoraka Al-Amin STP API v1.5.0 (9 January 2026).
|
|
||||||
|
|
||||||
## Agreed behavior
|
|
||||||
|
|
||||||
New financing uses Shoraka instead of deducting internal commodity stock.
|
|
||||||
Cash/online financing completion and SAG printing wait for a Completed trade.
|
|
||||||
Existing historical commodity records retain their legacy reporting/printing path.
|
|
||||||
|
|
||||||
The counter saves financing details, records a resumable local workflow, and
|
|
||||||
submits the trade. It redirects the teller to **Pembiayaan Shoraka**.
|
|
||||||
The teller uses **Semak Status**, then **Selesaikan Pembiayaan** when eligible.
|
|
||||||
This second step performs the cash update or registers the online payment for
|
|
||||||
the existing PC approval flow. It does not itself transfer funds through a bank.
|
|
||||||
|
|
||||||
Ordinary financing, the two older refinancing paths, additional financing and
|
|
||||||
auto-tawarruq use this workflow. API refinancing returns norujukan_baru to ensure
|
|
||||||
the commodity trade belongs to the newly created financing, not the redeemed one.
|
|
||||||
Existing refinancing registration/settlement database writes still occur before
|
|
||||||
the commodity request; the counter cash completion and SAG are held afterwards.
|
|
||||||
|
|
||||||
## Configuration and migrations
|
|
||||||
|
|
||||||
API environment settings:
|
|
||||||
|
|
||||||
- SHORAKA_BASE_URL: default https://uat2.shorakagroup.com
|
|
||||||
- SHORAKA_API_ID and SHORAKA_SECRET_KEY: supplied by Shoraka
|
|
||||||
- SHORAKA_TIMEOUT: seconds, default 60
|
|
||||||
|
|
||||||
The counter calls its existing API_URL; Shoraka credentials are kept in the API.
|
|
||||||
Register the public HTTPS /api/shoraka/callback URL and arrange server IP
|
|
||||||
whitelisting with Shoraka. Confirm the configured environment before UAT/live use.
|
|
||||||
|
|
||||||
Deploy both repositories together and apply the following migrations using the
|
|
||||||
normal deployment process, before enabling counter traffic. They are not run
|
|
||||||
against application databases by the automated tests.
|
|
||||||
|
|
||||||
In api-kopkb (configured default database; existing migration may already be applied):
|
|
||||||
|
|
||||||
~~~sh
|
|
||||||
php artisan migrate --path=database/migrations/2026_09_15_094818_create_shoraka_transactions_table.php
|
|
||||||
php artisan migrate --path=database/migrations/2026_10_05_120000_add_kodcaw_to_shoraka_transactions.php
|
|
||||||
~~~
|
|
||||||
|
|
||||||
In kaunter-kopkb (counter application database):
|
|
||||||
|
|
||||||
~~~sh
|
|
||||||
php artisan migrate --path=database/migrations/2026_10_05_120000_create_shoraka_financings_table.php
|
|
||||||
~~~
|
|
||||||
|
|
||||||
Clear/rebuild any deployment route, config and view caches through the usual
|
|
||||||
deployment process. Do not mix a newly deployed API with an old counter build.
|
|
||||||
|
|
||||||
## Data mapping
|
|
||||||
|
|
||||||
| Shoraka field | Source |
|
|
||||||
| --- | --- |
|
|
||||||
| ownership | Central customer.nama matched using gadai.nokp |
|
|
||||||
| ownership_doc_no | gadai.norujukan |
|
|
||||||
| order_amount | Saved gadai.pinjaman |
|
|
||||||
| murabaha_amount | Saved gadai.hargajualan |
|
|
||||||
| tenor / tenor_other / unit | OTHERS / 12 / MONTH |
|
|
||||||
| product / commodity / currency / order | FINANCING / 000-COPPER / MYR / Buy & Sell |
|
|
||||||
|
|
||||||
Both current financing engines calculate the contract using upah12 and a
|
|
||||||
12-month contract end date. The elapsed-charge fields tempoh and tempohbayar
|
|
||||||
are not contractual tenor fields.
|
|
||||||
|
|
||||||
The backend derives amounts and customer identity; it ignores client-supplied
|
|
||||||
amounts on the branch submission endpoint. Each Shoraka order stores kodcaw.
|
|
||||||
A completed response/callback synchronizes gadai.tagkomuditi to 1 only when saved
|
|
||||||
financing amounts still match. Contract edits after submission are rejected.
|
|
||||||
Payment completion also verifies the staged payout amount.
|
|
||||||
|
|
||||||
## API endpoints
|
|
||||||
|
|
||||||
| Method | Path | Purpose |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| GET | /api/shoraka/readiness | Configuration readiness (not a live connectivity probe) |
|
|
||||||
| GET | /api/shoraka/gadai/{kodcaw}/{norujukan} | Local order, financing amounts and completion eligibility |
|
|
||||||
| POST | /api/shoraka/gadai/{kodcaw}/{norujukan}/submit | Submit from saved financing data |
|
|
||||||
| POST | /api/shoraka/gadai/{kodcaw}/{norujukan}/refresh | Query Shoraka and synchronize branch status |
|
|
||||||
| GET | /api/shoraka/gadai/{kodcaw}/{norujukan}/certificate | Download the fixed-endpoint PDF after Completed |
|
|
||||||
| POST | /api/shoraka/callback | Verify signature, refresh and acknowledge |
|
|
||||||
| POST | /api/shoraka/orders | Existing standalone submission contract |
|
|
||||||
| POST | /api/shoraka/orders/{norujukan}/refresh | Existing standalone refresh |
|
|
||||||
| GET | /api/shoraka/orders/{norujukan}/certificate | Existing standalone PDF download |
|
|
||||||
|
|
||||||
The old PUT /api/komuditi/transaksi endpoint now delegates to branch Shoraka
|
|
||||||
submission. New loans do not consume KomuditiPurchase stock. Historical stock
|
|
||||||
administration remains available; old stock update/cancel endpoints reject
|
|
||||||
references already associated with Shoraka.
|
|
||||||
|
|
||||||
Counter /shoraka pages require session authentication, an authorized role and
|
|
||||||
the user's branch. Only the original teller can submit/finish a staged financing.
|
|
||||||
POST forms use CSRF protection. API endpoints retain the repository's existing
|
|
||||||
network/access-control arrangement; restrict API access accordingly.
|
|
||||||
|
|
||||||
## Completion and recovery
|
|
||||||
|
|
||||||
shoraka_financings is the counter's durable completion record:
|
|
||||||
|
|
||||||
- WAITING: awaiting trade completion or teller completion.
|
|
||||||
- PROCESSING: one request has atomically claimed payment completion.
|
|
||||||
- DONE: completion returned successfully; saved request context is cleared.
|
|
||||||
- REVIEW: completion raised an error; do not retry payment automatically.
|
|
||||||
|
|
||||||
Only one WAITING request can claim completion. Repeated requests for DONE do
|
|
||||||
not repeat cash/online processing. A worker crash while PROCESSING or an
|
|
||||||
ambiguous response during payment requires operational reconciliation.
|
|
||||||
|
|
||||||
Shoraka PENDING/UNKNOWN/legacy FAILED entries block repeated submissions.
|
|
||||||
A timeout does not prove a remote trade failed. If an orderId is known, refresh.
|
|
||||||
Otherwise reconcile the reference with Shoraka and link/correct the record
|
|
||||||
through an authorized operational process before resubmission.
|
|
||||||
|
|
||||||
A failure between remote financing registration and local workflow creation
|
|
||||||
also requires reconciliation using the newly created reference. Do not repeat
|
|
||||||
the original refinancing operation, which may already have settled the old loan.
|
|
||||||
The guide provides neither a reference-based lookup nor an idempotency key.
|
|
||||||
|
|
||||||
Historical print paths remain for records with no Shoraka order/workflow.
|
|
||||||
For Shoraka financing, SAG metadata uses certificateDetails1, commodityType,
|
|
||||||
unit and volume from its own trade. The separate PDF is fetched from the signed
|
|
||||||
certificate endpoint. Missing certificate metadata blocks SAG printing.
|
|
||||||
|
|
||||||
## Protocol choices and UAT checks
|
|
||||||
|
|
||||||
- Appendix specifies OTHERS although prose says OTHER; use OTHERS.
|
|
||||||
- Section 5 specifies POST /api/certificate although history says GET; use POST.
|
|
||||||
- Order lookup/PDF signature: SECRET_KEY;API_ID;order_id.
|
|
||||||
- Callback camelCase strings are hashed in documented order without decimal normalization.
|
|
||||||
- ownershipDocNo and certificate fields are unsigned. Match a known orderId and
|
|
||||||
re-query current status rather than trusting those callback fields.
|
|
||||||
- Respond with the exact plain text OK only after status persistence/synchronization.
|
|
||||||
- Unknown callback orderId returns 404. An early callback needs redelivery or
|
|
||||||
subsequent refresh; the guide does not specify a retry policy.
|
|
||||||
- Confirm protocol inconsistencies, callback delivery and full certificate metadata in UAT.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
API: tests/ShorakaGadaiTest.php (includes the existing protocol regression tests).
|
|
||||||
Counter: tests/Feature/ShorakaFinancingTest.php.
|
|
||||||
|
|
||||||
Use PHPUnit with PDO SQLite enabled. Tests use in-memory databases and fake
|
|
||||||
HTTP responses. They do not run live trades, payouts or branch migrations.
|
|
||||||
+1
-4
@@ -120,10 +120,7 @@ $router->group(['prefix'=>'api'], function () use ($router){
|
|||||||
$router->get('gadai/online/search/detail',['uses'=>'GadaiController@searchGadaiOnline']);
|
$router->get('gadai/online/search/detail',['uses'=>'GadaiController@searchGadaiOnline']);
|
||||||
|
|
||||||
$router->get('shoraka/readiness', ['uses' => 'ShorakaGadaiController@readiness']);
|
$router->get('shoraka/readiness', ['uses' => 'ShorakaGadaiController@readiness']);
|
||||||
$router->get('shoraka/financings', ['uses' => 'ShorakaFinancingController@index']);
|
$router->get('shoraka/transactions', ['uses' => 'ShorakaController@index']);
|
||||||
$router->get('shoraka/financings/{kodcaw}/{norujukan}', ['uses' => 'ShorakaFinancingController@show']);
|
|
||||||
$router->post('shoraka/financings/{kodcaw}/{norujukan}', ['uses' => 'ShorakaFinancingController@store']);
|
|
||||||
$router->put('shoraka/financings/{kodcaw}/{norujukan}', ['uses' => 'ShorakaFinancingController@update']);
|
|
||||||
$router->get('shoraka/gadai/{kodcaw}/{norujukan}', ['uses' => 'ShorakaGadaiController@showGadai']);
|
$router->get('shoraka/gadai/{kodcaw}/{norujukan}', ['uses' => 'ShorakaGadaiController@showGadai']);
|
||||||
$router->post('shoraka/gadai/{kodcaw}/{norujukan}/submit', ['uses' => 'ShorakaGadaiController@submitGadai']);
|
$router->post('shoraka/gadai/{kodcaw}/{norujukan}/submit', ['uses' => 'ShorakaGadaiController@submitGadai']);
|
||||||
$router->post('shoraka/gadai/{kodcaw}/{norujukan}/refresh', ['uses' => 'ShorakaGadaiController@refreshGadai']);
|
$router->post('shoraka/gadai/{kodcaw}/{norujukan}/refresh', ['uses' => 'ShorakaGadaiController@refreshGadai']);
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class ShorakaIntegrationTest extends TestCase
|
|||||||
(new CreateShorakaTransactionsTable())->up();
|
(new CreateShorakaTransactionsTable())->up();
|
||||||
require_once __DIR__ . '/../database/migrations/2026_10_05_120000_add_kodcaw_to_shoraka_transactions.php';
|
require_once __DIR__ . '/../database/migrations/2026_10_05_120000_add_kodcaw_to_shoraka_transactions.php';
|
||||||
(new AddKodcawToShorakaTransactions())->up();
|
(new AddKodcawToShorakaTransactions())->up();
|
||||||
|
require_once __DIR__ . '/../database/migrations/2026_10_26_130000_add_teller_to_shoraka_transactions.php';
|
||||||
|
(new AddTellerToShorakaTransactions())->up();
|
||||||
Http::swap(new Factory());
|
Http::swap(new Factory());
|
||||||
// Fail any HTTP request that a test did not explicitly stub.
|
// Fail any HTTP request that a test did not explicitly stub.
|
||||||
Log::shouldReceive('error')->zeroOrMoreTimes();
|
Log::shouldReceive('error')->zeroOrMoreTimes();
|
||||||
|
|||||||
Reference in New Issue
Block a user