Files
boursecu_server/app/Services/ZarinInboundWalletService.php
T

439 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}