added some feature for accounting and enhance performance for dashboard

This commit is contained in:
2026-08-30 20:18:48 +03:30
parent e059a4709e
commit 283f17561c
6 changed files with 1251 additions and 84 deletions
+275 -27
View File
@@ -84,7 +84,8 @@ class TahesabController extends Controller
])->json();
if (! is_array($response)) return [];
return $response;
return $this->tahesabCustomerRows($response);
} catch (\Exception $e) {
return [];
}
@@ -113,44 +114,83 @@ class TahesabController extends Controller
$tahesabOnlyCount = null;
$tahesabOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1];
$tahesabWithPhoneCount = null;
$tahesabWithoutPhoneCount = null;
$tahesabImportableCount = null;
$phoneMatchCount = null;
$phoneConflictCount = null;
$phoneMatches = [];
$phoneConflicts = [];
if ($request->boolean('include_tahesab')) {
$tahesabCustomers = $this->fetchAllTahesabCustomers();
$tahesabTotal = count($tahesabCustomers);
// موبایل‌های پنل بدون صفر ابتدایی
$panelMobiles = Customer::whereNull('deleted_at')
->whereNotNull('mobile')
->pluck('mobile')
->mapWithKeys(fn ($m) => [ltrim((string) $m, '0') => true])
->toArray();
// کدهای مشتریِ قبلاً ثبت‌شده در پنل (برای جلوگیری از تکرار)
$panelCodes = Customer::whereNull('deleted_at')
->whereNotNull('moshtari_code')
->pluck('moshtari_code')
->mapWithKeys(fn ($c) => [(string) $c => true])
->toArray();
$walletService = app(TahesabInboundWalletService::class);
$maps = $this->panelAccountingMaps();
$tahesabOnlyList = [];
$debugSample = [];
$withPhone = 0;
$withoutPhone = 0;
foreach ($tahesabCustomers as $item) {
if (! is_array($item)) continue;
if (count($debugSample) < 3) $debugSample[] = $item; // نمونهٔ خام برای تشخیص نام فیلدها
$code = $item['Code'] ?? null;
if ($code !== null && isset($panelCodes[(string) $code])) continue;
if (count($debugSample) < 3) $debugSample[] = $item;
$code = $this->extractTahesabCode($item);
$codeKey = $this->accountingCodeKey($code);
$phone = $this->extractTahesabPhone($item);
$tel = ltrim($phone, '0');
if ($tel && isset($panelMobiles[$tel])) continue;
$hasPhone = Customer::normalizeMobile($phone) !== null;
if ($hasPhone) {
$withPhone++;
} else {
$withoutPhone++;
}
if ($codeKey !== null && isset($maps['by_code'][$codeKey])) {
continue;
}
$mobileKey = Customer::normalizeMobile($phone);
if ($mobileKey && isset($maps['by_mobile'][$mobileKey])) {
$panel = $maps['by_mobile'][$mobileKey];
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
$row = [
'code' => $code !== null ? (int) $code : null,
'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')),
'tel' => $phone,
'panel_id' => (int) $panel->id,
'panel_name' => $panel->name,
'panel_code' => $panelCodeKey,
];
if ($panelCodeKey && $panelCodeKey !== $codeKey) {
$phoneConflicts[] = $row;
} else {
$phoneMatches[] = $row;
}
continue;
}
if ($codeKey === null) {
continue;
}
$mande = $walletService->mandeFromAccountingRow($item);
$tahesabOnlyList[] = [
'code' => $item['Code'] ?? null,
'name' => $item['Name'] ?? '',
'code' => $code !== null ? (int) $code : null,
'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')),
'tel' => $phone,
'gid' => $item['GID'] ?? null,
'group' => $item['GoroupName'] ?? '',
'gid' => $item['GID'] ?? $item['gid'] ?? null,
'group' => $item['GoroupName'] ?? $item['group'] ?? '',
'has_phone' => $hasPhone,
'gold' => $mande['gold'] ?? null,
'cash' => $mande['cash'] ?? null,
];
}
$tahesabOnlyCount = count($tahesabOnlyList);
// همه رو برمی‌گردونیم — frontend خودش paginate می‌کنه
$tahesabWithPhoneCount = $withPhone;
$tahesabWithoutPhoneCount = $withoutPhone;
$tahesabImportableCount = count(array_filter($tahesabOnlyList, fn ($r) => ! empty($r['has_phone'])));
$phoneMatchCount = count($phoneMatches);
$phoneConflictCount = count($phoneConflicts);
$tahesabOnlyResult = [
'data' => array_values($tahesabOnlyList),
'total' => $tahesabOnlyCount,
@@ -165,6 +205,13 @@ class TahesabController extends Controller
'panel_only' => $panelOnly,
'tahesab_total' => $tahesabTotal,
'tahesab_only_count' => $tahesabOnlyCount,
'tahesab_with_phone_count' => $tahesabWithPhoneCount,
'tahesab_without_phone_count' => $tahesabWithoutPhoneCount,
'tahesab_importable_count' => $tahesabImportableCount,
'phone_match_count' => $phoneMatchCount,
'phone_conflict_count' => $phoneConflictCount,
'phone_matches' => $phoneMatches,
'phone_conflicts' => $phoneConflicts,
'panel_only_list' => $panelOnlyList,
'tahesab_only_list' => $tahesabOnlyResult,
];
@@ -198,6 +245,206 @@ class TahesabController extends Controller
return '';
}
/**
* @param array<string, mixed> $response
* @return list<array<string, mixed>>
*/
private function tahesabCustomerRows(array $response): array
{
if ($response === []) {
return [];
}
if (isset($response[0]) && is_array($response[0])) {
return array_values(array_filter($response, 'is_array'));
}
foreach (['Moshtari', 'ListMoshtari', 'data', 'Data'] as $key) {
if (isset($response[$key]) && is_array($response[$key])) {
return array_values(array_filter($response[$key], 'is_array'));
}
}
return array_values(array_filter($response, 'is_array'));
}
/** @param array<string, mixed> $item */
private function extractTahesabCode(array $item): mixed
{
foreach (['Code', 'code', 'MoshtariCode', 'moshtari_code'] as $key) {
if (isset($item[$key]) && $item[$key] !== '' && $item[$key] !== null) {
return $item[$key];
}
}
return null;
}
private function accountingCodeKey(mixed $code): ?string
{
if ($code === null || $code === '' || $code === false) {
return null;
}
$s = trim((string) $code);
if ($s === '' || $s === '0' || $s === '-') {
return null;
}
if (is_numeric($s)) {
$n = (int) $s;
return $n > 0 ? (string) $n : null;
}
return $s;
}
/**
* @return array{by_code: array<string, Customer>, by_mobile: array<string, Customer>}
*/
private function panelAccountingMaps(): array
{
$byCode = [];
$byMobile = [];
$rows = Customer::whereNull('deleted_at')->get(['id', 'name', 'mobile', 'moshtari_code', 'type']);
foreach ($rows as $row) {
$codeKey = $this->accountingCodeKey($row->moshtari_code);
if ($codeKey !== null) {
$byCode[$codeKey] = $row;
}
$mobile = Customer::normalizeMobile((string) ($row->mobile ?? ''));
if ($mobile !== null) {
$byMobile[$mobile] = $row;
}
}
return ['by_code' => $byCode, 'by_mobile' => $byMobile];
}
/** لینک کد ته‌حساب روی مشتری پنل وقتی شماره یکسان است */
public function linkMatchingPhones(): \Illuminate\Http\JsonResponse
{
$tahesabCustomers = $this->fetchAllTahesabCustomers();
$maps = $this->panelAccountingMaps();
$linked = [];
$conflicts = [];
$toSync = [];
foreach ($tahesabCustomers as $item) {
if (! is_array($item)) {
continue;
}
$code = $this->extractTahesabCode($item);
$codeKey = $this->accountingCodeKey($code);
if ($codeKey === null) {
continue;
}
$phone = $this->extractTahesabPhone($item);
$mobileKey = Customer::normalizeMobile($phone);
if ($mobileKey === null || ! isset($maps['by_mobile'][$mobileKey])) {
continue;
}
$panel = $maps['by_mobile'][$mobileKey];
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
if (isset($maps['by_code'][$codeKey]) && (int) $maps['by_code'][$codeKey]->id !== (int) $panel->id) {
$conflicts[] = [
'code' => (int) $codeKey,
'name' => trim((string) ($item['Name'] ?? '')),
'tel' => $phone,
'panel_id' => (int) $panel->id,
'panel_name' => $panel->name,
'error' => 'این کد ته‌حساب روی مشتری دیگری در پنل است',
];
continue;
}
if ($panelCodeKey === $codeKey) {
continue;
}
if ($panelCodeKey) {
$conflicts[] = [
'code' => (int) $codeKey,
'name' => trim((string) ($item['Name'] ?? '')),
'tel' => $phone,
'panel_id' => (int) $panel->id,
'panel_name' => $panel->name,
'panel_code' => $panelCodeKey,
'error' => 'مشتری پنل کد دیگری دارد — دستی تصمیم بگیرید',
];
continue;
}
$panel->moshtari_code = (int) $codeKey;
$panel->save();
$maps['by_code'][$codeKey] = $panel;
$toSync[] = $panel;
$linked[] = [
'code' => (int) $codeKey,
'name' => $panel->name,
'tel' => $phone,
'panel_id' => (int) $panel->id,
];
}
if ($toSync !== []) {
try {
app(TahesabInboundWalletService::class)->syncAfterImport($toSync);
} catch (\Throwable $e) {
\Log::warning('Tahesab link-by-phone wallet sync failed', ['error' => $e->getMessage()]);
}
}
return response()->json([
'ok' => true,
'linked' => count($linked),
'conflicts' => count($conflicts),
'linked_rows' => $linked,
'conflict_rows' => $conflicts,
'message' => count($linked)
? (count($linked).' کد روی مشتری پنل با شماره یکسان نشست')
: (count($conflicts) ? 'کد یکسانی برای لینک خودکار نبود' : 'شماره یکسانی برای لینک پیدا نشد'),
]);
}
public function previewCustomerWallet(Request $request, $customerId): \Illuminate\Http\JsonResponse
{
if (! app(ConfigService::class)->isTahesabAccounting()) {
return response()->json(['ok' => false, 'message' => 'حسابداری ته‌حساب فعال نیست'], 422);
}
$customer = Customer::findOrFail($customerId);
$code = (int) $request->input('code', $customer->moshtari_code);
$result = app(TahesabInboundWalletService::class)->previewForCustomer($customer, $code);
$needsDecision = (bool) ($result['needs_decision'] ?? false);
return response()->json([
'ok' => (bool) ($result['ok'] ?? false),
'needs_decision' => $needsDecision,
'message' => $result['message'] ?? '',
'result' => $result,
]);
}
public function mandeByCodes(Request $request): \Illuminate\Http\JsonResponse
{
$codes = $request->input('codes', []);
if (! is_array($codes) || $codes === []) {
return response()->json(['ok' => true, 'mandes' => []]);
}
if (count($codes) > 50) {
return response()->json(['ok' => false, 'message' => 'حداکثر ۵۰ کد در هر درخواست'], 422);
}
$mandes = app(TahesabInboundWalletService::class)->fetchMandeByCodes($codes);
$out = [];
foreach ($mandes as $code => $row) {
$out[(string) $code] = [
'gold' => $row['gold'] ?? 0,
'cash' => $row['cash'] ?? 0,
];
}
return response()->json(['ok' => true, 'mandes' => $out]);
}
public function syncPanelToTahesab(): \Illuminate\Http\JsonResponse
{
$customers = Customer::whereNull('deleted_at')
@@ -494,8 +741,8 @@ class TahesabController extends Controller
$overwrite = $request->boolean('overwrite');
$keep = $request->boolean('keep');
// اگر صراحتاً overwrite/keep نباشد → از ادمین بپرس وقتی تداخل هست
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep);
$alwaysAsk = $request->boolean('always_ask');
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk);
$result = app(TahesabInboundWalletService::class)->syncWalletFromTahesab(
$customer,
@@ -504,6 +751,7 @@ class TahesabController extends Controller
'overwrite' => $overwrite,
'keep' => $keep,
'ask_if_conflict' => $askIfConflict,
'always_ask' => $alwaysAsk,
]
);
+379 -24
View File
@@ -9,6 +9,8 @@ use App\Models\Order;
use App\Models\Product;
use App\Models\Zarin;
use App\Services\AccountingProductDocMapper;
use App\Services\ConfigService;
use App\Services\ZarinInboundWalletService;
use App\Services\ZarinSyncService;
use Carbon\Carbon;
use Illuminate\Contracts\Cache\LockTimeoutException;
@@ -1056,20 +1058,14 @@ class ZarinController extends Controller
}
$zarinTotal = count($zarinCustomers);
$panelMobiles = Customer::whereNull('deleted_at')
->whereNotNull('mobile')
->pluck('mobile')
->mapWithKeys(fn ($m) => [ltrim((string) $m, '0') => true])
->toArray();
$panelCodes = Customer::whereNull('deleted_at')
->whereNotNull('moshtari_code')
->pluck('moshtari_code')
->mapWithKeys(fn ($c) => [(string) $c => true])
->toArray();
$maps = $this->panelAccountingMaps();
$walletService = app(ZarinInboundWalletService::class);
$zarinOnlyList = [];
$phoneMatches = [];
$phoneConflicts = [];
$withPhone = 0;
$withoutPhone = 0;
foreach ($zarinCustomers as $item) {
if (!is_array($item)) {
continue;
@@ -1077,21 +1073,54 @@ class ZarinController extends Controller
if (count($debugSample) < 3) {
$debugSample[] = $item;
}
$code = $item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null;
if ($code !== null && isset($panelCodes[(string) $code])) {
continue;
}
$code = $this->extractZarinHesabId($item) ?? ($item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null);
$codeKey = $this->accountingCodeKey($code);
$phone = $this->extractZarinPhone($item);
$tel = ltrim($phone, '0');
if ($tel && isset($panelMobiles[$tel])) {
$hasPhone = Customer::normalizeMobile($phone) !== null;
if ($hasPhone) {
$withPhone++;
} else {
$withoutPhone++;
}
if ($codeKey !== null && isset($maps['by_code'][$codeKey])) {
continue;
}
$mobileKey = Customer::normalizeMobile($phone);
if ($mobileKey && isset($maps['by_mobile'][$mobileKey])) {
$panel = $maps['by_mobile'][$mobileKey];
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
$row = [
'code' => $codeKey !== null ? (int) $codeKey : null,
'name' => $this->extractZarinName($item),
'tel' => $phone,
'panel_id' => (int) $panel->id,
'panel_name' => $panel->name,
'panel_code' => $panelCodeKey,
];
if ($panelCodeKey && $panelCodeKey !== $codeKey) {
$phoneConflicts[] = $row;
} else {
$phoneMatches[] = $row;
}
continue;
}
if ($codeKey === null) {
continue;
}
$mande = $walletService->mandeFromAccountingRow($item);
$zarinOnlyList[] = [
'code' => $code,
'code' => (int) $codeKey,
'name' => $this->extractZarinName($item),
'tel' => $phone,
'gid' => $item['GID'] ?? null,
'group' => $this->extractZarinGroup($item),
'has_phone' => $hasPhone,
'gold' => $mande['gold'] ?? null,
'cash' => $mande['cash'] ?? null,
];
}
$zarinOnlyCount = count($zarinOnlyList);
@@ -1109,6 +1138,15 @@ class ZarinController extends Controller
'panel_only' => $panelOnly,
'zarin_total' => $zarinTotal,
'zarin_only_count' => $zarinOnlyCount,
'zarin_with_phone_count' => $include ? ($withPhone ?? 0) : null,
'zarin_without_phone_count' => $include ? ($withoutPhone ?? 0) : null,
'zarin_importable_count' => $include
? count(array_filter($zarinOnlyList ?? [], fn ($r) => ! empty($r['has_phone'])))
: null,
'phone_match_count' => $include ? count($phoneMatches ?? []) : null,
'phone_conflict_count' => $include ? count($phoneConflicts ?? []) : null,
'phone_matches' => $phoneMatches ?? [],
'phone_conflicts' => $phoneConflicts ?? [],
'zarin_fetch_error' => $zarinFetchError,
'zarin_http_status' => $zarinHttpStatus,
'zarin_group_filter' => $zarinGroupFilter,
@@ -1166,6 +1204,46 @@ class ZarinController extends Controller
return null;
}
private function accountingCodeKey(mixed $code): ?string
{
if ($code === null || $code === '' || $code === false) {
return null;
}
$s = trim((string) $code);
if ($s === '' || $s === '0' || $s === '-') {
return null;
}
if (is_numeric($s)) {
$n = (int) $s;
return $n > 0 ? (string) $n : null;
}
return $s;
}
/**
* @return array{by_code: array<string, Customer>, by_mobile: array<string, Customer>}
*/
private function panelAccountingMaps(): array
{
$byCode = [];
$byMobile = [];
$rows = Customer::whereNull('deleted_at')->get(['id', 'name', 'mobile', 'moshtari_code', 'type']);
foreach ($rows as $row) {
$codeKey = $this->accountingCodeKey($row->moshtari_code);
if ($codeKey !== null) {
$byCode[$codeKey] = $row;
}
$mobile = Customer::normalizeMobile((string) ($row->mobile ?? ''));
if ($mobile !== null) {
$byMobile[$mobile] = $row;
}
}
return ['by_code' => $byCode, 'by_mobile' => $byMobile];
}
/** حساب از قبل ساخته‌شده در زرین — با موبایل، تا addhesab دوم نسازیم */
private function findExistingZarinHesabId(?string $mobile): ?int
{
@@ -1261,6 +1339,8 @@ class ZarinController extends Controller
$success = 0;
$failed = 0;
$errors = [];
/** @var list<Customer> $importedForWallet */
$importedForWallet = [];
foreach ($validated['customers'] as $item) {
$mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? '';
@@ -1271,7 +1351,16 @@ class ZarinController extends Controller
$fullName = 'مشتری زرین ' . $code;
}
// موبایل برای خیلی از حساب‌های زرین خالی است (مثل تستی هوم) — مانع import نیست
if ($mobile === '') {
$failed++;
$errors[] = [
'name' => $item['name'],
'code' => $item['code'],
'error' => 'شماره موبایل خالی است — وارد نشد',
];
continue;
}
$existing = Customer::whereNull('deleted_at')->where('moshtari_code', $code)->first();
if ($existing) {
$changed = false;
@@ -1300,6 +1389,7 @@ class ZarinController extends Controller
if ($changed) {
$existing->save();
}
$importedForWallet[] = $existing;
$success++;
continue;
}
@@ -1318,6 +1408,7 @@ class ZarinController extends Controller
$byMobile->password = $mobile;
}
$byMobile->save();
$importedForWallet[] = $byMobile;
$success++;
continue;
}
@@ -1345,15 +1436,14 @@ class ZarinController extends Controller
$customer->fname = $fullName;
$customer->lname = '';
$customer->name = $fullName;
$customer->mobile = $mobile !== '' ? $mobile : null;
$customer->mobile = $mobile;
$customer->active = true;
$customer->moshtari_code = $item['code'];
$customer->referral_id = $referralId;
$customer->type = 2;
if ($mobile !== '') {
$customer->password = $mobile;
}
$customer->password = $mobile;
$customer->save();
$importedForWallet[] = $customer;
$success++;
} catch (\Throwable $e) {
$failed++;
@@ -1367,6 +1457,17 @@ class ZarinController extends Controller
}
}
if ($importedForWallet !== []) {
try {
app(ZarinInboundWalletService::class)->syncAfterImport($importedForWallet);
} catch (\Throwable $e) {
Log::warning('Zarin import wallet sync batch failed', [
'count' => count($importedForWallet),
'error' => $e->getMessage(),
]);
}
}
return response()->json([
'ok' => true,
'success' => $success,
@@ -1375,6 +1476,260 @@ class ZarinController extends Controller
]);
}
public function linkMatchingPhones(): \Illuminate\Http\JsonResponse
{
$fetched = $this->fetchAllZarinCustomers();
$rows = $fetched['rows'] ?? [];
$groupFilter = $this->zarinCustomerGroupName();
if ($groupFilter !== '' && $rows !== []) {
$rows = array_values(array_filter($rows, function ($item) use ($groupFilter) {
if (! is_array($item)) {
return false;
}
$g = $this->extractZarinGroup($item);
return $g !== '' && $this->zarinGroupNamesMatch($g, $groupFilter);
}));
}
$maps = $this->panelAccountingMaps();
$linked = [];
$conflicts = [];
$toSync = [];
foreach ($rows as $item) {
if (! is_array($item)) {
continue;
}
$code = $this->extractZarinHesabId($item);
$codeKey = $this->accountingCodeKey($code);
if ($codeKey === null) {
continue;
}
$phone = $this->extractZarinPhone($item);
$mobileKey = Customer::normalizeMobile($phone);
if ($mobileKey === null || ! isset($maps['by_mobile'][$mobileKey])) {
continue;
}
$panel = $maps['by_mobile'][$mobileKey];
$panelCodeKey = $this->accountingCodeKey($panel->moshtari_code);
if (isset($maps['by_code'][$codeKey]) && (int) $maps['by_code'][$codeKey]->id !== (int) $panel->id) {
$conflicts[] = [
'code' => (int) $codeKey,
'name' => $this->extractZarinName($item),
'tel' => $phone,
'panel_id' => (int) $panel->id,
'panel_name' => $panel->name,
'error' => 'این کد زرین روی مشتری دیگری در پنل است',
];
continue;
}
if ($panelCodeKey === $codeKey) {
continue;
}
if ($panelCodeKey) {
$conflicts[] = [
'code' => (int) $codeKey,
'name' => $this->extractZarinName($item),
'tel' => $phone,
'panel_id' => (int) $panel->id,
'panel_name' => $panel->name,
'panel_code' => $panelCodeKey,
'error' => 'مشتری پنل کد دیگری دارد — دستی تصمیم بگیرید',
];
continue;
}
$panel->moshtari_code = (int) $codeKey;
$panel->save();
$maps['by_code'][$codeKey] = $panel;
$toSync[] = $panel;
$linked[] = [
'code' => (int) $codeKey,
'name' => $panel->name,
'tel' => $phone,
'panel_id' => (int) $panel->id,
];
}
if ($toSync !== []) {
try {
app(ZarinInboundWalletService::class)->syncAfterImport($toSync);
} catch (\Throwable $e) {
Log::warning('Zarin link-by-phone wallet sync failed', ['error' => $e->getMessage()]);
}
}
return response()->json([
'ok' => true,
'linked' => count($linked),
'conflicts' => count($conflicts),
'linked_rows' => $linked,
'conflict_rows' => $conflicts,
'message' => count($linked)
? (count($linked).' کد روی مشتری پنل با شماره یکسان نشست')
: (count($conflicts) ? 'کد یکسانی برای لینک خودکار نبود' : 'شماره یکسانی برای لینک پیدا نشد'),
]);
}
public function previewCustomerWallet(Request $request, $customerId): \Illuminate\Http\JsonResponse
{
if (! app(ConfigService::class)->isZarinAccounting()) {
return response()->json(['ok' => false, 'message' => 'حسابداری زرین فعال نیست'], 422);
}
$customer = Customer::findOrFail($customerId);
$code = (int) $request->input('code', $customer->moshtari_code);
$result = app(ZarinInboundWalletService::class)->previewForCustomer($customer, $code);
return response()->json([
'ok' => (bool) ($result['ok'] ?? false),
'needs_decision' => (bool) ($result['needs_decision'] ?? false),
'message' => $result['message'] ?? '',
'result' => $result,
]);
}
public function syncCustomerWallet(Request $request, $customerId)
{
if (! app(ConfigService::class)->isZarinAccounting()) {
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');
$alwaysAsk = $request->boolean('always_ask');
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk);
$result = app(ZarinInboundWalletService::class)->syncWalletFromZarin(
$customer,
null,
[
'overwrite' => $overwrite,
'keep' => $keep,
'ask_if_conflict' => $askIfConflict,
'always_ask' => $alwaysAsk,
]
);
$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,
]);
}
public function syncAllWallets(Request $request)
{
if (! app(ConfigService::class)->isZarinAccounting()) {
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(ZarinInboundWalletService::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->syncWalletFromZarin(
$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['zarin_gold'] ?? $result['tahesab_gold'] ?? null,
'tahesab_cash' => $result['zarin_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,
]);
}
/**
* حذف مشتریانِ خرابِ واردشده از زرین (نام خالی) برای پاک‌سازی import ناقص.
* با dry_run=1 فقط تعداد و نمونه را نشان می‌دهد و چیزی حذف نمی‌کند.