439 lines
14 KiB
PHP
439 lines
14 KiB
PHP
<?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;
|
||
}
|
||
}
|