feature/shoraka-integration
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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']);
|
||||||
|
|||||||
Reference in New Issue
Block a user