Apply Tahesab account balances to wallets with admin overwrite control.

Add inbound mande sync on customer import plus per-customer and bulk wallet-sync endpoints so previously linked accounts can catch up.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 14:16:36 +03:30
co-authored by Cursor
parent 050bd26318
commit e059a4709e
4 changed files with 585 additions and 0 deletions
@@ -11,6 +11,8 @@ use Illuminate\Support\Facades\Cache;
use App\Models\Tahesab;
use App\Models\Customer;
use App\Services\AccountingProductDocMapper;
use App\Services\ConfigService;
use App\Services\TahesabInboundWalletService;
use App\Services\TahesabSyncService;
use Morilog\Jalali\Jalalian;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
@@ -237,6 +239,8 @@ class TahesabController extends Controller
$success = 0;
$failed = 0;
$errors = [];
/** @var list<Customer> $importedForWallet */
$importedForWallet = [];
foreach ($validated['customers'] as $item) {
$mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? '';
@@ -282,6 +286,7 @@ class TahesabController extends Controller
if ($changed) {
$existing->save();
}
$importedForWallet[] = $existing;
$success++;
continue;
}
@@ -300,6 +305,7 @@ class TahesabController extends Controller
$byMobile->password = $mobile;
}
$byMobile->save();
$importedForWallet[] = $byMobile;
$success++;
continue;
}
@@ -340,6 +346,7 @@ class TahesabController extends Controller
$customer->password = $mobile;
}
$customer->save();
$importedForWallet[] = $customer;
$success++;
} catch (\Throwable $e) {
$failed++;
@@ -353,6 +360,18 @@ class TahesabController extends Controller
}
}
// مانده طلا/ریال ته‌حساب را روی کیف مشتریان واردشده بنشان
if ($importedForWallet !== []) {
try {
app(TahesabInboundWalletService::class)->syncAfterImport($importedForWallet);
} catch (\Throwable $e) {
\Log::warning('Tahesab import wallet sync batch failed', [
'count' => count($importedForWallet),
'error' => $e->getMessage(),
]);
}
}
return response()->json([
'ok' => true,
'success' => $success,
@@ -452,6 +471,161 @@ class TahesabController extends Controller
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
/**
* همگام‌سازی مانده یک مشتری با ته‌حساب.
* body: overwrite=1 | keep=1 | ask_if_conflict=1 (پیش‌فرض ask برای جلوگیری از overwrite ناخواسته)
*/
public function syncCustomerWallet(Request $request, $customerId)
{
if (! app(ConfigService::class)->isTahesabAccounting()) {
return response()->json([
'ok' => false,
'message' => 'حسابداری ته‌حساب فعال نیست',
], 422);
}
$customer = Customer::findOrFail($customerId);
if ((int) ($customer->moshtari_code ?? 0) <= 0) {
return response()->json([
'ok' => false,
'message' => 'این مشتری کد ته‌حساب ندارد',
], 422);
}
$overwrite = $request->boolean('overwrite');
$keep = $request->boolean('keep');
// اگر صراحتاً overwrite/keep نباشد → از ادمین بپرس وقتی تداخل هست
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep);
$result = app(TahesabInboundWalletService::class)->syncWalletFromTahesab(
$customer,
null,
[
'overwrite' => $overwrite,
'keep' => $keep,
'ask_if_conflict' => $askIfConflict,
]
);
$needsDecision = (bool) ($result['needs_decision'] ?? false);
$applied = (bool) ($result['applied'] ?? false);
$skipped = (bool) ($result['skipped'] ?? false);
$same = ! $applied && ! $needsDecision && ! $skipped
&& str_contains((string) ($result['message'] ?? ''), 'یکسان');
return response()->json([
'ok' => $applied || $skipped || $same,
'needs_decision' => $needsDecision,
'message' => $result['message'] ?? '',
'result' => $result,
]);
}
/**
* همگام‌سازی مانده همه مشتریان دارای کد ته‌حساب — تکه‌تکه.
* body: overwrite=1 برای اعمال اجباری اختلاف‌ها؛ بدون آن فقط کیف خالی یا یکسان پردازش می‌شود و تداخل‌ها در needs_decision می‌آیند.
*/
public function syncAllWallets(Request $request)
{
if (! app(ConfigService::class)->isTahesabAccounting()) {
return response()->json([
'ok' => false,
'message' => 'حسابداری ته‌حساب فعال نیست',
], 422);
}
$limit = max(1, min(50, (int) $request->input('limit', 20)));
$offset = max(0, (int) $request->input('offset', 0));
$overwrite = $request->boolean('overwrite');
$base = Customer::query()
->whereNull('deleted_at')
->whereNotNull('moshtari_code')
->where('moshtari_code', '!=', '')
->where('moshtari_code', '!=', '0');
$total = (clone $base)->count();
$customers = (clone $base)
->orderBy('id')
->offset($offset)
->limit($limit)
->get(['id', 'name', 'mobile', 'moshtari_code', 'type']);
$walletService = app(TahesabInboundWalletService::class);
$codes = $customers->pluck('moshtari_code')->all();
$mandes = $walletService->fetchMandeByCodes($codes);
$success = 0;
$changed = 0;
$needsDecision = 0;
$failed = 0;
$errors = [];
$decisions = [];
foreach ($customers as $customer) {
try {
$code = (string) ((int) $customer->moshtari_code);
$result = $walletService->syncWalletFromTahesab(
$customer,
$mandes[$code] ?? null,
[
'overwrite' => $overwrite,
'ask_if_conflict' => ! $overwrite,
]
);
if ($result['needs_decision'] ?? false) {
$needsDecision++;
$decisions[] = [
'id' => $customer->id,
'name' => $customer->name,
'mobile' => $customer->mobile,
'app_gold' => $result['app_gold'] ?? null,
'app_cash' => $result['app_cash'] ?? null,
'tahesab_gold' => $result['tahesab_gold'] ?? null,
'tahesab_cash' => $result['tahesab_cash'] ?? null,
'message' => $result['message'] ?? '',
];
continue;
}
$success++;
if ($result['applied'] ?? false) {
$changed++;
}
} catch (\Throwable $e) {
$failed++;
$errors[] = [
'id' => $customer->id,
'name' => $customer->name,
'mobile' => $customer->mobile,
'error' => $e->getMessage() !== '' ? $e->getMessage() : 'خطای ناشناخته',
];
}
}
$processed = $customers->count();
$nextOffset = $offset + $processed;
return response()->json([
'ok' => true,
'message' => $total === 0
? 'مشتری دارای کد ته‌حساب یافت نشد'
: "دستهٔ {$offset} تا {$nextOffset} از {$total}",
'total' => $total,
'offset' => $offset,
'processed' => $processed,
'next_offset' => $nextOffset,
'done' => $nextOffset >= $total,
'success' => $success,
'changed' => $changed,
'needs_decision' => $needsDecision,
'failed' => $failed,
'errors' => $errors,
'decisions' => $decisions,
]);
}
private function tahesabApiUrl(): string
{
$host = trim((string) $this->Config('tahesab_host'));
@@ -184,6 +184,9 @@ class WalletController extends Controller
if ((int) ($record->type ?? 0) === 22) {
$record->title = 'تعدیل حسابداری پرگار';
$record->unit = 'گرم';
} elseif ((int) ($record->type ?? 0) === 23) {
$record->title = 'تعدیل حسابداری ته‌حساب';
$record->unit = 'گرم';
} elseif ((int) ($record->type ?? 0) === 12) {
$record->title = 'برداشت وجه نقد';
$record->unit = 'ریال';
@@ -883,6 +886,10 @@ class WalletController extends Controller
return 'تعدیل حسابداری پرگار';
}
if ((int) $record->type === 23) {
return 'تعدیل حسابداری ته‌حساب';
}
return $record->type_str;
}