ifnex/04_Laravel/app/Models/AddressBookEntry.php
Kazem Alghasi a4fc807160 feat(address-book): implement customer address book system (phase 3.9)
Introduce a comprehensive address management system allowing users to
save, manage, and reuse sender and receiver addresses during the
ordering process. This feature bridges the WordPress frontend with the
Laravel backend API.

Key changes:
- Backend (Laravel):
  - Added `AddressBookEntry` model and migration for storing addresses
    with role (sender/receiver), usage tracking, and default status.
  - Implemented `AddressBookController` with full CRUD functionality
    secured via Sanctum and ownership validation.
  - Integrated address usage tracking and upsert logic into the
    `CustomerOrderController` to prevent duplicate entries.
  - Added 18 new feature tests covering ownership, limits, and
    validation.
- Frontend (WordPress):
  - Added a new "Address Book" tab in the user dashboard with card-based
    UI, filtering, and SweetAlert2 integration.
  - Enhanced the order form with dropdown selectors for saved addresses
    and "save this address" checkboxes for both sender and receiver.
  - Implemented secure AJAX proxies in `user-bridge.php` with nonce
    and sanitization.
- Documentation:
  - Updated `AGENT.md` and `CLIENT_DELIVERY.md` to reflect phase 3.9
    completion and updated the project roadmap.
  - Removed obsolete `README-INSTALL.md`.
2026-10-31 19:57:01 +03:30

155 lines
4.5 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Traits\Auditable;
/**
* AddressBookEntry — یک رکورد از دفترچه آدرس کاربر (فاز ۳.۹)
*
* نقش (role) مشخص می‌کند این آدرس برای فرستنده ذخیره شده یا گیرنده؛
* ساختار فیلدها عیناً مطابق ستون‌های sender_/receiver_ جدول shipments است.
*/
class AddressBookEntry extends Model
{
use Auditable;
public const ROLE_SENDER = 'sender';
public const ROLE_RECEIVER = 'receiver';
/** سقف تعداد آدرس ذخیره‌شده برای هر کاربر (ضد سوءاستفاده) */
public const MAX_ENTRIES_PER_USER = 50;
protected $fillable = [
'user_id',
'role',
'label',
'name',
'company',
'phone',
'email',
'state',
'city',
'postal_code',
'national_id',
'address',
'is_default',
'use_count',
'last_used_at',
];
protected $casts = [
'is_default' => 'boolean',
'use_count' => 'integer',
'last_used_at' => 'datetime',
];
/**
* مقادیر پیش‌فرض روی instance — برای اینکه در لحظهٔ create
* (و قبل از refresh از DB) مقدار use_count/is_default در دسترس باشد.
* SQLite مقادیر default ستون‌ها را بلافاصله روی مدل در حافظه بازنویسی نمی‌کند.
*/
protected $attributes = [
'use_count' => 0,
'is_default' => false,
];
// === Relationships ===
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
// === Scopes ===
public function scopeForUser($query, int $userId)
{
return $query->where('user_id', $userId);
}
public function scopeOfRole($query, string $role)
{
return $query->where('role', $role);
}
// === Helpers ===
/**
* داده‌های آمادهٔ پر کردن فیلدهای فرم سفارش
* کلیدها عیناً با prefix نقش مطابق فرم وردپرس برمی‌گردند.
*/
public function toFormFieldData(): array
{
$prefix = $this->role === self::ROLE_SENDER ? 'sender' : 'receiver';
return [
'id' => $this->id,
'role' => $this->role,
'label' => $this->label,
'is_default' => $this->is_default,
"{$prefix}_name" => $this->name,
"{$prefix}_phone" => $this->phone,
"{$prefix}_state" => $this->state,
"{$prefix}_city" => $this->city,
"{$prefix}_postal_code" => $this->postal_code,
"{$prefix}_national_id" => $this->national_id,
"{$prefix}_address" => $this->address,
];
}
/**
* پیدا کردن رکورد تکراری (همان نقش + همان تلفن + همان آدرس)
* برای جلوگیری از انباشت رکوردهای دوباره هنگام «ذخیره این آدرس».
*/
public static function findDuplicate(int $userId, string $role, string $phone, string $address): ?self
{
return static::forUser($userId)
->ofRole($role)
->where('phone', $phone)
->where('address', $address)
->first();
}
/**
* ثبت استفاده: افزایش شمارنده و بروزرسانی زمان آخرین استفاده
*/
public function markUsed(): void
{
$this->increment('use_count');
$this->forceFill(['last_used_at' => now()])->save();
}
/**
* این رکورد را پیش‌فرض نقش خودش می‌کند (سایر رکوردهای همان نقش off می‌شوند)
*/
public function makeDefault(): void
{
static::forUser($this->user_id)
->ofRole($this->role)
->where('id', '!=', $this->id)
->update(['is_default' => false]);
$this->forceFill(['is_default' => true])->save();
}
/**
* برچسب نمایشی در dropdown فرم
*/
public function getDisplayLabelAttribute(): string
{
$parts = [];
if ($this->label) {
$parts[] = $this->label;
}
$parts[] = $this->name;
if ($this->city) {
$parts[] = $this->city;
}
return implode(' — ', $parts);
}
}