feat(accounting): add Shayegan as fixed boursecu provider
Wire outbound Invoice/Document sync after confirmed ops, store sync status with timeout lock, and keep SQL credentials out of frontend responses. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -81,7 +81,7 @@ class SettingsController extends Controller
|
||||
->orderBy('key')
|
||||
->get();
|
||||
|
||||
return response()->json($rows);
|
||||
return response()->json($this->maskSecretSettingValues($rows));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,7 +162,19 @@ class SettingsController extends Controller
|
||||
|
||||
$previousValue = $setting->exists ? (string) ($setting->value ?? '') : null;
|
||||
|
||||
$setting->value = $request->value;
|
||||
$incomingValue = $request->value;
|
||||
if (
|
||||
in_array($key, ['shayegan_db_password', 'shayegan_auth_password'], true)
|
||||
&& is_string($incomingValue)
|
||||
&& ($incomingValue === '********' || $incomingValue === '')
|
||||
&& $previousValue !== null
|
||||
&& $previousValue !== ''
|
||||
) {
|
||||
// ماسک frontend را بهجای رمز واقعی ذخیره نکن
|
||||
$incomingValue = $previousValue;
|
||||
}
|
||||
|
||||
$setting->value = $incomingValue;
|
||||
$setting->description = $request->description;
|
||||
if ($request->has('is_active')) {
|
||||
$setting->is_active = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
|
||||
@@ -327,7 +339,7 @@ class SettingsController extends Controller
|
||||
$overlay = app(SuperAdminProgramSettingsOverlay::class);
|
||||
$saMap = $overlay->catalogMap();
|
||||
if ($saMap === null) {
|
||||
return $localRows;
|
||||
return $this->maskSecretSettingValues($localRows);
|
||||
}
|
||||
|
||||
$localByKey = [];
|
||||
@@ -380,7 +392,53 @@ class SettingsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values();
|
||||
return $this->maskSecretSettingValues(
|
||||
collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* رمزهای شایگان و مشابه در پاسخ frontend برنگردند (فقط پر/خالی بودن).
|
||||
*
|
||||
* @param mixed $rows
|
||||
* @return mixed
|
||||
*/
|
||||
private function maskSecretSettingValues($rows)
|
||||
{
|
||||
$secretKeys = [
|
||||
'shayegan_db_password',
|
||||
'shayegan_auth_password',
|
||||
];
|
||||
|
||||
$maskOne = function ($row) use ($secretKeys) {
|
||||
if (is_array($row)) {
|
||||
$key = (string) ($row['key'] ?? '');
|
||||
if (in_array($key, $secretKeys, true)) {
|
||||
$val = (string) ($row['value'] ?? '');
|
||||
$row['value'] = $val !== '' ? '********' : '';
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
if (is_object($row) && isset($row->key)) {
|
||||
$key = (string) $row->key;
|
||||
if (in_array($key, $secretKeys, true)) {
|
||||
$val = (string) ($row->value ?? '');
|
||||
$row->value = $val !== '' ? '********' : '';
|
||||
}
|
||||
}
|
||||
|
||||
return $row;
|
||||
};
|
||||
|
||||
if ($rows instanceof \Illuminate\Support\Collection) {
|
||||
return $rows->map($maskOne)->values();
|
||||
}
|
||||
if (is_array($rows)) {
|
||||
return array_map($maskOne, $rows);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function isEditableCanEdit(mixed $canEdit): bool
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Shayegan;
|
||||
use App\Services\ShayeganClient;
|
||||
use App\Services\ShayeganSyncService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ShayeganController extends Controller
|
||||
{
|
||||
/** @var ShayeganSyncService */
|
||||
private $sync;
|
||||
|
||||
/** @var ShayeganClient */
|
||||
private $client;
|
||||
|
||||
public function __construct(ShayeganSyncService $sync, ShayeganClient $client)
|
||||
{
|
||||
$this->sync = $sync;
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function healthCheck()
|
||||
{
|
||||
$result = $this->client->healthCheck();
|
||||
$status = ! empty($result['ok']) ? 200 : 422;
|
||||
|
||||
return response()->json([
|
||||
'ok' => ! empty($result['ok']),
|
||||
'message' => $result['message'] ?? null,
|
||||
'http_status' => $result['http_status'] ?? null,
|
||||
'timeout' => ! empty($result['timeout']),
|
||||
// never echo Config passwords
|
||||
'body' => $this->sanitizeBodyForAdmin($result['body'] ?? null),
|
||||
], $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function storeTrade(array $data)
|
||||
{
|
||||
return $this->sync->recordTrade($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function wallet(array $data)
|
||||
{
|
||||
return $this->sync->recordWalletCash($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function walletGold(array $data)
|
||||
{
|
||||
return $this->sync->recordWalletGold($data);
|
||||
}
|
||||
|
||||
public function table(Request $request)
|
||||
{
|
||||
if (! Schema::hasTable('shayegan')) {
|
||||
return response()->json(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$q = Shayegan::query()->orderByDesc('id');
|
||||
if ($request->filled('sync_status')) {
|
||||
$q->where('sync_status', $request->get('sync_status'));
|
||||
}
|
||||
if ($request->filled('type')) {
|
||||
$q->where('type', $request->get('type'));
|
||||
}
|
||||
|
||||
$perPage = min(100, max(10, (int) $request->get('per_page', 20)));
|
||||
$page = $q->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'data' => $page->items(),
|
||||
'total' => $page->total(),
|
||||
'current_page' => $page->currentPage(),
|
||||
'per_page' => $page->perPage(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$row = Shayegan::findOrFail($id);
|
||||
|
||||
return response()->json($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual retry — refuses timeout rows unless force=1 after operator verified remote.
|
||||
*/
|
||||
public function retrySanad(Request $request, $id)
|
||||
{
|
||||
$row = Shayegan::findOrFail($id);
|
||||
if ($row->sync_status === ShayeganSyncService::STATUS_SUCCESS) {
|
||||
return response()->json(['ok' => true, 'message' => 'already synced', 'row' => $row]);
|
||||
}
|
||||
if ($row->sync_status === ShayeganSyncService::STATUS_TIMEOUT && ! $request->boolean('force')) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => 'این سند timeout شده؛ بدون بررسی نتیجه در حسابگر دوباره ارسال نکنید. در صورت تایید force=1 بفرستید.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = json_decode((string) $row->data, true);
|
||||
$put = is_array($payload) ? ($payload['PutObject'] ?? null) : null;
|
||||
$endpoint = (string) ($row->endpoint ?: 'Document/Put');
|
||||
if (! is_array($put) || $put === []) {
|
||||
return response()->json(['ok' => false, 'message' => 'payload خالی است'], 422);
|
||||
}
|
||||
|
||||
// Clear timeout lock only when force retry requested
|
||||
if ($row->sync_status === ShayeganSyncService::STATUS_TIMEOUT) {
|
||||
$row->sync_status = ShayeganSyncService::STATUS_PENDING;
|
||||
$row->save();
|
||||
}
|
||||
|
||||
$result = $this->client->put($endpoint, $put);
|
||||
if (! empty($result['timeout'])) {
|
||||
$row->sync_status = ShayeganSyncService::STATUS_TIMEOUT;
|
||||
$row->sync_log = (string) ($result['message'] ?? 'timeout');
|
||||
$row->res = json_encode(['timeout' => true], JSON_UNESCAPED_UNICODE);
|
||||
$row->save();
|
||||
|
||||
return response()->json(['ok' => false, 'message' => 'timeout', 'row' => $row], 422);
|
||||
}
|
||||
|
||||
if (! empty($result['ok'])) {
|
||||
$row->sync_status = ShayeganSyncService::STATUS_SUCCESS;
|
||||
$row->sync_log = null;
|
||||
$row->shayegan_number = $result['number'] ?? null;
|
||||
$row->shayegan_guid = $result['guid'] ?? null;
|
||||
$row->res = json_encode($result['body'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$row->save();
|
||||
|
||||
return response()->json(['ok' => true, 'row' => $row]);
|
||||
}
|
||||
|
||||
$row->sync_status = ShayeganSyncService::STATUS_FAILED;
|
||||
$row->sync_log = (string) ($result['message'] ?? 'failed');
|
||||
$row->res = json_encode($result['body'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$row->save();
|
||||
|
||||
return response()->json(['ok' => false, 'message' => $row->sync_log, 'row' => $row], 422);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $body
|
||||
* @return mixed
|
||||
*/
|
||||
private function sanitizeBodyForAdmin($body)
|
||||
{
|
||||
if (! is_array($body)) {
|
||||
return $body;
|
||||
}
|
||||
// Drop huge Result lists from health if present
|
||||
if (isset($body['Result']) && is_array($body['Result'])) {
|
||||
$body['Result'] = array_slice($body['Result'], 0, 3);
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
@@ -284,6 +284,16 @@ class WalletController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (app(ConfigService::class)->isShayeganAccounting()) {
|
||||
try {
|
||||
$data['direction'] = $deposit ? 0 : 1;
|
||||
$data['withdraw_id'] = $Withdraw->id;
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->wallet($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
app(PargarSyncService::class)->queueWalletDocument(
|
||||
$customer,
|
||||
@@ -430,6 +440,16 @@ class WalletController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (app(ConfigService::class)->isShayeganAccounting()) {
|
||||
try {
|
||||
$data['direction'] = $deposit ? 1 : 0;
|
||||
$data['withdraw_id'] = $Withdraw->id;
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->walletGold($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
app(PargarSyncService::class)->queueWalletDocument(
|
||||
$customer,
|
||||
@@ -669,6 +689,17 @@ class WalletController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (app(ConfigService::class)->isShayeganAccounting()) {
|
||||
try {
|
||||
$data['direction'] = $deposit ? 1 : 0;
|
||||
$data['withdraw_id'] = $Withdraw->id;
|
||||
$data['product_id'] = $productId;
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->walletGold($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// فقط محصول متصل به کیف اصلی → سند طلای پرگار
|
||||
app(PargarSyncService::class)->queueWalletDocument(
|
||||
|
||||
@@ -445,6 +445,14 @@ class CartController extends Controller
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isShayeganAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->storeTrade($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isPargarAccounting()) {
|
||||
|
||||
@@ -486,6 +486,20 @@ class WalletController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (app(ConfigService::class)->isShayeganAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->wallet([
|
||||
'customer_id' => auth('customer')->id(),
|
||||
'value' => $amount,
|
||||
'direction' => 1,
|
||||
'withdraw_id' => $Withdraw->id,
|
||||
'note' => 'برداشت ریالی',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
$companyName = $this->Config('name_fa'); // Company name from configuration
|
||||
$message = "کاربر $userName با شماره موبایل $userMobile درخواست برداشت مبلغ " . number_format($withdrawAmount) . " ریال را دارد. با تشکر، $companyName";
|
||||
User::where('send_sms', 1)->get()->each(function ($admin) use ($message) {
|
||||
|
||||
@@ -2362,6 +2362,14 @@ class SingleOrderController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isShayeganAccounting()) {
|
||||
try {
|
||||
$data['shayegan'] = app(\App\Http\Controllers\Admin\ShayeganController::class)->storeTrade($data);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
app(\App\Services\ColleagueAccountingService::class)->recordCoveredTrade($data);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Shayegan extends Model
|
||||
{
|
||||
protected $table = 'shayegan';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'datetime',
|
||||
'amount' => 'float',
|
||||
];
|
||||
}
|
||||
@@ -59,5 +59,13 @@ class AccountingWalletPush
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isShayeganAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->walletGold($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1261,6 +1261,7 @@ class ConfigService
|
||||
public const ACCOUNTING_TAHESAB = 'tahesab';
|
||||
public const ACCOUNTING_PARGAR = 'pargar';
|
||||
public const ACCOUNTING_ZARIN = 'zarin';
|
||||
public const ACCOUNTING_SHAYEGAN = 'shayegan';
|
||||
|
||||
public function isConditionalOrderEnabled(): bool
|
||||
{
|
||||
@@ -1311,22 +1312,13 @@ class ConfigService
|
||||
return max(0, $this->getInt('fee_conditional_order', 0));
|
||||
}
|
||||
|
||||
/** none | tahesab | pargar | zarin — از setting accounting_provider */
|
||||
/**
|
||||
* بورس مس: حسابداری بیرونی همیشه شایگان است (مستقل از accounting_provider / legacy flags).
|
||||
* ارسال واقعی فقط وقتی shayegan_* در settings کامل باشد (ShayeganSyncService::isSyncEnabled).
|
||||
*/
|
||||
public function accountingProvider(): string
|
||||
{
|
||||
$provider = strtolower(trim($this->getString('accounting_provider', '')));
|
||||
if (in_array($provider, [self::ACCOUNTING_NONE, self::ACCOUNTING_TAHESAB, self::ACCOUNTING_PARGAR, self::ACCOUNTING_ZARIN], true)) {
|
||||
return $provider;
|
||||
}
|
||||
|
||||
if ($this->getString('send_to_pargar', 'false') === 'true') {
|
||||
return self::ACCOUNTING_PARGAR;
|
||||
}
|
||||
if ($this->getString('send_to_tahesab', 'false') === 'true') {
|
||||
return self::ACCOUNTING_TAHESAB;
|
||||
}
|
||||
|
||||
return self::ACCOUNTING_NONE;
|
||||
return self::ACCOUNTING_SHAYEGAN;
|
||||
}
|
||||
|
||||
public function isTahesabAccounting(): bool
|
||||
@@ -1344,6 +1336,11 @@ class ConfigService
|
||||
return $this->accountingProvider() === self::ACCOUNTING_ZARIN;
|
||||
}
|
||||
|
||||
public function isShayeganAccounting(): bool
|
||||
{
|
||||
return $this->accountingProvider() === self::ACCOUNTING_SHAYEGAN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hostname for WebOTP SMS (@domain #code) and other public links.
|
||||
* Prefer settings.app_url (دامنهٔ اپ کاربر) over the API request host —
|
||||
|
||||
@@ -125,5 +125,13 @@ class WalletDepositCreditor
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->config->isShayeganAccounting()) {
|
||||
try {
|
||||
app(\App\Http\Controllers\Admin\ShayeganController::class)->wallet($payload);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* HTTP client for Shayegan Cyber Account webservice (REST JSON).
|
||||
* Contract from vendor PDF: Config + Domain|PutObject; always inspect ErrorMessage.
|
||||
*/
|
||||
class ShayeganClient
|
||||
{
|
||||
/** @var ConfigService */
|
||||
private $config;
|
||||
|
||||
public function __construct(ConfigService $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->baseUrl() !== ''
|
||||
&& $this->config->getString('shayegan_sql_server', '') !== ''
|
||||
&& $this->config->getString('shayegan_database', '') !== ''
|
||||
&& $this->config->getString('shayegan_db_user', '') !== '';
|
||||
}
|
||||
|
||||
public function baseUrl(): string
|
||||
{
|
||||
return rtrim(trim($this->config->getString('shayegan_base_url', '')), '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildConfig(): array
|
||||
{
|
||||
$authUser = trim($this->config->getString('shayegan_auth_user', ''));
|
||||
$authPass = (string) $this->config->getString('shayegan_auth_password', '');
|
||||
|
||||
return [
|
||||
'Server' => $this->config->getString('shayegan_sql_server', ''),
|
||||
'AllowRowSecurity' => false,
|
||||
'Level' => 0,
|
||||
'DBUserName' => $this->config->getString('shayegan_db_user', ''),
|
||||
'DBPassword' => (string) $this->config->getString('shayegan_db_password', ''),
|
||||
'DataBaseName' => $this->config->getString('shayegan_database', ''),
|
||||
'AuthUser' => $authUser !== '' ? $authUser : '',
|
||||
'AuthPassword' => $authUser !== '' ? $authPass : '',
|
||||
'Language' => $this->config->getInt('shayegan_language', 3),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only smoke test: Account/Get one page.
|
||||
*
|
||||
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool}
|
||||
*/
|
||||
public function healthCheck(): array
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'تنظیمات اتصال شایگان ناقص است (base_url / Server / DataBase / DBUser).',
|
||||
'http_status' => null,
|
||||
'body' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->get('Account/Get', [], 0, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $domain
|
||||
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool}
|
||||
*/
|
||||
public function get(string $endpoint, array $domain = [], int $rowStart = 0, int $rowCount = 1): array
|
||||
{
|
||||
$payload = [
|
||||
'Config' => $this->buildConfig(),
|
||||
'Domain' => $domain,
|
||||
];
|
||||
|
||||
return $this->request('GET_STYLE', $endpoint, $payload, [
|
||||
'RowStart' => $rowStart,
|
||||
'RowCount' => $rowCount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $putObject
|
||||
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool,number:?string,guid:?string}
|
||||
*/
|
||||
public function put(string $endpoint, array $putObject): array
|
||||
{
|
||||
$payload = [
|
||||
'Config' => $this->buildConfig(),
|
||||
'PutObject' => array_values($putObject),
|
||||
];
|
||||
|
||||
$result = $this->request('PUT_STYLE', $endpoint, $payload, []);
|
||||
if (! empty($result['body']) && is_array($result['body'])) {
|
||||
$first = $this->firstResultRow($result['body']);
|
||||
$result['number'] = isset($first['Number']) ? (string) $first['Number'] : null;
|
||||
$result['guid'] = isset($first['GuId']) ? (string) $first['GuId'] : (isset($first['Guid']) ? (string) $first['Guid'] : null);
|
||||
} else {
|
||||
$result['number'] = null;
|
||||
$result['guid'] = null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<string, int|string> $query
|
||||
* @return array{ok:bool,message:string,http_status:?int,body:?array,timeout?:bool}
|
||||
*/
|
||||
private function request(string $style, string $endpoint, array $payload, array $query): array
|
||||
{
|
||||
$url = $this->endpointUrl($endpoint, $query);
|
||||
$timeout = max(5, $this->config->getInt('shayegan_timeout_seconds', 30));
|
||||
|
||||
try {
|
||||
$response = Http::timeout($timeout)
|
||||
->withHeaders(['Content-Type' => 'application/json', 'Accept' => 'application/json'])
|
||||
->post($url, $payload);
|
||||
|
||||
$status = $response->status();
|
||||
$json = $response->json();
|
||||
if (! is_array($json)) {
|
||||
$raw = (string) $response->body();
|
||||
$decoded = json_decode($raw, true);
|
||||
$json = is_array($decoded) ? $decoded : ['raw' => Str::limit($raw, 2000)];
|
||||
}
|
||||
|
||||
$error = $this->extractErrorMessage($json);
|
||||
if ($status >= 400) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $error !== '' ? $error : ('HTTP ' . $status),
|
||||
'http_status' => $status,
|
||||
'body' => $json,
|
||||
];
|
||||
}
|
||||
|
||||
if ($error !== '') {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $error,
|
||||
'http_status' => $status,
|
||||
'body' => $json,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => 'OK',
|
||||
'http_status' => $status,
|
||||
'body' => $json,
|
||||
];
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $e) {
|
||||
Log::warning('shayegan.timeout_or_connection', [
|
||||
'endpoint' => $endpoint,
|
||||
'style' => $style,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'timeout/connection: ' . $e->getMessage(),
|
||||
'http_status' => null,
|
||||
'body' => null,
|
||||
'timeout' => true,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('shayegan.request_failed', [
|
||||
'endpoint' => $endpoint,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
'http_status' => null,
|
||||
'body' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int|string> $query
|
||||
*/
|
||||
private function endpointUrl(string $endpoint, array $query): string
|
||||
{
|
||||
$endpoint = ltrim(str_replace('\\', '/', $endpoint), '/');
|
||||
$url = $this->baseUrl() . '/api/' . $endpoint;
|
||||
if ($query !== []) {
|
||||
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($query);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
public function extractErrorMessage(array $body): string
|
||||
{
|
||||
$top = trim((string) ($body['ErrorMessage'] ?? ''));
|
||||
if ($top !== '') {
|
||||
return $top;
|
||||
}
|
||||
|
||||
$row = $this->firstResultRow($body);
|
||||
if ($row !== null) {
|
||||
$rowError = trim((string) ($row['ErrorMessage'] ?? ''));
|
||||
if ($rowError !== '') {
|
||||
return $rowError;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function firstResultRow(array $body): ?array
|
||||
{
|
||||
$result = $body['Result'] ?? null;
|
||||
if (! is_array($result) || $result === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$first = isset($result[0]) && is_array($result[0]) ? $result[0] : null;
|
||||
if ($first !== null) {
|
||||
return $first;
|
||||
}
|
||||
|
||||
return is_array($result) && ! isset($result[0]) ? $result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip secrets before logging / storing in admin-visible res when needed.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function redactPayload(array $payload): array
|
||||
{
|
||||
if (isset($payload['Config']) && is_array($payload['Config'])) {
|
||||
foreach (['DBPassword', 'AuthPassword'] as $key) {
|
||||
if (array_key_exists($key, $payload['Config']) && $payload['Config'][$key] !== '' && $payload['Config'][$key] !== null) {
|
||||
$payload['Config'][$key] = '***';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Shayegan;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Outbound Shayegan sync — does not mutate wallets.
|
||||
* Timeout rows stay timeout and are never auto-retried.
|
||||
*/
|
||||
class ShayeganSyncService
|
||||
{
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_SUCCESS = 'success';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
public const STATUS_TIMEOUT = 'timeout';
|
||||
|
||||
public const TYPE_TRADE = 'trade';
|
||||
public const TYPE_WALLET_CASH = 'wallet_cash';
|
||||
public const TYPE_WALLET_GOLD = 'wallet_gold';
|
||||
public const TYPE_CUSTOMER = 'customer';
|
||||
|
||||
/** InvTyp from Shayegan PDF (company view). */
|
||||
public const INV_SALE = 2;
|
||||
public const INV_PURCHASE = 3;
|
||||
public const INV_HAVALE = 4;
|
||||
public const INV_RECEIPT = 5;
|
||||
|
||||
/** @var ConfigService */
|
||||
private $config;
|
||||
|
||||
/** @var ShayeganClient */
|
||||
private $client;
|
||||
|
||||
public function __construct(ConfigService $config, ShayeganClient $client)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function isSyncEnabled(): bool
|
||||
{
|
||||
return $this->config->isShayeganAccounting()
|
||||
&& $this->client->isConfigured();
|
||||
}
|
||||
|
||||
public function tradeMappingReady(): bool
|
||||
{
|
||||
return $this->config->getString('shayegan_item_copper', '') !== ''
|
||||
&& $this->config->getString('shayegan_stock_number', '') !== '';
|
||||
}
|
||||
|
||||
public function cashMappingReady(): bool
|
||||
{
|
||||
return $this->config->getString('shayegan_bank_account', '') !== '';
|
||||
}
|
||||
|
||||
public function metalWalletMode(): string
|
||||
{
|
||||
$mode = strtolower(trim($this->config->getString('shayegan_metal_wallet_mode', 'skip')));
|
||||
if (in_array($mode, ['skip', 'document', 'receipt_havale'], true)) {
|
||||
return $mode;
|
||||
}
|
||||
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
/**
|
||||
* After confirmed trade: one Invoice only (no duplicate Document).
|
||||
* Customer buy → company sale (InvTyp 2); customer sell → company purchase (InvTyp 3).
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function recordTrade(array $data): ?Shayegan
|
||||
{
|
||||
if (! $this->isSyncEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if (! $this->tradeMappingReady()) {
|
||||
Log::info('shayegan.trade_skipped_mapping', ['order_id' => $data['order']->id ?? null]);
|
||||
return null;
|
||||
}
|
||||
|
||||
$order = $data['order'] ?? null;
|
||||
$orderId = $order ? (int) $order->id : (int) ($data['order_id'] ?? 0);
|
||||
$customerId = (int) ($data['customer_id'] ?? ($order->customer_id ?? 0));
|
||||
if ($customerId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$existing = $this->findExisting(self::TYPE_TRADE, $orderId > 0 ? $orderId : null, null, $customerId);
|
||||
if ($existing && $this->shouldSkipSend($existing)) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$customer = Customer::find($customerId);
|
||||
$accountNumber = $this->customerAccountNumber($customer);
|
||||
if ($accountNumber === '') {
|
||||
return $this->markLocalFailed($existing, $customerId, $orderId, null, self::TYPE_TRADE, 'کد حساب مشتری در شایگان خالی است (moshtari_code).', null);
|
||||
}
|
||||
|
||||
$isBuy = (int) ($data['direction'] ?? 0) === 1;
|
||||
$invTyp = $isBuy ? self::INV_SALE : self::INV_PURCHASE;
|
||||
$quan = $this->scaleWeight((float) ($data['number'] ?? $data['amount'] ?? 0));
|
||||
$price = $this->scaleAmount((float) ($data['price'] ?? $data['fee'] ?? 0));
|
||||
if ($quan <= 0) {
|
||||
return $this->markLocalFailed($existing, $customerId, $orderId, null, self::TYPE_TRADE, 'مقدار وزن معامله برای شایگان صفر است.', null);
|
||||
}
|
||||
|
||||
$guid = $existing && $existing->request_guid
|
||||
? (string) $existing->request_guid
|
||||
: (string) Str::uuid();
|
||||
|
||||
$today = date('Ymd');
|
||||
$put = [[
|
||||
'Body' => [[
|
||||
'STNumber' => $this->config->getString('shayegan_stock_number', ''),
|
||||
'STGuId' => null,
|
||||
'ItemNumber' => $this->config->getString('shayegan_item_copper', ''),
|
||||
'ItemGuId' => null,
|
||||
'Quan' => $quan,
|
||||
'Quan2' => null,
|
||||
'Price' => $price > 0 ? $price : null,
|
||||
'Price2' => null,
|
||||
'LineDiscAmount' => null,
|
||||
'LineItemDesc' => $isBuy ? 'خرید مس مشتری' : 'فروش مس مشتری',
|
||||
'UpdateKind' => 0,
|
||||
]],
|
||||
'Expense' => [],
|
||||
'InvTyp' => $invTyp,
|
||||
'InvNo' => null,
|
||||
'InvDescription' => $isBuy ? 'فروش مس به مشتری' : 'خرید مس از مشتری',
|
||||
'InvDate' => $today,
|
||||
'InvPayDue' => $today,
|
||||
'AccountNumber' => $accountNumber,
|
||||
'AccountGuId' => null,
|
||||
'GeneralRef' => $orderId > 0 ? ('order:' . $orderId) : null,
|
||||
'GuId' => $guid,
|
||||
'UpdateKind' => 0,
|
||||
]];
|
||||
|
||||
$row = $existing ?: new Shayegan();
|
||||
$row->customer_id = $customerId;
|
||||
$row->order_id = $orderId > 0 ? $orderId : null;
|
||||
$row->withdraw_id = null;
|
||||
$row->amount = $this->scaleAmount((float) ($data['total'] ?? $data['total_price'] ?? ($quan * $price)));
|
||||
$row->desc = $put[0]['InvDescription'];
|
||||
$row->type = self::TYPE_TRADE;
|
||||
$row->date = now();
|
||||
$row->endpoint = 'Invoice/Put';
|
||||
$row->request_guid = $guid;
|
||||
$row->data = json_encode($this->client->redactPayload([
|
||||
'Config' => $this->client->buildConfig(),
|
||||
'PutObject' => $put,
|
||||
]), JSON_UNESCAPED_UNICODE);
|
||||
$row->sync_status = self::STATUS_PENDING;
|
||||
$row->sync_log = null;
|
||||
$row->save();
|
||||
|
||||
return $this->dispatchPut($row, 'Invoice/Put', $put);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rial wallet: Document only (not Invoice).
|
||||
* direction: 0 deposit, 1 withdraw (same as Zarin/Tahesab wallet cash).
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function recordWalletCash(array $data): ?Shayegan
|
||||
{
|
||||
if (! $this->isSyncEnabled()) {
|
||||
return null;
|
||||
}
|
||||
if (! $this->cashMappingReady()) {
|
||||
Log::info('shayegan.cash_skipped_mapping', ['withdraw_id' => $data['withdraw_id'] ?? null]);
|
||||
return null;
|
||||
}
|
||||
|
||||
$customerId = (int) ($data['customer_id'] ?? 0);
|
||||
$withdrawId = isset($data['withdraw_id']) ? (int) $data['withdraw_id'] : null;
|
||||
$customer = Customer::find($customerId);
|
||||
$accountNumber = $this->customerAccountNumber($customer);
|
||||
if ($accountNumber === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$existing = $this->findExisting(self::TYPE_WALLET_CASH, null, $withdrawId, $customerId);
|
||||
if ($existing && $this->shouldSkipSend($existing)) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$isDeposit = (int) ($data['direction'] ?? 0) === 0;
|
||||
$amount = $this->scaleAmount(abs((float) ($data['value'] ?? 0)));
|
||||
if ($amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bank = $this->config->getString('shayegan_bank_account', '');
|
||||
$guid = $existing && $existing->request_guid
|
||||
? (string) $existing->request_guid
|
||||
: (string) Str::uuid();
|
||||
|
||||
// Deposit: bank debit + customer credit. Withdraw: customer debit + bank credit.
|
||||
if ($isDeposit) {
|
||||
$body = [
|
||||
['AccountNumber' => $bank, 'AmountBed' => $amount, 'AmountBes' => 0, 'RialBed' => $amount, 'RialBes' => 0, 'DocRowDesc' => 'واریز ریالی'],
|
||||
['AccountNumber' => $accountNumber, 'AmountBed' => 0, 'AmountBes' => $amount, 'RialBed' => 0, 'RialBes' => $amount, 'DocRowDesc' => 'واریز ریالی مشتری'],
|
||||
];
|
||||
$desc = 'واریز ریالی';
|
||||
} else {
|
||||
$body = [
|
||||
['AccountNumber' => $accountNumber, 'AmountBed' => $amount, 'AmountBes' => 0, 'RialBed' => $amount, 'RialBes' => 0, 'DocRowDesc' => 'برداشت ریالی مشتری'],
|
||||
['AccountNumber' => $bank, 'AmountBed' => 0, 'AmountBes' => $amount, 'RialBed' => 0, 'RialBes' => $amount, 'DocRowDesc' => 'برداشت ریالی'],
|
||||
];
|
||||
$desc = 'برداشت ریالی';
|
||||
}
|
||||
|
||||
$put = [[
|
||||
'Body' => $body,
|
||||
'DocDescription' => trim((string) ($data['note'] ?? '')) !== '' ? (string) $data['note'] : $desc,
|
||||
'DocDate' => date('Ymd'),
|
||||
'DocReference' => $withdrawId ? ('withdraw:' . $withdrawId) : null,
|
||||
'GuId' => $guid,
|
||||
'UpdateKind' => 0,
|
||||
]];
|
||||
|
||||
$row = $existing ?: new Shayegan();
|
||||
$row->customer_id = $customerId;
|
||||
$row->order_id = null;
|
||||
$row->withdraw_id = $withdrawId;
|
||||
$row->amount = $amount;
|
||||
$row->desc = $desc;
|
||||
$row->type = self::TYPE_WALLET_CASH;
|
||||
$row->date = now();
|
||||
$row->endpoint = 'Document/Put';
|
||||
$row->request_guid = $guid;
|
||||
$row->data = json_encode($this->client->redactPayload([
|
||||
'Config' => $this->client->buildConfig(),
|
||||
'PutObject' => $put,
|
||||
]), JSON_UNESCAPED_UNICODE);
|
||||
$row->sync_status = self::STATUS_PENDING;
|
||||
$row->sync_log = null;
|
||||
$row->save();
|
||||
|
||||
return $this->dispatchPut($row, 'Document/Put', $put);
|
||||
}
|
||||
|
||||
/**
|
||||
* Product / metal wallet movement — never invent warehouse unless mode says so.
|
||||
* direction: 1 deposit, 0 withdraw (walletGold convention).
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function recordWalletGold(array $data): ?Shayegan
|
||||
{
|
||||
if (! $this->isSyncEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mode = $this->metalWalletMode();
|
||||
if ($mode === 'skip') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customerId = (int) ($data['customer_id'] ?? 0);
|
||||
$withdrawId = isset($data['withdraw_id']) ? (int) $data['withdraw_id'] : null;
|
||||
$customer = Customer::find($customerId);
|
||||
$accountNumber = $this->customerAccountNumber($customer);
|
||||
if ($accountNumber === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$existing = $this->findExisting(self::TYPE_WALLET_GOLD, null, $withdrawId, $customerId);
|
||||
if ($existing && $this->shouldSkipSend($existing)) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$isDeposit = (int) ($data['direction'] ?? 1) === 1;
|
||||
$quan = $this->scaleWeight(abs((float) ($data['value'] ?? 0)));
|
||||
if ($quan <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$guid = $existing && $existing->request_guid
|
||||
? (string) $existing->request_guid
|
||||
: (string) Str::uuid();
|
||||
|
||||
if ($mode === 'receipt_havale') {
|
||||
if (! $this->tradeMappingReady()) {
|
||||
return null;
|
||||
}
|
||||
$invTyp = $isDeposit ? self::INV_RECEIPT : self::INV_HAVALE;
|
||||
$put = [[
|
||||
'Body' => [[
|
||||
'STNumber' => $this->config->getString('shayegan_stock_number', ''),
|
||||
'ItemNumber' => $this->config->getString('shayegan_item_copper', ''),
|
||||
'Quan' => $quan,
|
||||
'Price' => null,
|
||||
'LineItemDesc' => $isDeposit ? 'واریز محصول' : 'برداشت محصول',
|
||||
'UpdateKind' => 0,
|
||||
]],
|
||||
'Expense' => [],
|
||||
'InvTyp' => $invTyp,
|
||||
'InvNo' => null,
|
||||
'InvDescription' => $isDeposit ? 'رسید انبار (واریز محصول)' : 'حواله (برداشت محصول)',
|
||||
'InvDate' => date('Ymd'),
|
||||
'AccountNumber' => $accountNumber,
|
||||
'GeneralRef' => $withdrawId ? ('withdraw:' . $withdrawId) : null,
|
||||
'GuId' => $guid,
|
||||
'UpdateKind' => 0,
|
||||
]];
|
||||
$endpoint = 'Invoice/Put';
|
||||
$desc = $put[0]['InvDescription'];
|
||||
} else {
|
||||
// document mode: accounting note only — no stock invoice
|
||||
$bank = $this->config->getString('shayegan_bank_account', '');
|
||||
if ($bank === '') {
|
||||
Log::info('shayegan.gold_document_needs_bank_or_switch_mode');
|
||||
return null;
|
||||
}
|
||||
// Placeholder balanced zero-amount skip — without operator chart of accounts for metal,
|
||||
// document mode requires explicit counterpart accounts; refuse rather than invent.
|
||||
Log::info('shayegan.gold_document_needs_account_map', [
|
||||
'hint' => 'Provide metal counterpart accounts or use receipt_havale after confirming warehouse semantics.',
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $existing ?: new Shayegan();
|
||||
$row->customer_id = $customerId;
|
||||
$row->order_id = null;
|
||||
$row->withdraw_id = $withdrawId;
|
||||
$row->amount = $quan;
|
||||
$row->desc = $desc;
|
||||
$row->type = self::TYPE_WALLET_GOLD;
|
||||
$row->date = now();
|
||||
$row->endpoint = $endpoint;
|
||||
$row->request_guid = $guid;
|
||||
$row->data = json_encode($this->client->redactPayload([
|
||||
'Config' => $this->client->buildConfig(),
|
||||
'PutObject' => $put,
|
||||
]), JSON_UNESCAPED_UNICODE);
|
||||
$row->sync_status = self::STATUS_PENDING;
|
||||
$row->sync_log = null;
|
||||
$row->save();
|
||||
|
||||
return $this->dispatchPut($row, $endpoint, $put);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $putObject
|
||||
*/
|
||||
private function dispatchPut(Shayegan $row, string $endpoint, array $putObject): Shayegan
|
||||
{
|
||||
$result = $this->client->put($endpoint, $putObject);
|
||||
|
||||
if (! empty($result['timeout'])) {
|
||||
$row->sync_status = self::STATUS_TIMEOUT;
|
||||
$row->sync_log = (string) ($result['message'] ?? 'timeout');
|
||||
$row->res = json_encode(['timeout' => true, 'message' => $result['message'] ?? null], JSON_UNESCAPED_UNICODE);
|
||||
$row->save();
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
if (! empty($result['ok'])) {
|
||||
$row->sync_status = self::STATUS_SUCCESS;
|
||||
$row->sync_log = null;
|
||||
$row->shayegan_number = $result['number'] ?? null;
|
||||
$row->shayegan_guid = $result['guid'] ?? null;
|
||||
$row->res = json_encode($result['body'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$row->save();
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
$row->sync_status = self::STATUS_FAILED;
|
||||
$row->sync_log = (string) ($result['message'] ?? 'failed');
|
||||
$row->res = json_encode($result['body'] ?? ['message' => $result['message'] ?? null], JSON_UNESCAPED_UNICODE);
|
||||
$row->save();
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function shouldSkipSend(Shayegan $row): bool
|
||||
{
|
||||
$status = (string) $row->sync_status;
|
||||
if ($status === self::STATUS_SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
// Never auto-resend after timeout without manual verification.
|
||||
if ($status === self::STATUS_TIMEOUT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function findExisting(string $type, ?int $orderId, ?int $withdrawId, int $customerId): ?Shayegan
|
||||
{
|
||||
if (! Schema::hasTable('shayegan')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$q = Shayegan::query()->where('type', $type)->where('customer_id', $customerId);
|
||||
if ($orderId) {
|
||||
$q->where('order_id', $orderId);
|
||||
}
|
||||
if ($withdrawId) {
|
||||
$q->where('withdraw_id', $withdrawId);
|
||||
}
|
||||
if (! $orderId && ! $withdrawId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $q->orderByDesc('id')->first();
|
||||
}
|
||||
|
||||
private function customerAccountNumber(?Customer $customer): string
|
||||
{
|
||||
if (! $customer) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return trim((string) ($customer->moshtari_code ?? ''));
|
||||
}
|
||||
|
||||
private function scaleAmount(float $amount): float
|
||||
{
|
||||
$scale = (float) $this->config->getString('shayegan_amount_scale', '1');
|
||||
if ($scale <= 0) {
|
||||
$scale = 1.0;
|
||||
}
|
||||
|
||||
return round($amount * $scale, 4);
|
||||
}
|
||||
|
||||
private function scaleWeight(float $weight): float
|
||||
{
|
||||
$scale = (float) $this->config->getString('shayegan_weight_scale', '1');
|
||||
if ($scale <= 0) {
|
||||
$scale = 1.0;
|
||||
}
|
||||
|
||||
return round($weight * $scale, 6);
|
||||
}
|
||||
|
||||
private function markLocalFailed(
|
||||
?Shayegan $existing,
|
||||
int $customerId,
|
||||
?int $orderId,
|
||||
?int $withdrawId,
|
||||
string $type,
|
||||
string $message,
|
||||
$amount
|
||||
): Shayegan {
|
||||
$row = $existing ?: new Shayegan();
|
||||
$row->customer_id = $customerId;
|
||||
$row->order_id = $orderId;
|
||||
$row->withdraw_id = $withdrawId;
|
||||
$row->amount = $amount;
|
||||
$row->type = $type;
|
||||
$row->date = now();
|
||||
$row->sync_status = self::STATUS_FAILED;
|
||||
$row->sync_log = $message;
|
||||
$row->save();
|
||||
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user