0451ad3dbe
Reviewed-on: #24 Co-authored-by: ISMAIL MASSERAN <ismail@koppkb.com> Co-committed-by: ISMAIL MASSERAN <ismail@koppkb.com>
375 lines
14 KiB
PHP
375 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Bsas;
|
|
|
|
use App\BsasOrder;
|
|
use App\KomuditiTransaksi;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Log;
|
|
use RuntimeException;
|
|
|
|
class BsasOrderService
|
|
{
|
|
/** @var BsasClient */
|
|
protected $client;
|
|
|
|
public function __construct(BsasClient $client = null)
|
|
{
|
|
$this->client = $client ?: new BsasClient();
|
|
}
|
|
|
|
/**
|
|
* Place BSAS order for a gadai Terima, long-poll result, fetch eCert.
|
|
*
|
|
* @param array $input norujukan, kodcaw, pinjaman, hargajualan, nama_pelanggan, teller
|
|
* @return BsasOrder
|
|
*/
|
|
public function placeAndConfirm(array $input): BsasOrder
|
|
{
|
|
$norujukan = (string) ($input['norujukan'] ?? '');
|
|
$kodcaw = (string) ($input['kodcaw'] ?? '');
|
|
$pinjaman = round((float) ($input['pinjaman'] ?? 0), 2);
|
|
$hargajualan = round((float) ($input['hargajualan'] ?? 0), 2);
|
|
$counterParty = trim((string) ($input['nama_pelanggan'] ?? ''));
|
|
$createdBy = (string) ($input['teller'] ?? '');
|
|
|
|
if ($norujukan === '' || $kodcaw === '' || $pinjaman <= 0) {
|
|
throw new RuntimeException('BSAS order requires norujukan, kodcaw and pinjaman.');
|
|
}
|
|
|
|
$existing = BsasOrder::on('mysql7')
|
|
->where('norujukan', $norujukan)
|
|
->orderBy('id', 'desc')
|
|
->first();
|
|
|
|
if ($existing && $existing->status === 'confirmed') {
|
|
if ($this->needsEcertFetch($existing)) {
|
|
$this->fetchAndStoreEcert($existing);
|
|
}
|
|
$this->ensureKomuditiTransaksi($existing, $createdBy);
|
|
return $existing->fresh();
|
|
}
|
|
|
|
if ($existing && in_array($existing->status, ['pending', 'submitted'], true) && !empty($existing->uuid)) {
|
|
return $this->completeSubmittedOrder($existing, $createdBy);
|
|
}
|
|
|
|
$serial = '1';
|
|
$payload = $this->buildOrderPayload($pinjaman, $hargajualan, $counterParty, $serial);
|
|
|
|
$order = BsasOrder::on('mysql7')->create([
|
|
'uuid' => null,
|
|
'norujukan' => $norujukan,
|
|
'kodcaw' => $kodcaw,
|
|
'serial_number' => $serial,
|
|
'product_code' => config('bsas.product_code'),
|
|
'bid_value' => $pinjaman,
|
|
'otc_murabaha_value' => $hargajualan,
|
|
'otc_counter_party' => $counterParty,
|
|
'status' => 'pending',
|
|
'request_payload' => $payload,
|
|
'created_by' => $createdBy,
|
|
]);
|
|
|
|
try {
|
|
$orderResponse = $this->client->placeOrder($payload);
|
|
} catch (\Throwable $e) {
|
|
$order->status = 'failed';
|
|
$order->status_message = $e->getMessage();
|
|
$order->save();
|
|
throw $e;
|
|
}
|
|
|
|
$order->order_response = $orderResponse;
|
|
$uuid = data_get($orderResponse, 'header.uuid');
|
|
$errorCode = trim((string) data_get($orderResponse, 'header.errorCode', ''));
|
|
$errorMsg = trim((string) data_get($orderResponse, 'header.errorMsg', ''));
|
|
|
|
if ($errorCode !== '' || !$uuid) {
|
|
$order->status = 'failed';
|
|
$order->status_message = $errorMsg !== '' ? $errorMsg : 'BSAS order rejected (missing uuid or errorCode set).';
|
|
$order->save();
|
|
throw new RuntimeException($order->status_message);
|
|
}
|
|
|
|
$bodyStatus = data_get($orderResponse, 'body.0.statusCode');
|
|
if ($bodyStatus !== null && (int) $bodyStatus !== 0) {
|
|
$order->uuid = $uuid;
|
|
$order->status = 'failed';
|
|
$order->status_code = (int) $bodyStatus;
|
|
$order->status_message = (string) data_get($orderResponse, 'body.0.statusMessage', 'BSAS order body statusCode != 0');
|
|
$order->save();
|
|
throw new RuntimeException($order->status_message);
|
|
}
|
|
|
|
$order->uuid = $uuid;
|
|
$order->status = 'submitted';
|
|
$order->save();
|
|
|
|
return $this->completeSubmittedOrder($order, $createdBy);
|
|
}
|
|
|
|
/**
|
|
* Re-query orderResult / eCert for an existing uuid.
|
|
*/
|
|
public function refreshByUuid(string $uuid): BsasOrder
|
|
{
|
|
$order = BsasOrder::on('mysql7')->where('uuid', $uuid)->first();
|
|
if (!$order) {
|
|
throw new RuntimeException('BSAS order not found for uuid.');
|
|
}
|
|
|
|
if ($order->status === 'confirmed') {
|
|
if ($this->needsEcertFetch($order)) {
|
|
$this->fetchAndStoreEcert($order);
|
|
}
|
|
return $order->fresh();
|
|
}
|
|
|
|
return $this->completeSubmittedOrder($order, (string) $order->created_by);
|
|
}
|
|
|
|
protected function completeSubmittedOrder(BsasOrder $order, string $createdBy): BsasOrder
|
|
{
|
|
if (empty($order->uuid)) {
|
|
throw new RuntimeException('BSAS order has no uuid to poll.');
|
|
}
|
|
|
|
$result = $this->longPollOrderResult($order->uuid, (string) $order->serial_number);
|
|
$order->result_response = $result;
|
|
|
|
$headerError = trim((string) data_get($result, 'header.errorCode', ''));
|
|
$headerMsg = trim((string) data_get($result, 'header.errorMsg', ''));
|
|
$statusCode = data_get($result, 'body.0.statusCode');
|
|
$statusMessage = (string) data_get($result, 'body.0.statusMessage', '');
|
|
$bidErrNo = trim((string) data_get($result, 'body.0.bidErrNo', ''));
|
|
$otcErrNo = trim((string) data_get($result, 'body.0.otcErrNo', ''));
|
|
$stbErrNo = trim((string) data_get($result, 'body.0.stbErrNo', ''));
|
|
$bidMsg = trim((string) data_get($result, 'body.0.bidMsg', ''));
|
|
$otcMsg = trim((string) data_get($result, 'body.0.otcMsg', ''));
|
|
$stbMsg = trim((string) data_get($result, 'body.0.stbMsg', ''));
|
|
$ecertNo = $this->extractEcertNo($result, $order->order_response);
|
|
|
|
if ($headerError !== '') {
|
|
$order->status = 'failed';
|
|
$order->status_code = is_numeric($statusCode) ? (int) $statusCode : null;
|
|
$order->status_message = $headerMsg !== '' ? $headerMsg : 'BSAS orderResult header errorCode set.';
|
|
$order->save();
|
|
throw new RuntimeException($order->status_message);
|
|
}
|
|
|
|
// Live BSAS result often has no statusCode; success is ecertNo and/or bid/otc/stb ErrNo=999 (OK).
|
|
$successByStatusCode = $statusCode !== null && (int) $statusCode === 0;
|
|
$successByErrNo = ($bidErrNo === '999' || strtoupper($bidMsg) === 'OK')
|
|
&& ($otcErrNo === '999' || strtoupper($otcMsg) === 'OK')
|
|
&& ($stbErrNo === '999' || strtoupper($stbMsg) === 'OK');
|
|
$successByEcert = $ecertNo !== null && $ecertNo !== '';
|
|
|
|
if (!$successByStatusCode && !$successByErrNo && !$successByEcert) {
|
|
if ($statusCode !== null && (int) $statusCode !== 0) {
|
|
$order->status = 'failed';
|
|
$order->status_code = (int) $statusCode;
|
|
$order->status_message = $statusMessage !== '' ? $statusMessage : 'BSAS orderResult statusCode != 0';
|
|
$order->save();
|
|
throw new RuntimeException($order->status_message);
|
|
}
|
|
|
|
$order->status = 'submitted';
|
|
$order->status_message = 'BSAS orderResult belum selesai / tiada ecert.';
|
|
$order->save();
|
|
throw new RuntimeException('BSAS orderResult belum selesai. Sila cuba semula.');
|
|
}
|
|
|
|
$order->status_code = $statusCode !== null ? (int) $statusCode : 0;
|
|
$order->status_message = $statusMessage !== '' ? $statusMessage : ($bidMsg ?: 'OK');
|
|
$order->status = 'confirmed';
|
|
if ($ecertNo) {
|
|
$order->e_cert_no = $ecertNo;
|
|
}
|
|
$order->save();
|
|
|
|
if ($this->needsEcertFetch($order)) {
|
|
$this->fetchAndStoreEcert($order);
|
|
}
|
|
|
|
if (empty($order->e_cert_no)) {
|
|
Log::warning('BSAS order confirmed but ecertno missing', [
|
|
'norujukan' => $order->norujukan,
|
|
'uuid' => $order->uuid,
|
|
]);
|
|
}
|
|
|
|
$this->ensureKomuditiTransaksi($order, $createdBy);
|
|
|
|
return $order->fresh();
|
|
}
|
|
|
|
protected function longPollOrderResult(string $uuid, string $serial): array
|
|
{
|
|
$retries = max(1, (int) config('bsas.order_result_retries', 2));
|
|
$last = [];
|
|
|
|
for ($i = 0; $i < $retries; $i++) {
|
|
$last = $this->client->orderResult($uuid, [
|
|
'serialNumbers' => $serial,
|
|
'forceYN' => 'Y',
|
|
'maxWaitTime' => (string) config('bsas.max_wait_time', 10),
|
|
'waitAllDoneYN' => 'Y',
|
|
]);
|
|
|
|
// Done when statusCode is set, or bid, OTC and STB have all reported.
|
|
$bidDone = trim((string) data_get($last, 'body.0.bidErrNo', '')) !== '';
|
|
$otcDone = trim((string) data_get($last, 'body.0.otcErrNo', '')) !== '';
|
|
$stbDone = trim((string) data_get($last, 'body.0.stbErrNo', '')) !== '';
|
|
$hasEcert = trim((string) data_get($last, 'body.0.ecertNo', '')) !== ''
|
|
|| trim((string) data_get($last, 'body.0.eCertNo', '')) !== '';
|
|
|
|
if (
|
|
data_get($last, 'body.0.statusCode') !== null
|
|
|| ($bidDone && $otcDone && $stbDone)
|
|
|| ($hasEcert && $stbDone)
|
|
) {
|
|
return $last;
|
|
}
|
|
}
|
|
|
|
return $last;
|
|
}
|
|
|
|
protected function needsEcertFetch(BsasOrder $order): bool
|
|
{
|
|
return empty($order->bid_ecert_response)
|
|
|| empty($order->otc_ecert_response)
|
|
|| empty($order->stb_ecert_response);
|
|
}
|
|
|
|
protected function fetchAndStoreEcert(BsasOrder $order): void
|
|
{
|
|
$ecertNo = $order->e_cert_no;
|
|
if (!$ecertNo) {
|
|
$ecertNo = $this->extractEcertNo($order->result_response, $order->order_response);
|
|
if ($ecertNo) {
|
|
$order->e_cert_no = $ecertNo;
|
|
$order->save();
|
|
}
|
|
}
|
|
|
|
if (!$ecertNo) {
|
|
return;
|
|
}
|
|
|
|
$calls = [
|
|
'bid_ecert_response' => 'getBidEcert',
|
|
'otc_ecert_response' => 'getOtcEcert',
|
|
'stb_ecert_response' => 'getStbEcert',
|
|
];
|
|
|
|
foreach ($calls as $column => $method) {
|
|
if (!empty($order->{$column})) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$ecert = $this->client->{$method}($ecertNo);
|
|
$order->{$column} = $ecert;
|
|
if ($column === 'otc_ecert_response' && empty($order->e_cert_no) && !empty($ecert['ECERTNO'])) {
|
|
$order->e_cert_no = $ecert['ECERTNO'];
|
|
}
|
|
$order->save();
|
|
} catch (\Throwable $e) {
|
|
Log::error('BSAS eCert fetch failed after confirmed order', [
|
|
'norujukan' => $order->norujukan,
|
|
'uuid' => $order->uuid,
|
|
'e_cert_no' => $ecertNo,
|
|
'certificate' => $column,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function ensureKomuditiTransaksi(BsasOrder $order, string $teller): void
|
|
{
|
|
$exists = KomuditiTransaksi::on('mysql7')
|
|
->where('norujukan', $order->norujukan)
|
|
->first();
|
|
|
|
if ($exists) {
|
|
return;
|
|
}
|
|
|
|
$current = Carbon::now();
|
|
KomuditiTransaksi::on('mysql7')->create([
|
|
'unit_komuditi' => round((float) $order->bid_value, 2),
|
|
'nosijil' => $order->e_cert_no ?: $order->uuid,
|
|
'norujukan' => $order->norujukan,
|
|
'kodcaw' => $order->kodcaw,
|
|
'teller' => $teller !== '' ? $teller : $order->created_by,
|
|
'masa' => $current->toTimeString(),
|
|
'tarikh' => $current->toDateString(),
|
|
]);
|
|
}
|
|
|
|
protected function buildOrderPayload(float $pinjaman, float $hargajualan, string $counterParty, string $serial): array
|
|
{
|
|
return [
|
|
'header' => [
|
|
'memberShortName' => config('bsas.member_short_name'),
|
|
'uuid' => $this->uuidV4(),
|
|
],
|
|
'body' => [
|
|
'serialNumber' => $serial,
|
|
'bidOption' => 'Y',
|
|
'otcOption' => 'Y',
|
|
'stbOption' => 'Y',
|
|
'productCode' => config('bsas.product_code'),
|
|
'purchaseType' => 'P',
|
|
'clientName' => '',
|
|
'currency' => 'MYR',
|
|
'bidValue' => number_format($pinjaman, 2, '.', ''),
|
|
'valueDate' => Carbon::now()->format('Ymd'),
|
|
'tenor' => config('bsas.tenor'),
|
|
'otcCounterParty' => $counterParty,
|
|
'otcMurabaha' => '',
|
|
'otcMurabahaValue' => number_format($hargajualan, 2, '.', ''),
|
|
'eCertNo' => '',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Best-effort extract ecert number from result/order payloads.
|
|
*/
|
|
protected function extractEcertNo($result, $orderResponse): ?string
|
|
{
|
|
$candidates = [
|
|
data_get($result, 'body.0.eCertNo'),
|
|
data_get($result, 'body.0.ecertNo'),
|
|
data_get($result, 'body.0.ECERTNO'),
|
|
data_get($result, 'body.0.ecertno'),
|
|
data_get($result, 'body.0.certificateNo'),
|
|
data_get($orderResponse, 'body.0.eCertNo'),
|
|
data_get($orderResponse, 'body.0.ecertNo'),
|
|
data_get($orderResponse, 'body.0.ECERTNO'),
|
|
];
|
|
|
|
foreach ($candidates as $value) {
|
|
$value = trim((string) $value);
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function uuidV4(): string
|
|
{
|
|
$data = random_bytes(16);
|
|
$data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
|
|
$data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
|
|
|
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
|
}
|
|
}
|