2026-08-30 14:16:36 +03:30
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Models\Customer;
|
2026-08-30 21:18:24 +03:30
|
|
|
|
use App\Models\Tahesab;
|
2026-08-30 14:16:36 +03:30
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
|
use Rooyesh\Wallet\interfaces\TransactionInterface;
|
|
|
|
|
|
use Rooyesh\Wallet\interfaces\WalletInterface;
|
2026-08-30 21:18:24 +03:30
|
|
|
|
use Rooyesh\Wallet\models\Transaction;
|
|
|
|
|
|
use Rooyesh\Wallet\models\Wallet;
|
2026-08-30 14:16:36 +03:30
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* ماندهٔ طلا/ریال تهحساب → کیف اپ.
|
|
|
|
|
|
*
|
|
|
|
|
|
* منبع: GetMandeHesabByCode
|
|
|
|
|
|
* MandeyeKolBeTALA → طلای کیف اصلی
|
|
|
|
|
|
* MandeyeKolBePool → موجودی ریالی
|
|
|
|
|
|
*
|
|
|
|
|
|
* سیاست تصمیم ادمین:
|
|
|
|
|
|
* overwrite=true → اختلاف را اعمال کن
|
|
|
|
|
|
* keep=true → موجودی اپ را دست نزن
|
|
|
|
|
|
* ask_if_conflict → اگر اپ مانده دارد و با تهحساب فرق دارد، بدون نوشتن برگردان
|
2026-08-30 20:18:48 +03:30
|
|
|
|
* always_ask → هر اختلافی (حتی کیف خالی) بدون نوشتن برای تأیید ادمین برمیگردد
|
|
|
|
|
|
* جمع دو مانده هرگز انجام نمیشود تا موجودی دو برابر نشود.
|
2026-08-30 14:16:36 +03:30
|
|
|
|
*/
|
|
|
|
|
|
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
|
2026-08-30 20:18:48 +03:30
|
|
|
|
* @param array{overwrite?: bool, keep?: bool, ask_if_conflict?: bool, always_ask?: bool} $options
|
2026-08-30 14:16:36 +03:30
|
|
|
|
* @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);
|
2026-08-30 20:18:48 +03:30
|
|
|
|
$alwaysAsk = (bool) ($options['always_ask'] ?? false);
|
2026-08-30 14:16:36 +03:30
|
|
|
|
|
|
|
|
|
|
$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' => 'مانده از تهحساب دریافت نشد'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 20:18:48 +03:30
|
|
|
|
$compared = $this->compareWithWallet($customer, $mande);
|
|
|
|
|
|
$comparison = $compared['comparison'];
|
|
|
|
|
|
$goldSame = $compared['gold_same'];
|
|
|
|
|
|
$rialSame = $compared['rial_same'];
|
|
|
|
|
|
$appHasBalance = $compared['app_has_balance'];
|
2026-08-30 14:16:36 +03:30
|
|
|
|
|
|
|
|
|
|
if ($goldSame && $rialSame) {
|
|
|
|
|
|
return array_merge($comparison, [
|
|
|
|
|
|
'applied' => false,
|
|
|
|
|
|
'message' => 'مانده با تهحساب یکسان است',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($keep) {
|
|
|
|
|
|
return array_merge($comparison, [
|
|
|
|
|
|
'applied' => false,
|
|
|
|
|
|
'skipped' => true,
|
|
|
|
|
|
'message' => 'موجودی اپ حفظ شد — مانده تهحساب اعمال نشد',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 20:18:48 +03:30
|
|
|
|
$needsAsk = ! $overwrite && (
|
|
|
|
|
|
($alwaysAsk)
|
|
|
|
|
|
|| ($askIfConflict && $appHasBalance)
|
|
|
|
|
|
);
|
|
|
|
|
|
if ($needsAsk) {
|
2026-08-30 21:18:24 +03:30
|
|
|
|
return array_merge($comparison, $this->listPreviewDocuments($customer), [
|
2026-08-30 14:16:36 +03:30
|
|
|
|
'applied' => false,
|
|
|
|
|
|
'needs_decision' => true,
|
|
|
|
|
|
'message' => 'موجودی اپ با تهحساب فرق دارد — تصمیم ادمین لازم است',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 20:18:48 +03:30
|
|
|
|
$goldDiff = (float) $comparison['gold_delta'];
|
|
|
|
|
|
$rialDiff = (float) $comparison['rial_delta'];
|
|
|
|
|
|
$ownerType = (int) ($customer->type ?? 2);
|
|
|
|
|
|
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
|
|
|
|
|
|
2026-08-30 14:16:36 +03:30
|
|
|
|
$input = [
|
2026-08-30 21:18:24 +03:30
|
|
|
|
'title' => 'تعدیل حسابداری تهحساب',
|
|
|
|
|
|
'note' => $this->customerVisibleAdjustNote($code, $goldDiff, $rialDiff, $goldSame, $rialSame),
|
2026-08-30 14:16:36 +03:30
|
|
|
|
'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,
|
2026-08-30 20:18:48 +03:30
|
|
|
|
'gold_target' => $comparison['tahesab_gold'],
|
|
|
|
|
|
'cash_target' => $comparison['tahesab_cash'],
|
2026-08-30 14:16:36 +03:30
|
|
|
|
'gold_delta' => $goldDiff,
|
|
|
|
|
|
'rial_delta' => $rialDiff,
|
|
|
|
|
|
'overwrite' => $overwrite,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return array_merge($comparison, [
|
|
|
|
|
|
'applied' => true,
|
|
|
|
|
|
'message' => 'مانده با تهحساب همتراز شد',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 20:18:48 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* مقایسه بدون نوشتن — برای تأیید ادمین قبل از ست کردن کد.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array{
|
|
|
|
|
|
* applied: bool,
|
|
|
|
|
|
* needs_decision?: bool,
|
|
|
|
|
|
* ok?: bool,
|
|
|
|
|
|
* app_gold?: float,
|
|
|
|
|
|
* app_cash?: float,
|
|
|
|
|
|
* tahesab_gold?: float,
|
|
|
|
|
|
* tahesab_cash?: float,
|
|
|
|
|
|
* gold_delta?: float,
|
|
|
|
|
|
* rial_delta?: float,
|
|
|
|
|
|
* message: string
|
|
|
|
|
|
* }
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function previewForCustomer(Customer $customer, int $code): array
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($code <= 0) {
|
|
|
|
|
|
return ['applied' => false, 'ok' => false, 'message' => 'کد تهحساب نامعتبر است'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$all = $this->fetchMandeByCodes([$code]);
|
|
|
|
|
|
$mande = $all[(string) $code] ?? null;
|
|
|
|
|
|
if ($mande === null) {
|
|
|
|
|
|
return ['applied' => false, 'ok' => false, 'message' => 'مانده از تهحساب دریافت نشد'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$compared = $this->compareWithWallet($customer, $mande);
|
|
|
|
|
|
$same = $compared['gold_same'] && $compared['rial_same'];
|
|
|
|
|
|
|
2026-08-30 21:18:24 +03:30
|
|
|
|
return array_merge($compared['comparison'], $this->listPreviewDocuments($customer), [
|
2026-08-30 20:18:48 +03:30
|
|
|
|
'applied' => false,
|
|
|
|
|
|
'ok' => true,
|
|
|
|
|
|
'needs_decision' => ! $same,
|
|
|
|
|
|
'message' => $same
|
|
|
|
|
|
? 'مانده با تهحساب یکسان است'
|
|
|
|
|
|
: 'موجودی اپ با تهحساب فرق دارد — تصمیم ادمین لازم است',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 21:18:24 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* آخرین سندهای کیف اپ و تهحساب — برای تصمیم ادمین روی اختلاف مانده.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array{wallet_docs: list<array<string, mixed>>, tahesab_docs: list<array<string, mixed>>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function listPreviewDocuments(Customer $customer): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$ownerType = (int) ($customer->type ?? 2);
|
|
|
|
|
|
$wallet = Wallet::query()
|
|
|
|
|
|
->where('owner', (int) $customer->id)
|
|
|
|
|
|
->where('owner_type', $ownerType)
|
|
|
|
|
|
->whereNull('fund_id')
|
|
|
|
|
|
->whereNull('product_id')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
$walletDocs = [];
|
|
|
|
|
|
if ($wallet) {
|
|
|
|
|
|
$txs = Transaction::query()
|
|
|
|
|
|
->where('wallet', $wallet->id)
|
|
|
|
|
|
->orderByDesc('id')
|
|
|
|
|
|
->limit(8)
|
|
|
|
|
|
->get(['id', 'title', 'type', 'gold', 'cash', 'created_at']);
|
|
|
|
|
|
foreach ($txs as $tx) {
|
|
|
|
|
|
$typeKey = (int) $tx->getRawOriginal('type');
|
|
|
|
|
|
$typeLabel = Transaction::$TYPES[$typeKey] ?? '';
|
|
|
|
|
|
$walletDocs[] = [
|
|
|
|
|
|
'id' => (int) $tx->id,
|
|
|
|
|
|
'title' => (string) ($tx->title ?: $typeLabel ?: 'تراکنش کیف'),
|
|
|
|
|
|
'gold' => (float) ($tx->gold ?? 0),
|
|
|
|
|
|
'cash' => (float) ($tx->cash ?? 0),
|
|
|
|
|
|
'created_at' => optional($tx->created_at)->toDateTimeString(),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$tahesabDocs = [];
|
|
|
|
|
|
$rows = Tahesab::query()
|
|
|
|
|
|
->where('customer_id', (int) $customer->id)
|
|
|
|
|
|
->orderByDesc('id')
|
|
|
|
|
|
->limit(8)
|
|
|
|
|
|
->get(['id', 'desc', 'amount', 'type', 'factor_number', 'date', 'created_at']);
|
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
|
$title = trim((string) ($row->desc ?? ''));
|
|
|
|
|
|
if ($title === '' && $row->factor_number) {
|
|
|
|
|
|
$title = 'فاکتور '.$row->factor_number;
|
|
|
|
|
|
}
|
|
|
|
|
|
$tahesabDocs[] = [
|
|
|
|
|
|
'id' => (int) $row->id,
|
|
|
|
|
|
'title' => $title !== '' ? $title : 'سند تهحساب',
|
|
|
|
|
|
'amount' => (float) ($row->amount ?? 0),
|
|
|
|
|
|
'type' => (string) ($row->type ?? ''),
|
|
|
|
|
|
'date' => (string) ($row->date ?? ''),
|
|
|
|
|
|
'created_at' => optional($row->created_at)->toDateTimeString(),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'wallet_docs' => $walletDocs,
|
|
|
|
|
|
'tahesab_docs' => $tahesabDocs,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private function customerVisibleAdjustNote(
|
|
|
|
|
|
int $code,
|
|
|
|
|
|
float $goldDiff,
|
|
|
|
|
|
float $rialDiff,
|
|
|
|
|
|
bool $goldSame,
|
|
|
|
|
|
bool $rialSame
|
|
|
|
|
|
): string {
|
|
|
|
|
|
$parts = ['تعدیل مانده با تهحساب، کد '.$code];
|
|
|
|
|
|
if (! $goldSame) {
|
|
|
|
|
|
$sign = $goldDiff > 0 ? '+' : '';
|
|
|
|
|
|
$gold = rtrim(rtrim(sprintf('%.3f', $goldDiff), '0'), '.');
|
|
|
|
|
|
$parts[] = 'طلا '.$sign.$gold;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (! $rialSame) {
|
|
|
|
|
|
$sign = $rialDiff > 0 ? '+' : '';
|
|
|
|
|
|
$parts[] = 'ریال '.$sign.(int) round($rialDiff);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return implode('، ', $parts);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 20:18:48 +03:30
|
|
|
|
/** @param array<string, mixed> $row */
|
|
|
|
|
|
public function mandeFromAccountingRow(array $row): ?array
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->parseMandeRow($row);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 20:47:26 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* مقایسهٔ کیف اپ با ماندهٔ تهحساب برای لیست ادمین — بدون نوشتن.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param iterable<Customer> $customers
|
|
|
|
|
|
* @return list<array<string, mixed>>
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function compareCustomers(iterable $customers): array
|
|
|
|
|
|
{
|
|
|
|
|
|
$list = [];
|
|
|
|
|
|
foreach ($customers as $customer) {
|
|
|
|
|
|
if ($customer instanceof Customer) {
|
|
|
|
|
|
$list[] = $customer;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($list === []) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$codes = [];
|
|
|
|
|
|
foreach ($list as $customer) {
|
|
|
|
|
|
$code = (int) ($customer->moshtari_code ?? 0);
|
|
|
|
|
|
if ($code > 0) {
|
|
|
|
|
|
$codes[] = $code;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
$mandes = $this->fetchMandeByCodes($codes);
|
|
|
|
|
|
|
|
|
|
|
|
$rows = [];
|
|
|
|
|
|
foreach ($list as $customer) {
|
|
|
|
|
|
$code = (int) ($customer->moshtari_code ?? 0);
|
|
|
|
|
|
$mande = $code > 0 ? ($mandes[(string) $code] ?? null) : null;
|
|
|
|
|
|
$base = [
|
|
|
|
|
|
'id' => (int) $customer->id,
|
|
|
|
|
|
'name' => (string) ($customer->name ?? ''),
|
|
|
|
|
|
'mobile' => (string) ($customer->mobile ?? ''),
|
|
|
|
|
|
'moshtari_code' => $code > 0 ? $code : null,
|
|
|
|
|
|
];
|
|
|
|
|
|
if ($mande === null) {
|
|
|
|
|
|
$ownerType = (int) ($customer->type ?? 2);
|
|
|
|
|
|
$wallet = (new WalletInterface((int) $customer->id, $ownerType))->wallet;
|
|
|
|
|
|
$appGold = $this->walletBalance
|
|
|
|
|
|
->accountingVisibleMainWalletGold((int) $customer->id, $ownerType);
|
|
|
|
|
|
$appCash = (float) ($wallet->cash ?? 0);
|
|
|
|
|
|
$rows[] = array_merge($base, [
|
|
|
|
|
|
'app_gold' => $appGold,
|
|
|
|
|
|
'app_cash' => $appCash,
|
|
|
|
|
|
'tahesab_gold' => null,
|
|
|
|
|
|
'tahesab_cash' => null,
|
|
|
|
|
|
'gold_delta' => null,
|
|
|
|
|
|
'rial_delta' => null,
|
|
|
|
|
|
'status' => 'missing',
|
|
|
|
|
|
'message' => 'مانده از تهحساب دریافت نشد',
|
|
|
|
|
|
]);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$compared = $this->compareWithWallet($customer, $mande);
|
|
|
|
|
|
$same = $compared['gold_same'] && $compared['rial_same'];
|
|
|
|
|
|
$status = 'conflict';
|
|
|
|
|
|
if ($same) {
|
|
|
|
|
|
$status = 'same';
|
|
|
|
|
|
} elseif (! $compared['app_has_balance']) {
|
|
|
|
|
|
$status = 'ready';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$rows[] = array_merge($base, $compared['comparison'], [
|
|
|
|
|
|
'status' => $status,
|
|
|
|
|
|
'message' => $same
|
|
|
|
|
|
? 'مانده با تهحساب یکسان است'
|
|
|
|
|
|
: ($status === 'ready'
|
|
|
|
|
|
? 'کیف خالی است؛ همگامسازی مانده تهحساب را مینشاند'
|
|
|
|
|
|
: 'موجودی اپ با تهحساب فرق دارد'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $rows;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 20:18:48 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* @param array{gold?: float, cash?: float} $mande
|
|
|
|
|
|
* @return array{
|
|
|
|
|
|
* comparison: array{app_gold: float, app_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,
|
|
|
|
|
|
'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,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-30 14:16:36 +03:30
|
|
|
|
/**
|
|
|
|
|
|
* بعد از 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 : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|