- PUT v1/client/update: gender theo enum ClientGender (0 Nam/1 Nữ), thêm
birthday (cột mới), province_code/ward_code validate theo bảng provinces/wards
(phường phải thuộc tỉnh). Migration nạp dữ liệu hành chính 2025 từ
province_ward_import.sql (chỉ bảng trống), API GET v1/provinces[/{code}/wards].
- CustomerObserver/OrderObserver log giá trị cũ bằng getRawOriginal: getOriginal
trả enum object, nối chuỗi gây 500 khi khách đổi giới tính.
- ClientResource: các cờ trả boolean, CCCD chỉ trả cccd_updated (không lộ ảnh/số).
- GET v1/setting/menu trả cả items + quick_links, bỏ v1/setting/quick-links.
- Thông báo admin/khách tra theo uuid thay vì id tự tăng.
- Chặn gửi template marketing cho khách đã tắt nhận email marketing.
- Xoá ProvinceWardSeeder và Customer::$updatable không dùng.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
268 lines
5.7 KiB
PHP
268 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Traits\LogsActivity;
|
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
|
use Tymon\JWTAuth\Contracts\JWTSubject;
|
|
|
|
class Customer extends Authenticatable implements JWTSubject
|
|
{
|
|
use HasFactory, HasUuids, SoftDeletes, LogsActivity;
|
|
|
|
protected $table = 'customers';
|
|
|
|
/**
|
|
* Chỉ định cột sẽ nhận UUID.
|
|
* Mặc định HasUuids dùng cột 'id', mình đổi sang 'uuid'.
|
|
*/
|
|
public function uniqueIds()
|
|
{
|
|
return ['uuid'];
|
|
}
|
|
|
|
/**
|
|
* Điểm tích luỹ cộng cho một khoản nạp: 1 credit / 100 VNĐ.
|
|
* Trước đây khớp sao kê tự động cộng credit bằng nguyên số tiền còn duyệt tay cộng
|
|
* amount/100, cùng một khoản ra hai kết quả lệch nhau 100 lần — nay đi chung một chỗ.
|
|
*/
|
|
public static function creditFor(int $amount): int
|
|
{
|
|
return (int) floor(max(0, $amount) / 100);
|
|
}
|
|
|
|
protected $fillable = [
|
|
'fullname',
|
|
'username',
|
|
'avatar',
|
|
'address',
|
|
'state',
|
|
'city',
|
|
'postcode',
|
|
'country',
|
|
'company',
|
|
'phone',
|
|
'email',
|
|
'email_verified_at',
|
|
'email_verify',
|
|
'email_verify_code',
|
|
'email_verify_code_expired_at',
|
|
'email_hash',
|
|
'email_marketing',
|
|
'receive_deposit',
|
|
'identity_number',
|
|
'front_identity_number',
|
|
'back_identity_number',
|
|
'password',
|
|
'note',
|
|
'type',
|
|
'credit',
|
|
'total',
|
|
'total_pending',
|
|
'remember_token',
|
|
'status',
|
|
'gender',
|
|
'birthday',
|
|
'province_code',
|
|
'ward_code',
|
|
];
|
|
|
|
/**
|
|
* Appends for API
|
|
*/
|
|
protected $appends = [
|
|
'point',
|
|
'gender_label',
|
|
];
|
|
|
|
protected $casts = [
|
|
'gender' => \App\Enums\Client\ClientGender::class,
|
|
'email_verified_at' => 'datetime',
|
|
'two_factor_enabled' => 'boolean',
|
|
'birthday' => 'date',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be hidden for serialization.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
protected $hidden = [
|
|
'password',
|
|
'remember_token',
|
|
];
|
|
|
|
/**
|
|
* Quan hệ với ModuleHosting (Hosting).
|
|
*/
|
|
public function hosting()
|
|
{
|
|
return $this->hasMany(ModuleHosting::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với ModuleDomain (Domain).
|
|
*/
|
|
public function domain()
|
|
{
|
|
return $this->hasMany(ModuleDomain::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với ModuleVmware (VPS).
|
|
*/
|
|
public function vps()
|
|
{
|
|
return $this->hasMany(ModuleVmware::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với Service (tbl.services).
|
|
*/
|
|
public function service()
|
|
{
|
|
return $this->hasMany(Service::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với Invoice.
|
|
*/
|
|
public function invoice()
|
|
{
|
|
return $this->hasMany(Invoice::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với Ticket.
|
|
*/
|
|
public function ticket()
|
|
{
|
|
return $this->hasMany(Ticket::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với Mail.
|
|
*/
|
|
public function mail()
|
|
{
|
|
return $this->hasMany(Mail::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với File.
|
|
*/
|
|
public function file()
|
|
{
|
|
return $this->hasMany(File::class, 'customer_id');
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với Order.
|
|
*/
|
|
public function order()
|
|
{
|
|
return $this->hasMany(Order::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với Transaction.
|
|
*/
|
|
public function transaction()
|
|
{
|
|
return $this->hasMany(Transaction::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với InvoiceItem.
|
|
*/
|
|
public function invoiceItem()
|
|
{
|
|
return $this->hasMany(InvoiceItem::class);
|
|
}
|
|
|
|
/**
|
|
* Quan hệ với Log.
|
|
*/
|
|
public function log()
|
|
{
|
|
return $this->hasMany(Log::class);
|
|
}
|
|
|
|
public function acceptsMarketingEmail(): bool
|
|
{
|
|
return (bool) $this->email_marketing;
|
|
}
|
|
|
|
public function getReceiveDepositBoolAttribute()
|
|
{
|
|
return (bool) $this->receive_deposit;
|
|
}
|
|
|
|
public function getPointAttribute()
|
|
{
|
|
return (string) floor($this->total / 100);
|
|
}
|
|
|
|
public function getNameAttribute()
|
|
{
|
|
return $this->fullname;
|
|
}
|
|
|
|
public function getCmndBeforeAttribute()
|
|
{
|
|
return $this->front_identity_number ?? '';
|
|
}
|
|
|
|
public function getCmndAfterAttribute()
|
|
{
|
|
return $this->back_identity_number ?? '';
|
|
}
|
|
|
|
public function getCmndAttribute()
|
|
{
|
|
return $this->identity_number;
|
|
}
|
|
|
|
public function getOffMailMarketingAttribute()
|
|
{
|
|
return $this->email_marketing;
|
|
}
|
|
|
|
public function province()
|
|
{
|
|
return $this->belongsTo(Province::class, 'province_code', 'code');
|
|
}
|
|
|
|
public function ward()
|
|
{
|
|
return $this->belongsTo(Ward::class, 'ward_code', 'code');
|
|
}
|
|
|
|
public function getGenderLabelAttribute()
|
|
{
|
|
return $this->gender?->label() ?? 'Khác';
|
|
}
|
|
|
|
public function getJWTIdentifier()
|
|
{
|
|
return $this->getKey();
|
|
}
|
|
|
|
public function getJWTCustomClaims()
|
|
{
|
|
return ['pv' => $this->passwordVersion()];
|
|
}
|
|
|
|
/**
|
|
* Dấu vân tay của hash mật khẩu, nhúng vào JWT. Đổi/đặt lại mật khẩu → giá trị đổi →
|
|
* mọi access token cũ bị ApiAuthCheck từ chối (không cần blacklist).
|
|
*/
|
|
public function passwordVersion(): string
|
|
{
|
|
return substr(hash('sha256', (string) $this->password), 0, 16);
|
|
}
|
|
}
|