'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(); } }