Files

404 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Carbon\Carbon;
use Rooyesh\Wallet\models\Wallet;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Tymon\JWTAuth\Contracts\JWTSubject;
class Customer extends Authenticatable implements JWTSubject
{
use HasFactory,SoftDeletes,Selector;
protected $fillable = [
'fname',
'lname',
'name',
'active',
'finger_print',
'mobile',
'email',
'avatar_id',
'last_seen_at',
'default_address_id',
'sts',
'total_transaction',
'token_sms',
'password',
'orders_count',
'birth_date',
'shop_name',
'phone_number',
'code',
'city',
'province',
'n_code',
'is_archive',
'gram',
'trade_active',//آیا امکان معامله دارد؟
'conditional_order_active',//آیا سفارش شرطی مجاز است؟
'conditional_order_limit',//سقف تعداد pending (null = نامحدود)
'chat_room_id',
'chat_user_id',
'type',
'referral_id',
'referral_to_id',
'total_referrals',
'credential_id',
'moshtari_code',
'pargar_account_id',
'profit_amount_by_lvl',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'token_sms',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
"active" => "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;
}
}