Files
boursecu_server/app/Services/TahesabInboundWalletService.php
T

403 lines
12 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use App\Models\Customer;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Rooyesh\Wallet\interfaces\TransactionInterface;
use Rooyesh\Wallet\interfaces\WalletInterface;
/**
* ماندهٔ طلا/ریال ته‌حساب → کیف اپ.
*
* منبع: GetMandeHesabByCode
* MandeyeKolBeTALA → طلای کیف اصلی
* MandeyeKolBePool → موجودی ریالی
*
* سیاست تصمیم ادمین:
* overwrite=true → اختلاف را اعمال کن
* keep=true → موجودی اپ را دست نزن
* ask_if_conflict → اگر اپ مانده دارد و با ته‌حساب فرق دارد، بدون نوشتن برگردان
*/
class TahesabInboundWalletService
{
public const TX_TYPE_TAHESAB = 23;
private const BATCH_SIZE = 40;
private const EPSILON_GOLD = 0.0005;
private const EPSILON_CASH = 1.0;
public function __construct(
private readonly ConfigService $config,
private readonly WalletBroadcastService $walletBroadcast,
private readonly WalletBalanceService $walletBalance,
) {
}
/**
* @param list<int|string> $codes
* @return array<string, array{gold: float, cash: float, raw: array}>
*/
public function fetchMandeByCodes(array $codes): array
{
$normalized = [];
foreach ($codes as $code) {
$c = (int) $code;
if ($c > 0) {
$normalized[(string) $c] = $c;
}
}
if ($normalized === []) {
return [];
}
$out = [];
foreach (array_chunk(array_values($normalized), self::BATCH_SIZE) as $chunk) {
$batch = $this->requestMandeChunk($chunk);
foreach ($batch as $code => $row) {
$out[(string) $code] = $row;
}
}
return $out;
}
/**
* @param array{gold?: float, cash?: float}|null $mande
* @param array{overwrite?: bool, keep?: bool, ask_if_conflict?: bool} $options
* @return array{
* applied: bool,
* needs_decision?: bool,
* skipped?: bool,
* gold_delta?: float,
* rial_delta?: float,
* app_gold?: float,
* app_cash?: float,
* tahesab_gold?: float,
* tahesab_cash?: float,
* message: string
* }
*/
public function syncWalletFromTahesab(Customer $customer, ?array $mande = null, array $options = []): array
{
$overwrite = (bool) ($options['overwrite'] ?? false);
$keep = (bool) ($options['keep'] ?? false);
$askIfConflict = (bool) ($options['ask_if_conflict'] ?? false);
$code = (int) ($customer->moshtari_code ?? 0);
if ($code <= 0) {
return ['applied' => false, 'message' => 'کد ته‌حساب ندارد'];
}
if ($mande === null) {
$all = $this->fetchMandeByCodes([$code]);
$mande = $all[(string) $code] ?? null;
}
if ($mande === null) {
return ['applied' => false, 'message' => 'مانده از ته‌حساب دریافت نشد'];
}
$goldInv = (float) ($mande['gold'] ?? 0);
$rialInv = (float) ($mande['cash'] ?? 0);
$ownerType = (int) ($customer->type ?? 2);
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
$wallet = $walletInterface->wallet;
$mainGold = $this->walletBalance
->accountingVisibleMainWalletGold((int) $customer->id, $ownerType);
$mainCash = (float) ($wallet->cash ?? 0);
$goldDiff = $goldInv - $mainGold;
$rialDiff = $rialInv - $mainCash;
$goldSame = abs($goldDiff) < self::EPSILON_GOLD;
$rialSame = abs($rialDiff) < self::EPSILON_CASH;
$comparison = [
'app_gold' => $mainGold,
'app_cash' => $mainCash,
'tahesab_gold' => $goldInv,
'tahesab_cash' => $rialInv,
'gold_delta' => $goldDiff,
'rial_delta' => $rialDiff,
];
if ($goldSame && $rialSame) {
return array_merge($comparison, [
'applied' => false,
'message' => 'مانده با ته‌حساب یکسان است',
]);
}
if ($keep) {
return array_merge($comparison, [
'applied' => false,
'skipped' => true,
'message' => 'موجودی اپ حفظ شد — مانده ته‌حساب اعمال نشد',
]);
}
$appHasBalance = abs($mainGold) >= self::EPSILON_GOLD
|| abs($mainCash) >= self::EPSILON_CASH;
// اگر اپ مانده دارد و با ته‌حساب فرق دارد و ادمین هنوز تصمیم نگرفته
if ($askIfConflict && ! $overwrite && $appHasBalance) {
return array_merge($comparison, [
'applied' => false,
'needs_decision' => true,
'message' => 'موجودی اپ با ته‌حساب فرق دارد — تصمیم ادمین لازم است',
]);
}
$input = [
'title' => 'تطبیق ماندهٔ ته‌حساب',
'note' => 'tahesab-mande:'.$code,
'expire_at' => now(),
'active' => true,
];
if (! $goldSame) {
$input['gold'] = abs($goldDiff);
}
if (! $rialSame) {
$input['cash'] = (int) round(abs($rialDiff));
}
new TransactionInterface(
$walletInterface,
$input,
self::TX_TYPE_TAHESAB,
$goldDiff > 0,
$rialDiff > 0,
0,
[],
true
);
$this->walletBroadcast->broadcastForCustomer(
(int) $customer->id,
$ownerType
);
Log::info('Tahesab mande applied to wallet', [
'customer_id' => (int) $customer->id,
'moshtari_code' => $code,
'gold_target' => $goldInv,
'cash_target' => $rialInv,
'gold_delta' => $goldDiff,
'rial_delta' => $rialDiff,
'overwrite' => $overwrite,
]);
return array_merge($comparison, [
'applied' => true,
'message' => 'مانده با ته‌حساب هم‌تراز شد',
]);
}
/**
* بعد از import: اگر کیف خالی باشد مانده را بنشان؛ اگر مانده دارد صبر کن تا ادمین تصمیم بگیرد.
*
* @param list<Customer> $customers
*/
public function syncAfterImport(array $customers): void
{
$customers = array_values(array_filter($customers, fn ($c) => $c instanceof Customer));
if ($customers === []) {
return;
}
$codes = [];
foreach ($customers as $customer) {
$code = (int) ($customer->moshtari_code ?? 0);
if ($code > 0) {
$codes[] = $code;
}
}
$mandes = $this->fetchMandeByCodes($codes);
foreach ($customers as $customer) {
try {
$code = (string) ((int) ($customer->moshtari_code ?? 0));
$this->syncWalletFromTahesab(
$customer,
$mandes[$code] ?? null,
['ask_if_conflict' => true]
);
} catch (\Throwable $e) {
Log::warning('Tahesab wallet sync after import failed', [
'customer_id' => $customer->id,
'moshtari_code' => $customer->moshtari_code,
'error' => $e->getMessage(),
]);
}
}
}
/**
* @param list<int> $codes
* @return array<string, array{gold: float, cash: float, raw: array}>
*/
private function requestMandeChunk(array $codes): array
{
$url = $this->apiUrl();
if ($url === '' || $codes === []) {
return [];
}
try {
$response = Http::withoutVerifying()->timeout(45)->withHeaders([
'DBName' => $this->config->getString('tahesab_db', ''),
'Content-Type' => 'application/json',
'Authorization' => 'Bearer '.$this->config->getString('tahesab_token', ''),
])->post($url, [
'GetMandeHesabByCode' => array_map('strval', $codes),
])->json();
} catch (\Throwable $e) {
Log::warning('Tahesab GetMandeHesabByCode failed', [
'codes' => $codes,
'error' => $e->getMessage(),
]);
return [];
}
if (! is_array($response)) {
return [];
}
$rows = $response['MandeHesab'] ?? null;
if (! is_array($rows)) {
if ($this->looksLikeMandeRow($response) && count($codes) === 1) {
$rows = [array_merge($response, ['Code' => (string) $codes[0]])];
} else {
$rows = array_is_list($response) ? $response : array_values($response);
}
}
$out = [];
foreach ($rows as $row) {
if (! is_array($row)) {
continue;
}
$code = (string) ($row['Code'] ?? $row['code'] ?? '');
if ($code === '' || $code === '0') {
continue;
}
$parsed = $this->parseMandeRow($row);
if ($parsed !== null) {
$out[$code] = $parsed;
}
}
if ($out === [] && count($codes) === 1 && $this->looksLikeMandeRow($response)) {
$parsed = $this->parseMandeRow($response);
if ($parsed !== null) {
$out[(string) $codes[0]] = $parsed;
}
}
return $out;
}
/**
* @param array<string, mixed> $row
* @return array{gold: float, cash: float, raw: array}|null
*/
private function parseMandeRow(array $row): ?array
{
$gold = $this->pickNumber($row, [
'MandeyeKolBeTALA',
'MandeyeKolBeTala',
'MandeyeVazni',
'MandeMenhayeSekeBeTala',
'GoldInv',
]);
$cash = $this->pickNumber($row, [
'MandeyeKolBePool',
'MandeyeMali',
'MandeMenhayeSekeBePool',
'RialInv',
]);
$hasAny = false;
foreach ([
'MandeyeKolBeTALA', 'MandeyeKolBeTala', 'MandeyeVazni', 'MandeyeKolBePool', 'MandeyeMali',
] as $key) {
if (array_key_exists($key, $row)) {
$hasAny = true;
break;
}
}
if (! $hasAny) {
return null;
}
return [
'gold' => $gold,
'cash' => $cash,
'raw' => $row,
];
}
/**
* @param array<string, mixed> $row
* @param list<string> $keys
*/
private function pickNumber(array $row, array $keys): float
{
foreach ($keys as $key) {
if (! array_key_exists($key, $row) || $row[$key] === null || $row[$key] === '') {
continue;
}
return $this->parseNumber($row[$key]);
}
return 0.0;
}
private function parseNumber(mixed $value): float
{
if (is_int($value) || is_float($value)) {
return (float) $value;
}
$s = str_replace([',', ' ', '٫', '/'], ['', '', '.', '.'], trim((string) $value));
return is_numeric($s) ? (float) $s : 0.0;
}
/** @param array<string, mixed> $row */
private function looksLikeMandeRow(array $row): bool
{
foreach (['MandeyeKolBeTALA', 'MandeyeKolBeTala', 'MandeyeVazni', 'MandeyeKolBePool', 'MandeyeMali'] as $key) {
if (array_key_exists($key, $row)) {
return true;
}
}
return false;
}
private function apiUrl(): string
{
$host = trim($this->config->getString('tahesab_host', ''));
$port = trim($this->config->getString('tahesab_port', ''));
if ($host !== '' && $port !== '') {
$host = rtrim(preg_replace('#^https?://#i', '', $host), '/');
return 'httpS://'.$host.':'.$port.'/';
}
$legacy = trim((string) env('TAHESAB', ''));
return $legacy !== '' ? $legacy : '';
}
}