"boolean", "gold" => "float", "is_archive" => "boolean", "finger_print" => "boolean", "conditional_order_active" => "boolean", "conditional_order_limit" => "integer", "profit_amount_by_lvl" => "array", ]; static $TYPE = [ 1=>'همکار', 2=>'خانگی', 3=>'سرمایه گذار' ]; static $TYPES = [ 1=>'همکار', 2=>'خانگی', 3=>'سرمایه گذار' ]; public function setNameAttribute($value = null) { $fname = $this->attributes['fname'] ?? ''; $lname = $this->attributes['lname'] ?? ''; $this->attributes['name'] = trim("$fname $lname"); } public function wallet() { return $this->hasMany(Wallet::class ,'owner')->with(['fund','product'])->select(['cash', 'fund_id','product_id','gold', 'owner', 'owner_type']); } public function orders() { return $this->hasMany(Order::class, 'customer_id'); } public function user() { return $this->belongsTo(User::class)->select(['id','name']); } public function referral() { return $this->belongsTo(Customer::class, 'referral_to_id', 'referral_id'); } /** معرف واقعی — با جستجوی صریح و جلوگیری از برگشت خود کاربر */ public function referrer(): ?self { if (empty($this->referral_to_id)) { return null; } return static::query() ->where('referral_id', $this->referral_to_id) ->where('id', '!=', $this->id) ->first(); } // hash password — ارقام فارسی/عربی را قبل از هش انگلیسی کن (لاگین اپ هم انگلیسی می‌فرستد) public function setPasswordAttribute($value){ if ($value === null || $value === '') { return; } $value = trim((string) $value); if ($value === '') { return; } // اگر از قبل bcrypt است دوباره هش نکن if (preg_match('/^\$2[ayb]\$.{56}$/', $value) === 1) { $this->attributes['password'] = $value; return; } $this->attributes['password'] = Hash::make(self::normalizeCredentialDigits($value)); } /** ارقام فارسی/عربی → لاتین (برای رمز و موبایل در احراز هویت) */ public static function normalizeCredentialDigits(string $value): string { $persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; $arabic = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; $latin = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; return str_replace(array_merge($persian, $arabic), array_merge($latin, $latin), $value); } public function setBirthDateAttribute($value): void { if ($value === null || $value === '') { $this->attributes['birth_date'] = null; return; } $this->attributes['birth_date'] = Carbon::parse($value)->format('Y-m-d'); } public function getJWTIdentifier() { return $this->getKey(); } public function getJWTCustomClaims() { return []; } public function tahesab() { return $this->hasMany(Tahesab::class, 'customer_id', 'id'); } public function customer_settings() { return $this->hasMany(CustomersSetting::class, 'agent_id'); } public function package() { return $this->belongsTo(Package::class); } public function agent() { return $this->belongsTo(Customer::class, 'agent_id'); } protected static function booted(): void { // کد کاربری = customers.id. AUTO_INCREMENT بعد از UPDATE دستی id پایین نمی‌آید // (مثلاً ۱..۲۳ اصلاح شد ولی شمارنده روی ۵۵۱ ماند). همیشه اولین id خالی. static::creating(function (self $customer) { if (! empty($customer->id)) { return; } $customer->id = self::nextSequentialId(); $customer->incrementing = false; }); static::saving(function (self $customer) { $mobile = self::normalizeMobile($customer->mobile); if ($mobile === null) { return; } $customer->mobile = $mobile; self::assertMobileUnique($mobile, $customer->exists ? (int) $customer->id : null); }); } /** کوچک‌ترین id آزاد، شامل ردیف‌های soft-deleted تا PK تکراری نشود. */ public static function nextSequentialId(): int { $table = (new static)->getTable(); $row = DB::selectOne( "SELECT t.id + 1 AS next_id FROM (SELECT 0 AS id UNION ALL SELECT id FROM `{$table}`) t LEFT JOIN `{$table}` c ON c.id = t.id + 1 WHERE c.id IS NULL ORDER BY t.id LIMIT 1" ); return max(1, (int) ($row->next_id ?? 1)); } public static function normalizeMobile(?string $mobile): ?string { $digits = preg_replace('/\D+/', '', trim((string) $mobile)); if ($digits === '') { return null; } if (str_starts_with($digits, '98') && strlen($digits) >= 12) { $digits = substr($digits, 2); } $digits = ltrim($digits, '0'); if ($digits === '') { return null; } return '0' . $digits; } public static function findActiveByMobile(?string $mobile, ?int $exceptId = null): ?self { $mobile = self::normalizeMobile($mobile); if ($mobile === null) { return null; } $query = static::query() ->whereNull('deleted_at') ->whereIn('mobile', self::mobileLookupVariants($mobile)); if ($exceptId !== null) { $query->where('id', '<>', $exceptId); } 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 { return self::isMobileTakenOnTable(self::class, $mobile, $exceptId); } public static function isMobileTakenOnTable(string $modelClass, ?string $mobile, ?int $exceptId = null): bool { $normalized = self::normalizeMobile($mobile); if ($normalized === null) { return false; } if ($exceptId !== null) { $current = $modelClass::query()->whereNull('deleted_at')->find($exceptId); if ($current !== null && self::normalizeMobile($current->mobile ?? '') === $normalized) { return false; } } $query = $modelClass::query() ->whereNull('deleted_at') ->whereIn('mobile', self::mobileLookupVariants($normalized)); if ($exceptId !== null) { $query->where('id', '<>', $exceptId); } return $query->exists(); } public static function mobileLookupVariants(string $normalized): array { $digits = ltrim($normalized, '0'); return array_values(array_unique(array_filter([ $normalized, $digits, '0'.$digits, '98'.$digits, '+98'.$digits, ]))); } public static function assertMobileUnique(?string $mobile, ?int $exceptId = null): void { if (self::isMobileTaken($mobile, $exceptId)) { throw new UnprocessableEntityHttpException('این شماره موبایل قبلاً ثبت شده است'); } } /** کد معتبر حسابداری (پرگار/ته‌حساب) — خالی و 0 و -1 نامعتبرند */ public static function isValidAccountingCode(mixed $value): bool { if ($value === null) { return false; } $code = trim((string) $value); return $code !== '' && ! in_array($code, ['0', '-1'], true); } public function hasValidPargarAccountId(): bool { 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; } }