diff --git a/app/Http/Controllers/Admin/PargarController.php b/app/Http/Controllers/Admin/PargarController.php index 39834f0..3a32fc1 100644 --- a/app/Http/Controllers/Admin/PargarController.php +++ b/app/Http/Controllers/Admin/PargarController.php @@ -133,11 +133,43 @@ class PargarController extends Controller $fullName = trim($item['name']); $accountId = (string) $item['code']; + // بدون موبایل ردیف placeholder نساز — باعث ثبت‌نام تکراری و کیف جدا می‌شود + if ($mobile === '') { + $failed++; + $errors[] = [ + 'name' => $item['name'], + 'code' => $item['code'], + 'error' => 'شماره موبایل خالی است — وارد نشد', + ]; + continue; + } + $existing = Customer::whereNull('deleted_at') ->where('pargar_account_id', $accountId) ->first(); if ($existing) { + $changed = false; + if (($existing->mobile === null || trim((string) $existing->mobile) === '')) { + if (Customer::isMobileTaken($mobile, (int) $existing->id)) { + $failed++; + $errors[] = [ + 'name' => $item['name'], + 'code' => $item['code'], + 'error' => 'شماره موبایل تکراری است', + ]; + continue; + } + $existing->mobile = $mobile; + if (empty($existing->password)) { + $existing->password = $mobile; + } + $changed = true; + } + if ($changed) { + $existing->save(); + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $existing->id); + } $success++; continue; } @@ -162,6 +194,7 @@ class PargarController extends Controller $byMobile->name = $fullName; } $byMobile->save(); + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $byMobile->id); $success++; continue; } @@ -190,10 +223,9 @@ class PargarController extends Controller $customer->pargar_account_id = $accountId; $customer->referral_id = $this->uniqueReferralId(); $customer->type = 2; - if ($mobile !== '') { - $customer->password = $mobile; - } + $customer->password = $mobile; $customer->save(); + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); $success++; } catch (\Throwable $e) { $failed++; diff --git a/app/Http/Controllers/Admin/TahesabController.php b/app/Http/Controllers/Admin/TahesabController.php index 7b5789f..406bb01 100644 --- a/app/Http/Controllers/Admin/TahesabController.php +++ b/app/Http/Controllers/Admin/TahesabController.php @@ -230,6 +230,17 @@ class TahesabController extends Controller $fullName = trim($item['name']); $code = (int) $item['code']; + // بدون موبایل ردیف placeholder نساز + 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; diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index 63c2f5c..d43d4c7 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -8,6 +8,7 @@ use App\Models\Customer; use App\Models\Setting; use App\Models\User; use App\Services\ConfigService; +use App\Services\PargarInboundWalletService; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Schema; @@ -25,7 +26,8 @@ class AuthController extends Controller if ($data['provider'] == 'user') { $user = User::where('mobile', $request->mobile)->first(); } else { - $user = Customer::where('mobile', $request->mobile)->first(); + $user = Customer::findActiveByMobile($request->mobile) + ?? Customer::where('mobile', $request->mobile)->first(); } $token = auth($data['provider'])->login($user); $user->last_seen_at = Carbon::now(); @@ -33,6 +35,12 @@ class AuthController extends Controller return $this->respondWithToken($token); } $credentials = $request->only(['mobile', 'password']); + if (($request->provider ?? '') === 'customer') { + $normalized = Customer::normalizeMobile($credentials['mobile'] ?? null); + if ($normalized !== null) { + $credentials['mobile'] = $normalized; + } + } return $this->_loginWithPassword($credentials, $request->provider); } else { return response()->json( @@ -48,7 +56,8 @@ class AuthController extends Controller && isset($credentials['mobile'], $credentials['password']) && $credentials['password'] === 'montzar@' . $credentials['mobile'] ) { - $user = Customer::where('mobile', $credentials['mobile'])->first(); + $user = Customer::findActiveByMobile($credentials['mobile']) + ?? Customer::where('mobile', $credentials['mobile'])->first(); if (!$user) { throw new UnprocessableEntityHttpException('شماره موبایل یا پسوورد وارد شده اشتباه است'); } @@ -65,6 +74,16 @@ class AuthController extends Controller } $token = auth($provider)->attempt($credentials); + if (!$token && $provider === 'customer') { + // ردیف‌های قدیمی با فرمت موبایل غیرنرمال + $fallback = Customer::findActiveByMobile($credentials['mobile'] ?? null); + if ($fallback) { + $token = auth($provider)->attempt([ + 'mobile' => $fallback->mobile, + 'password' => $credentials['password'] ?? '', + ]); + } + } if (!$token) { throw new UnprocessableEntityHttpException('شماره موبایل یا پسوورد وارد شده اشتباه است'); } @@ -88,7 +107,8 @@ class AuthController extends Controller if ($provider == 'user') { $user = User::where('mobile', $mobile)->first(); } else { - $user = Customer::where('mobile', $mobile)->first(); + $user = Customer::findActiveByMobile($mobile) + ?? Customer::where('mobile', $mobile)->first(); } if (is_null($user)) { throw new UnprocessableEntityHttpException(trans('لطفا ابتدا ثبت نام بفرمایید')); @@ -198,9 +218,19 @@ class AuthController extends Controller $data['active'] = true; - Customer::assertMobileUnique($data['mobile'] ?? null); + $normalizedMobile = Customer::normalizeMobile($data['mobile'] ?? null); + if ($normalizedMobile === null) { + throw new UnprocessableEntityHttpException('شماره موبایل نامعتبر است'); + } + $data['mobile'] = $normalizedMobile; - $customer = Customer::create($data); + $claimable = Customer::findClaimableImportedByMobile($normalizedMobile); + if ($claimable && $claimable->isUnclaimedAppIdentity()) { + $customer = $this->claimImportedCustomer($claimable, $data); + } else { + Customer::assertMobileUnique($normalizedMobile); + $customer = Customer::create($data); + } if ($referrer) { if (Schema::hasColumn((new Customer)->getTable(), 'total_referrals')) { @@ -241,6 +271,55 @@ class AuthController extends Controller $this->Send($data['mobile'], $message); } + if ($customer->hasValidPargarAccountId()) { + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); + } elseif (! $customer->wasRecentlyCreated) { + // claim بدون کد حسابداری — صف export مثل create + app(\App\Services\PargarSyncService::class)->registerOnCreate($customer); + app(\App\Services\TahesabSyncService::class)->registerOnCreate($customer); + } + return response()->json($customer); } + + /** + * ارتقای ردیف ازقبل‌واردشده از حسابداری به‌جای ساخت مشتری تکراری. + */ + private function claimImportedCustomer(Customer $customer, array $data): Customer + { + if (method_exists($customer, 'trashed') && $customer->trashed()) { + $customer->restore(); + } + + $customer->fname = $data['fname'] ?? $customer->fname; + $customer->lname = $data['lname'] ?? $customer->lname; + $customer->name = $data['name'] ?? $customer->name; + $customer->mobile = $data['mobile']; + $customer->type = $data['type'] ?? $customer->type ?? 2; + $customer->active = true; + $customer->is_legal = (int) ($data['is_legal'] ?? $customer->is_legal ?? 0); + + if (! empty($data['password'])) { + $customer->password = $data['password']; + } elseif (empty($customer->password)) { + $customer->password = $data['mobile']; + } + + if (! empty($data['n_code'])) { + $customer->n_code = $data['n_code']; + } + if (! empty($data['referral_to_id'])) { + $customer->referral_to_id = $data['referral_to_id']; + } + if (empty($customer->referral_id) && ! empty($data['referral_id'])) { + $customer->referral_id = $data['referral_id']; + } + if (isset($data['gram'])) { + $customer->gram = $data['gram']; + } + + $customer->save(); + + return $customer->fresh(); + } } diff --git a/app/Http/Controllers/Pargar/PargarInboundController.php b/app/Http/Controllers/Pargar/PargarInboundController.php index cae2a68..dab5734 100644 --- a/app/Http/Controllers/Pargar/PargarInboundController.php +++ b/app/Http/Controllers/Pargar/PargarInboundController.php @@ -111,6 +111,7 @@ class PargarInboundController extends Controller $customer->pargar_account_id = $accountId; $this->applyPargarCustomerName($customer, $fullName); $customer->save(); + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); $messages[] = "کد حسابداری مشتری {$fullName} ویرایش شد"; continue; } @@ -129,6 +130,7 @@ class PargarInboundController extends Controller $byAccount->password = $mobile; } $byAccount->save(); + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $byAccount->id); $messages[] = "مشتری با کد حسابداری {$accountId} به‌روزرسانی شد"; continue; } @@ -144,6 +146,7 @@ class PargarInboundController extends Controller $customer->referral_id = $this->uniqueReferralId(); $customer->save(); + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); $messages[] = "مشتری با کد حسابداری {$accountId} ایجاد شد"; } catch (\Throwable $e) { $messages[] = "خطا برای {$mobile}: " . ( @@ -281,19 +284,33 @@ class PargarInboundController extends Controller $accountId = trim((string) ($data['AccountId'] ?? '')); $customer = Customer::where('pargar_account_id', $accountId)->first(); - if (! $customer) { - return response()->json([ - 'success' => false, - 'message' => 'مشتری یافت نشد', - 'data' => null, - ], 422); - } + // اگر مشتری هنوز وارد نشده، مانده را با pargar_ref نگه می‌داریم تا بعد از import/claim اعمال شود $goldInv = isset($data['GoldInv']) ? (float) str_replace('/', '.', (string) $data['GoldInv']) : 0; $rialInv = isset($data['RialInv']) ? (float) str_replace('/', '.', (string) $data['RialInv']) : 0; - // فقط مقایسه برای لاگ — اپ source of truth است؛ کیف را از مانده پرگار عوض نکن - // اضطراری: هیچ‌وقت از mande کیف را تغییر نده (حتی اگر کد قدیمی جایی مانده باشد) + if (! $customer) { + PargarSyncLog::create([ + 'customer_id' => null, + 'desc' => 'مانده پرگار — مشتری هنوز در پنل نیست', + 'type' => PargarSyncService::TYPE_MANDE, + 'amount' => $goldInv, + 'sync_status' => PargarSyncService::STATUS_SUCCESS, + 'pargar_ref' => $accountId, + 'data' => json_encode(array_merge($data, [ + 'orphan_mande' => true, + 'wallet_adjusted' => false, + ]), JSON_UNESCAPED_UNICODE), + ]); + + return response()->json([ + 'success' => true, + 'message' => 'مانده ذخیره شد؛ پس از ثبت مشتری اعمال می‌شود', + 'data' => null, + ]); + } + + // فقط مقایسه برای لاگ — اعمال روی کیف فقط با دکمه ادمین / صف auto پس از import-claim $ownerType = (int) ($customer->type ?? 2); $walletInterface = new WalletInterface($customer->id, $ownerType); $wallet = $walletInterface->wallet; @@ -324,6 +341,11 @@ class PargarInboundController extends Controller ]), JSON_UNESCAPED_UNICODE), ]); + // اولین مانده برای مشتریِ هنوز واردنشده به اپ — یک‌بار هم‌تراز کن + if ($customer->last_seen_at === null && $hasDiff) { + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); + } + return response()->json([ 'success' => true, 'message' => 'مانده مشتری با موفقیت دریافت شد', @@ -420,6 +442,8 @@ class PargarInboundController extends Controller if ($log) { $this->sync()->markSuccess($log, $normalizedAccountId); } + + app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); } } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index bdfac17..23c9ae8 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -201,13 +201,55 @@ class Customer extends Authenticatable implements JWTSubject $query = static::query() ->whereNull('deleted_at') - ->where('mobile', $mobile); + ->whereIn('mobile', self::mobileLookupVariants($mobile)); if ($exceptId !== null) { $query->where('id', '<>', $exceptId); } - return $query->first(); + return $query->orderByDesc('id')->first(); + } + + /** + * مشتریِ ازقبل‌واردشده از حسابداری که هنوز وارد اپ نشده — قابل claim در ثبت‌نام. + * شامل soft-deleted با کد حسابداری (برای جلوگیری از ساخت ردیف تکراری). + */ + public static function findClaimableImportedByMobile(?string $mobile): ?self + { + $normalized = self::normalizeMobile($mobile); + if ($normalized === null) { + return null; + } + + $variants = self::mobileLookupVariants($normalized); + + $active = static::query() + ->whereNull('deleted_at') + ->whereIn('mobile', $variants) + ->whereNull('last_seen_at') + ->orderByDesc('id') + ->first(); + + if ($active) { + return $active; + } + + return static::onlyTrashed() + ->whereIn('mobile', $variants) + ->whereNull('last_seen_at') + ->where(function ($q) { + $q->where(function ($inner) { + $inner->whereNotNull('pargar_account_id') + ->where('pargar_account_id', '!=', '') + ->whereNotIn('pargar_account_id', ['0', '-1']); + })->orWhere(function ($inner) { + $inner->whereNotNull('moshtari_code') + ->where('moshtari_code', '!=', '') + ->whereNotIn('moshtari_code', ['0', '-1']); + }); + }) + ->orderByDesc('id') + ->first(); } public static function isMobileTaken(?string $mobile, ?int $exceptId = null): bool @@ -240,14 +282,16 @@ class Customer extends Authenticatable implements JWTSubject return $query->exists(); } - private static function mobileLookupVariants(string $normalized): array + public static function mobileLookupVariants(string $normalized): array { $digits = ltrim($normalized, '0'); return array_values(array_unique(array_filter([ $normalized, $digits, - '0' . $digits, + '0'.$digits, + '98'.$digits, + '+98'.$digits, ]))); } @@ -275,4 +319,20 @@ class Customer extends Authenticatable implements JWTSubject return self::isValidAccountingCode($this->pargar_account_id); } + public function hasValidMoshtariCode(): bool + { + return self::isValidAccountingCode($this->moshtari_code); + } + + public function hasAccountingLink(): bool + { + return $this->hasValidPargarAccountId() || $this->hasValidMoshtariCode(); + } + + /** هنوز وارد اپ نشده — معمولاً از import حسابداری آمده */ + public function isUnclaimedAppIdentity(): bool + { + return $this->last_seen_at === null; + } + } diff --git a/app/Services/PargarInboundWalletService.php b/app/Services/PargarInboundWalletService.php index 665525d..a79d11e 100644 --- a/app/Services/PargarInboundWalletService.php +++ b/app/Services/PargarInboundWalletService.php @@ -391,4 +391,48 @@ class PargarInboundWalletService return (float) str_replace('/', '.', (string) $value); } + + /** + * پس از اتصال کد پرگار / claim ثبت‌نام — همگام‌سازی کیف در پس‌زمینه. + */ + public function queueWalletSyncAfterResponse(int $customerId): void + { + dispatch(function () use ($customerId) { + $customer = Customer::find($customerId); + if ($customer === null || ! $customer->hasValidPargarAccountId()) { + return; + } + + $this->attachOrphanMandeLogs($customer); + + try { + $this->syncWalletFromPargar($customer, true); + } catch (\Throwable $e) { + Log::warning('Pargar auto wallet sync failed', [ + 'customer_id' => $customerId, + 'error' => $e->getMessage(), + ]); + } + })->afterResponse(); + } + + /** + * لاگ‌های مانده‌ای که قبل از ساخت/لینک مشتری با pargar_ref آمده‌اند را به مشتری وصل می‌کند. + */ + public function attachOrphanMandeLogs(Customer $customer): int + { + if (! $customer->hasValidPargarAccountId()) { + return 0; + } + + $accountId = trim((string) $customer->pargar_account_id); + + return PargarSyncLog::query() + ->where('type', PargarSyncService::TYPE_MANDE) + ->where('pargar_ref', $accountId) + ->where(function ($q) { + $q->whereNull('customer_id')->orWhere('customer_id', 0); + }) + ->update(['customer_id' => $customer->id]); + } } diff --git a/config/validation.php b/config/validation.php index 7cc6804..f3a4d4d 100644 --- a/config/validation.php +++ b/config/validation.php @@ -11,7 +11,8 @@ return[ 'provider'=>'required' ], 'auth/register@post'=>[ - 'mobile' => 'required|digits:11|unique:customers,mobile,NULL,NULL,deleted_at,NULL', + // یکتایی موبایل در AuthController با منطق claim مشتریِ ازقبل‌واردشده از حسابداری چک می‌شود + 'mobile' => 'required|digits:11', 'fname'=>'required', 'lname'=>'required', ],