207 lines
5.7 KiB
PHP
207 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
|
|
class Member extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
// Membership status constants
|
|
const STATUS_PENDING = 'pending';
|
|
const STATUS_ACTIVE = 'active';
|
|
const STATUS_EXPIRED = 'expired';
|
|
const STATUS_SUSPENDED = 'suspended';
|
|
|
|
// Membership type constants
|
|
const TYPE_REGULAR = 'regular';
|
|
const TYPE_HONORARY = 'honorary';
|
|
const TYPE_LIFETIME = 'lifetime';
|
|
const TYPE_STUDENT = 'student';
|
|
|
|
protected $fillable = [
|
|
'user_id',
|
|
'full_name',
|
|
'email',
|
|
'phone',
|
|
'address_line_1',
|
|
'address_line_2',
|
|
'city',
|
|
'postal_code',
|
|
'emergency_contact_name',
|
|
'emergency_contact_phone',
|
|
'national_id_encrypted',
|
|
'national_id_hash',
|
|
'membership_started_at',
|
|
'membership_expires_at',
|
|
'membership_status',
|
|
'membership_type',
|
|
];
|
|
|
|
protected $casts = [
|
|
'membership_started_at' => 'date',
|
|
'membership_expires_at' => 'date',
|
|
];
|
|
|
|
protected $appends = ['national_id'];
|
|
|
|
public function user()
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
public function payments()
|
|
{
|
|
return $this->hasMany(MembershipPayment::class);
|
|
}
|
|
|
|
/**
|
|
* Get the decrypted national ID
|
|
*/
|
|
public function getNationalIdAttribute(): ?string
|
|
{
|
|
if (empty($this->national_id_encrypted)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return Crypt::decryptString($this->national_id_encrypted);
|
|
} catch (\Exception $e) {
|
|
\Log::error('Failed to decrypt national_id', [
|
|
'member_id' => $this->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the national ID (encrypt and hash)
|
|
*/
|
|
public function setNationalIdAttribute(?string $value): void
|
|
{
|
|
if (empty($value)) {
|
|
$this->attributes['national_id_encrypted'] = null;
|
|
$this->attributes['national_id_hash'] = null;
|
|
return;
|
|
}
|
|
|
|
$this->attributes['national_id_encrypted'] = Crypt::encryptString($value);
|
|
$this->attributes['national_id_hash'] = hash('sha256', $value);
|
|
}
|
|
|
|
/**
|
|
* Check if membership status is pending (not yet paid/verified)
|
|
*/
|
|
public function isPending(): bool
|
|
{
|
|
return $this->membership_status === self::STATUS_PENDING;
|
|
}
|
|
|
|
/**
|
|
* Check if membership is active (paid & activated)
|
|
*/
|
|
public function isActive(): bool
|
|
{
|
|
return $this->membership_status === self::STATUS_ACTIVE;
|
|
}
|
|
|
|
/**
|
|
* Check if membership is expired
|
|
*/
|
|
public function isExpired(): bool
|
|
{
|
|
return $this->membership_status === self::STATUS_EXPIRED;
|
|
}
|
|
|
|
/**
|
|
* Check if membership is suspended
|
|
*/
|
|
public function isSuspended(): bool
|
|
{
|
|
return $this->membership_status === self::STATUS_SUSPENDED;
|
|
}
|
|
|
|
/**
|
|
* Check if member has paid membership (active status with valid dates)
|
|
*/
|
|
public function hasPaidMembership(): bool
|
|
{
|
|
return $this->isActive()
|
|
&& $this->membership_started_at
|
|
&& $this->membership_expires_at
|
|
&& $this->membership_expires_at->isFuture();
|
|
}
|
|
|
|
/**
|
|
* Get the membership status badge class for display
|
|
*/
|
|
public function getMembershipStatusBadgeAttribute(): string
|
|
{
|
|
$label = $this->membership_status_label;
|
|
|
|
$class = match($this->membership_status) {
|
|
self::STATUS_PENDING => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
|
self::STATUS_ACTIVE => 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
|
self::STATUS_EXPIRED => 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
|
self::STATUS_SUSPENDED => 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
|
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
|
|
};
|
|
|
|
return trim("{$label} {$class}");
|
|
}
|
|
|
|
/**
|
|
* Get the membership status label in Chinese
|
|
*/
|
|
public function getMembershipStatusLabelAttribute(): string
|
|
{
|
|
return match($this->membership_status) {
|
|
self::STATUS_PENDING => '待繳費',
|
|
self::STATUS_ACTIVE => '已啟用',
|
|
self::STATUS_EXPIRED => '已過期',
|
|
self::STATUS_SUSPENDED => '已暫停',
|
|
default => $this->membership_status,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the membership type label in Chinese
|
|
*/
|
|
public function getMembershipTypeLabelAttribute(): string
|
|
{
|
|
return match($this->membership_type) {
|
|
self::TYPE_REGULAR => '一般會員',
|
|
self::TYPE_HONORARY => '榮譽會員',
|
|
self::TYPE_LIFETIME => '終身會員',
|
|
self::TYPE_STUDENT => '學生會員',
|
|
default => $this->membership_type,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get pending payment (if any)
|
|
*/
|
|
public function getPendingPayment()
|
|
{
|
|
return $this->payments()
|
|
->where('status', MembershipPayment::STATUS_PENDING)
|
|
->orWhere('status', MembershipPayment::STATUS_APPROVED_CASHIER)
|
|
->orWhere('status', MembershipPayment::STATUS_APPROVED_ACCOUNTANT)
|
|
->latest()
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Check if member can submit payment
|
|
*/
|
|
public function canSubmitPayment(): bool
|
|
{
|
|
// Can submit if pending status and no pending payment
|
|
return $this->isPending() && !$this->getPendingPayment();
|
|
}
|
|
}
|