feature/shoraka-integration

This commit is contained in:
nurafrinaalimi16
2026-09-17 15:08:43 +08:00
parent aa4c8ca5ba
commit 50cf966da7
8 changed files with 592 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
<?php
namespace App\Http\Controllers;
use App\ShorakaTransaction;
use App\Services\ShorakaService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use RuntimeException;
use Throwable;
class ShorakaController extends Controller
{
/**
* Hantar transaksi pembiayaan pelanggan ke Shoraka.
*/
public function submit(
Request $request,
ShorakaService $shoraka
) {
/*
* Validation untuk Lumen 7.
*/
$validated = $this->validate($request, [
'norujukan' =>
'required|string',
'ownership' =>
'required|string',
'order_amount' =>
'required|numeric|min:0.01',
'murabaha_amount' =>
'required|numeric|min:0.01',
'tenor_other' =>
'required|integer|min:1',
]);
/*
* Jangan hubungi Shoraka selagi credentials
* belum diterima atau belum dimasukkan.
*/
if (
empty(config('services.shoraka.api_id')) ||
empty(config('services.shoraka.secret_key'))
) {
return response()->json([
'success' => false,
'message' =>
'API ID dan Secret Key Shoraka belum dikonfigurasi.',
], 503);
}
/*
* Semak sama ada nombor rujukan yang sama
* sudah pernah berjaya dihantar.
*/
$existing = ShorakaTransaction::where(
'norujukan',
$validated['norujukan']
)->first();
if (
$existing &&
!empty($existing->order_id)
) {
return response()->json([
'success' => false,
'message' =>
'Transaksi ini telah dihantar ke Shoraka sebelum ini.',
'data' => [
'norujukan' =>
$existing->norujukan,
'order_id' =>
$existing->order_id,
'status' =>
$existing->status,
],
], 409);
}
/*
* Sediakan data mengikut format Shoraka.
*/
$payload = [
'product_type' =>
'FINANCING',
/*
* Kod komoditi perlu disahkan
* dengan pihak Shoraka.
*/
'commodity_type' =>
'000-COPPER',
/*
* Nama pelanggan.
*/
'ownership' =>
$validated['ownership'],
/*
* Nombor rujukan pembiayaan eRahn.
*/
'ownership_doc_no' =>
$validated['norujukan'],
/*
* Format yang diminta Shoraka:
* DD/MM/YYYY
*/
'value_date' =>
Carbon::now()->format('d/m/Y'),
'order_currency' =>
'MYR',
'order_amount' =>
$validated['order_amount'],
'murabaha_amount' =>
$validated['murabaha_amount'],
/*
* Sila sahkan dengan Shoraka sama ada
* nilai sebenar ialah OTHER atau OTHERS.
*/
'tenor' =>
'OTHERS',
'tenor_other' =>
$validated['tenor_other'],
'tenor_other_unit' =>
'MONTH',
'order_type' =>
'Buy & Sell',
];
/*
* Simpan rekod awal sebelum request dihantar.
*/
$transaction = ShorakaTransaction::updateOrCreate(
[
'norujukan' =>
$validated['norujukan'],
],
[
'status' =>
'PENDING',
'product_type' =>
$payload['product_type'],
'commodity_type' =>
$payload['commodity_type'],
'ownership_name' =>
$payload['ownership'],
'ownership_doc_no' =>
$payload['ownership_doc_no'],
'order_amount' =>
$payload['order_amount'],
'murabaha_amount' =>
$payload['murabaha_amount'],
'request_payload' =>
$payload,
'error_message' =>
null,
]
);
try {
/*
* Tambah bilangan percubaan request.
*/
$transaction->increment(
'attempt_count'
);
/*
* Hantar order kepada API Shoraka.
*/
$response = $shoraka->submitOrder(
$payload
);
/*
* Shoraka mesti memberikan orderId.
*/
if (empty($response['orderId'])) {
throw new RuntimeException(
'Shoraka tidak memberikan orderId.'
);
}
$status = isset($response['status'])
? $response['status']
: 'SUBMITTED';
/*
* Simpan respons Shoraka.
*/
$transaction->update([
'order_id' =>
$response['orderId'],
'status' =>
$status,
'certificate_url' =>
isset($response['certificateUrl'])
? $response['certificateUrl']
: null,
'response_payload' =>
$response,
'submitted_at' =>
Carbon::now(),
'completed_at' =>
$status === 'Completed'
? Carbon::now()
: null,
'error_message' =>
null,
]);
return response()->json([
'success' => true,
'message' =>
'Order berjaya dihantar ke Shoraka.',
'data' => [
'norujukan' =>
$transaction->norujukan,
'order_id' =>
$transaction->order_id,
'status' =>
$transaction->status,
'certificate_url' =>
$transaction->certificate_url,
],
], 200);
} catch (Throwable $exception) {
/*
* Simpan status gagal.
*/
$transaction->update([
'status' =>
'FAILED',
'error_message' =>
$exception->getMessage(),
]);
/*
* Rekodkan ralat tanpa API Secret Key.
*/
Log::error(
'Shoraka order submission failed',
[
'norujukan' =>
$validated['norujukan'],
'message' =>
$exception->getMessage(),
]
);
return response()->json([
'success' => false,
'message' =>
'Order gagal dihantar ke Shoraka.',
/*
* Paparkan error untuk UAT sahaja.
* Buang field ini semasa production.
*/
'error' =>
$exception->getMessage(),
], 502);
}
}
}
+138
View File
@@ -0,0 +1,138 @@
<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class ShorakaService
{
protected $baseUrl;
protected $apiId;
protected $secretKey;
protected $timeout;
public function __construct()
{
$this->baseUrl = rtrim(
config('services.shoraka.base_url'),
'/'
);
$this->apiId =
config('services.shoraka.api_id');
$this->secretKey =
config('services.shoraka.secret_key');
$this->timeout =
config('services.shoraka.timeout', 60);
}
public function submitOrder(array $data): array
{
$payload = [
'api_id' => $this->apiId,
'product_type' =>
$data['product_type'],
'commodity_type' =>
$data['commodity_type'],
'ownership' =>
$data['ownership'],
'value_date' =>
$data['value_date'],
'order_currency' =>
$data['order_currency'],
'order_amount' =>
$this->formatAmount(
$data['order_amount']
),
'murabaha_amount' =>
$this->formatAmount(
$data['murabaha_amount']
),
'tenor' =>
$data['tenor'],
'tenor_other' =>
$data['tenor_other'] ?? '',
'tenor_other_unit' =>
$data['tenor_other_unit'] ?? '',
'order_type' =>
$data['order_type'],
'ownership_doc_no' =>
$data['ownership_doc_no'] ?? '',
];
$payload['signature'] =
$this->makeSubmitSignature($payload);
$response = Http::asForm()
->acceptJson()
->timeout($this->timeout)
->post(
$this->baseUrl . '/api/submitorder',
$payload
);
return $this->getJsonResponse($response);
}
protected function makeSubmitSignature(
array $payload
): string {
$values = [
$this->secretKey,
$this->apiId,
$payload['product_type'],
$payload['commodity_type'],
$payload['ownership'],
$payload['value_date'],
$payload['order_currency'],
$payload['order_amount'],
$payload['murabaha_amount'],
$payload['tenor'],
$payload['tenor_other'],
$payload['tenor_other_unit'],
$payload['order_type'],
];
return hash(
'sha256',
implode(';', $values)
);
}
protected function formatAmount($amount): string
{
return number_format(
(float) $amount,
2,
'.',
''
);
}
protected function getJsonResponse(
Response $response
): array {
if (!$response->successful()) {
throw new RuntimeException(
'Shoraka request failed. HTTP ' .
$response->status() .
': ' .
$response->body()
);
}
$data = $response->json();
if (!is_array($data)) {
throw new RuntimeException(
'Shoraka returned an invalid response.'
);
}
return $data;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ShorakaTransaction extends Model
{
protected $fillable = [
'norujukan',
'order_id',
'status',
'product_type',
'commodity_type',
'ownership_name',
'ownership_doc_no',
'order_amount',
'murabaha_amount',
'certificate_url',
'certificate_path',
'request_payload',
'response_payload',
'callback_payload',
'attempt_count',
'error_message',
'submitted_at',
'completed_at',
];
protected $casts = [
'order_amount' => 'decimal:2',
'murabaha_amount' => 'decimal:2',
'request_payload' => 'array',
'response_payload' => 'array',
'callback_payload' => 'array',
'submitted_at' => 'datetime',
'completed_at' => 'datetime',
];
}
+1
View File
@@ -87,6 +87,7 @@ $app->singleton(
*/ */
$app->configure('app'); $app->configure('app');
$app->configure('services');
$app->configure('logging'); $app->configure('logging');
$app->configure('tinker'); $app->configure('tinker');
$app->configure('tennant_database'); $app->configure('tennant_database');
+21
View File
@@ -0,0 +1,21 @@
<?php
return [
'shoraka' => [
'base_url' => env(
'SHORAKA_BASE_URL',
'https://uat2.shorakagroup.com'
),
'api_id' => env('SHORAKA_API_ID'),
'secret_key' => env('SHORAKA_SECRET_KEY'),
'timeout' => env(
'SHORAKA_TIMEOUT',
60
),
],
];
@@ -0,0 +1,87 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateShorakaTransactionsTable extends Migration
{
public function up()
{
Schema::create(
'shoraka_transactions',
function (Blueprint $table) {
$table->id();
// Rujukan pembiayaan dalam eRahn
$table->string('norujukan')->unique();
// Maklumat daripada Shoraka
$table->string('order_id')
->nullable()
->unique();
$table->string('status')
->default('PENDING');
$table->string('product_type')
->default('FINANCING');
$table->string('commodity_type');
$table->string('ownership_name');
$table->string('ownership_doc_no')
->nullable();
$table->decimal(
'order_amount',
15,
2
);
$table->decimal(
'murabaha_amount',
15,
2
);
$table->string('certificate_url')
->nullable();
$table->string('certificate_path')
->nullable();
// Simpan payload untuk audit dan troubleshooting
$table->json('request_payload')
->nullable();
$table->json('response_payload')
->nullable();
$table->json('callback_payload')
->nullable();
$table->unsignedInteger('attempt_count')
->default(0);
$table->text('error_message')
->nullable();
$table->timestamp('submitted_at')
->nullable();
$table->timestamp('completed_at')
->nullable();
$table->timestamps();
$table->index('status');
}
);
}
public function down()
{
Schema::dropIfExists('shoraka_transactions');
}
}
View File
+3
View File
@@ -116,6 +116,9 @@ $router->group(['prefix'=>'api'], function () use ($router){
$router->get('gadai/list/rekod/old',['uses'=>'GadaiController@rekodGadaian']); $router->get('gadai/list/rekod/old',['uses'=>'GadaiController@rekodGadaian']);
$router->get('gadai/online/search/detail',['uses'=>'GadaiController@searchGadaiOnline']); $router->get('gadai/online/search/detail',['uses'=>'GadaiController@searchGadaiOnline']);
// Shoraka Commodity API
$router->post('shoraka/orders',['uses' => 'ShorakaController@submit']);
$router->get('check/serah/gadai',['uses'=>'GadaiController@checkSerahGadaian']); $router->get('check/serah/gadai',['uses'=>'GadaiController@checkSerahGadaian']);
$router->get('check/serah/tebus',['uses'=>'TebusController@checkSerahTebus']); $router->get('check/serah/tebus',['uses'=>'TebusController@checkSerahTebus']);
$router->get('check/serah/lelong',['uses'=>'LelongController@checkSerahLelong']); $router->get('check/serah/lelong',['uses'=>'LelongController@checkSerahLelong']);