Files
boursecu_server/app/Services/ZarinInboundWalletService.php
T

439 lines
14 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;
/**
* ماندهٔ طلا/ریال زرین → کیف اپ.
*
* منبع: listhesabha — فقط فیلدهای صریح طلا/ریال.
* اگر فیلد مبهم باشد (مثلاً فقط mande) چیزی نوشته نمی‌شود.
*
* سیاست: overwrite / keep / ask_if_conflict / always_ask — مثل ته‌حساب.
* جمع دو مانده هرگز انجام نمی‌شود.
*/
class ZarinInboundWalletService
{
public const TX_TYPE_ZARIN = 24;
private const EPSILON_GOLD = 0.0005;
private const EPSILON_CASH = 1.0;
/** @var array<string, array{gold: float, cash: float}>|null */
private ?array $mandeIndex = null;
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}>
*/
public function fetchMandeByCodes(array $codes): array
{
$index = $this->mandeIndex();
$out = [];
foreach ($codes as $code) {
$key = (string) ((int) $code);
if ($key !== '0' && isset($index[$key])) {
$out[$key] = $index[$key];
}
}
return $out;
}
/**
* @param array{overwrite?: bool, keep?: bool, ask_if_conflict?: bool, always_ask?: bool} $options
* @return array{
* applied: bool,
* needs_decision?: bool,
* skipped?: bool,
* gold_delta?: float,
* rial_delta?: float,
* app_gold?: float,
* app_cash?: float,
* zarin_gold?: float,
* zarin_cash?: float,
* tahesab_gold?: float,
* tahesab_cash?: float,
* message: string
* }
*/
public function syncWalletFromZarin(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);
$alwaysAsk = (bool) ($options['always_ask'] ?? 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' => 'مانده طلا/ریال زرین برای این حساب خوانده نشد'];
}
$compared = $this->compareWithWallet($customer, $mande);
$comparison = $compared['comparison'];
$goldSame = $compared['gold_same'];
$rialSame = $compared['rial_same'];
$appHasBalance = $compared['app_has_balance'];
if ($goldSame && $rialSame) {
return array_merge($comparison, [
'applied' => false,
'message' => 'مانده با زرین یکسان است',
]);
}
if ($keep) {
return array_merge($comparison, [
'applied' => false,
'skipped' => true,
'message' => 'موجودی اپ حفظ شد — مانده زرین اعمال نشد',
]);
}
$needsAsk = ! $overwrite && ($alwaysAsk || ($askIfConflict && $appHasBalance));
if ($needsAsk) {
return array_merge($comparison, [
'applied' => false,
'needs_decision' => true,
'message' => 'موجودی اپ با زرین فرق دارد — تصمیم ادمین لازم است',
]);
}
$goldDiff = (float) $comparison['gold_delta'];
$rialDiff = (float) $comparison['rial_delta'];
$ownerType = (int) ($customer->type ?? 2);
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
$input = [
'title' => 'تطبیق ماندهٔ زرین',
'note' => 'zarin-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_ZARIN,
$goldDiff > 0,
$rialDiff > 0,
0,
[],
true
);
$this->walletBroadcast->broadcastForCustomer((int) $customer->id, $ownerType);
Log::info('Zarin mande applied to wallet', [
'customer_id' => (int) $customer->id,
'moshtari_code' => $code,
'gold_target' => $comparison['zarin_gold'],
'cash_target' => $comparison['zarin_cash'],
'gold_delta' => $goldDiff,
'rial_delta' => $rialDiff,
'overwrite' => $overwrite,
]);
return array_merge($comparison, [
'applied' => true,
'message' => 'مانده با زرین هم‌تراز شد',
]);
}
/**
* @return array{
* applied: bool,
* needs_decision?: bool,
* ok?: bool,
* app_gold?: float,
* app_cash?: float,
* zarin_gold?: float,
* zarin_cash?: float,
* tahesab_gold?: float,
* tahesab_cash?: float,
* message: string
* }
*/
public function previewForCustomer(Customer $customer, int $code): array
{
if ($code <= 0) {
return ['applied' => false, 'ok' => false, 'message' => 'کد زرین نامعتبر است'];
}
$mande = $this->fetchMandeByCodes([$code])[(string) $code] ?? null;
if ($mande === null) {
return ['applied' => false, 'ok' => false, 'message' => 'مانده طلا/ریال زرین برای این حساب خوانده نشد'];
}
$compared = $this->compareWithWallet($customer, $mande);
$same = $compared['gold_same'] && $compared['rial_same'];
return array_merge($compared['comparison'], [
'applied' => false,
'ok' => true,
'needs_decision' => ! $same,
'message' => $same
? 'مانده با زرین یکسان است'
: 'موجودی اپ با زرین فرق دارد — تصمیم ادمین لازم است',
]);
}
/** @param array<string, mixed> $row */
public function mandeFromAccountingRow(array $row): ?array
{
return $this->parseMandeRow($row);
}
/**
* @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->syncWalletFromZarin(
$customer,
$mandes[$code] ?? null,
['ask_if_conflict' => true]
);
} catch (\Throwable $e) {
Log::warning('Zarin wallet sync after import failed', [
'customer_id' => $customer->id,
'moshtari_code' => $customer->moshtari_code,
'error' => $e->getMessage(),
]);
}
}
}
/**
* @param array{gold?: float, cash?: float} $mande
* @return array{
* comparison: array{app_gold: float, app_cash: float, zarin_gold: float, zarin_cash: float, tahesab_gold: float, tahesab_cash: float, gold_delta: float, rial_delta: float},
* gold_same: bool,
* rial_same: bool,
* app_has_balance: bool
* }
*/
private function compareWithWallet(Customer $customer, array $mande): array
{
$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;
return [
'comparison' => [
'app_gold' => $mainGold,
'app_cash' => $mainCash,
'zarin_gold' => $goldInv,
'zarin_cash' => $rialInv,
'tahesab_gold' => $goldInv,
'tahesab_cash' => $rialInv,
'gold_delta' => $goldDiff,
'rial_delta' => $rialDiff,
],
'gold_same' => abs($goldDiff) < self::EPSILON_GOLD,
'rial_same' => abs($rialDiff) < self::EPSILON_CASH,
'app_has_balance' => abs($mainGold) >= self::EPSILON_GOLD
|| abs($mainCash) >= self::EPSILON_CASH,
];
}
/**
* @return array<string, array{gold: float, cash: float}>
*/
private function mandeIndex(): array
{
if ($this->mandeIndex !== null) {
return $this->mandeIndex;
}
$this->mandeIndex = [];
foreach ($this->fetchListHesabha() as $item) {
if (! is_array($item)) {
continue;
}
$id = $this->extractHesabId($item);
if ($id === null) {
continue;
}
$parsed = $this->parseMandeRow($item);
if ($parsed !== null) {
$this->mandeIndex[(string) $id] = $parsed;
}
}
return $this->mandeIndex;
}
/** @return list<array<string, mixed>> */
private function fetchListHesabha(): array
{
$api = rtrim($this->config->getString('zarin_api', ''), '/');
$token = trim($this->config->getString('zarin_token', ''));
if ($api === '' || $token === '') {
return [];
}
$url = $api.'/ZarrinService/accounting';
try {
$response = Http::withoutVerifying()->timeout(45)->get($url, [
'kind' => 'listhesabha',
'token' => $token,
]);
} catch (\Throwable $e) {
Log::warning('Zarin listhesabha for mande failed', ['error' => $e->getMessage()]);
return [];
}
if (! $response->successful()) {
return [];
}
$json = $response->json();
if (! is_array($json)) {
return [];
}
$data = $json['data'] ?? $json['Data'] ?? [];
if (! is_array($data)) {
return [];
}
return array_is_list($data) ? $data : [$data];
}
/** @param array<string, mixed> $item */
private function extractHesabId(array $item): ?int
{
foreach (['idHesab', 'idhesab', 'Code', 'code', 'OK', 'id'] as $key) {
if (! array_key_exists($key, $item)) {
continue;
}
$value = $item[$key];
if (is_numeric($value) && (int) $value > 0) {
return (int) $value;
}
}
return null;
}
/**
* فقط کلیدهای صریح طلا و ریال — فیلد مبهم mande را طلا فرض نمی‌کنیم.
*
* @param array<string, mixed> $row
* @return array{gold: float, cash: float}|null
*/
private function parseMandeRow(array $row): ?array
{
$gold = $this->pickNumber($row, [
'GoldInv', 'goldInv', 'tala', 'Tala', 'mandeTala', 'MandeTala',
'mojodiTala', 'MojodiTala', 'vaznTala', 'VaznTala',
]);
$cash = $this->pickNumber($row, [
'RialInv', 'rialInv', 'pool', 'Pool', 'mandeRial', 'MandeRial',
'mojodiRial', 'MojodiRial', 'mandePool', 'MandePool',
]);
$hasGoldKey = $this->hasAnyKey($row, [
'GoldInv', 'goldInv', 'tala', 'Tala', 'mandeTala', 'MandeTala',
'mojodiTala', 'MojodiTala', 'vaznTala', 'VaznTala',
]);
$hasCashKey = $this->hasAnyKey($row, [
'RialInv', 'rialInv', 'pool', 'Pool', 'mandeRial', 'MandeRial',
'mojodiRial', 'MojodiRial', 'mandePool', 'MandePool',
]);
if (! $hasGoldKey && ! $hasCashKey) {
return null;
}
return [
'gold' => $gold,
'cash' => $cash,
];
}
/** @param list<string> $keys */
private function hasAnyKey(array $row, array $keys): bool
{
foreach ($keys as $key) {
if (array_key_exists($key, $row) && $row[$key] !== null && $row[$key] !== '') {
return true;
}
}
return false;
}
/** @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;
}
$s = str_replace([',', ' ', '٫', '/'], ['', '', '.', '.'], trim((string) $row[$key]));
if (is_numeric($s)) {
return (float) $s;
}
}
return 0.0;
}
}