$codes * @return 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 $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 $codes * @return 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 $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 $row * @param list $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 $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 : ''; } }