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`.
248 lines
8.2 KiB
PHP
248 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\AddressBookEntry;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
/**
|
|
* AddressBookController — CRUD دفترچه آدرس کاربر (فاز ۳.۹)
|
|
*
|
|
* همهٔ عملیات با auth:sanctum محافظت میشوند و کاربر فقط به
|
|
* رکوردهای خودش دسترسی دارد (Ownership check در همهٔ اکشنها).
|
|
*/
|
|
class AddressBookController extends Controller
|
|
{
|
|
/**
|
|
* لیست آدرسهای کاربر
|
|
* GET /api/v1/addresses?role=sender|receiver&search=...
|
|
*/
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$user = Auth::user();
|
|
|
|
$query = AddressBookEntry::forUser($user->id)
|
|
->orderByDesc('is_default')
|
|
->orderByDesc('last_used_at')
|
|
->orderByDesc('updated_at');
|
|
|
|
if ($role = $request->query('role')) {
|
|
if (in_array($role, [AddressBookEntry::ROLE_SENDER, AddressBookEntry::ROLE_RECEIVER], true)) {
|
|
$query->ofRole($role);
|
|
}
|
|
}
|
|
|
|
if ($search = trim((string) $request->query('search', ''))) {
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('label', 'like', "%{$search}%")
|
|
->orWhere('phone', 'like', "%{$search}%")
|
|
->orWhere('city', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
$entries = $query->get();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $entries->map(fn (AddressBookEntry $e) => $this->formatEntry($e)),
|
|
'count' => $entries->count(),
|
|
'max_entries' => AddressBookEntry::MAX_ENTRIES_PER_USER,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* افزودن آدرس جدید
|
|
* POST /api/v1/addresses
|
|
*/
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validated = $this->validateEntry($request);
|
|
|
|
$user = Auth::user();
|
|
|
|
$count = AddressBookEntry::forUser($user->id)->count();
|
|
if ($count >= AddressBookEntry::MAX_ENTRIES_PER_USER) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'تعداد آدرسهای ذخیرهشده شما به حداکثر مجاز (' . AddressBookEntry::MAX_ENTRIES_PER_USER . ') رسیده است. لطفاً آدرسهای قدیمی را حذف کنید.',
|
|
], 422);
|
|
}
|
|
|
|
// جلوگیری از رکورد تکراری (همان نقش + تلفن + آدرس) → بروزرسانی رکورد موجود
|
|
$duplicate = AddressBookEntry::findDuplicate(
|
|
$user->id,
|
|
$validated['role'],
|
|
$validated['phone'],
|
|
$validated['address']
|
|
);
|
|
|
|
if ($duplicate) {
|
|
$duplicate->update(collect($validated)->except(['role'])->all());
|
|
|
|
if (!empty($validated['is_default'])) {
|
|
$duplicate->makeDefault();
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'آدرس مشابه از قبل ذخیره شده بود و بروزرسانی شد.',
|
|
'data' => $this->formatEntry($duplicate->refresh()),
|
|
'updated_existing' => true,
|
|
]);
|
|
}
|
|
|
|
$entry = AddressBookEntry::create([
|
|
...$validated,
|
|
'user_id' => $user->id,
|
|
]);
|
|
|
|
if (!empty($validated['is_default'])) {
|
|
$entry->makeDefault();
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'آدرس با موفقیت ذخیره شد.',
|
|
'data' => $this->formatEntry($entry),
|
|
], 201);
|
|
}
|
|
|
|
/**
|
|
* ویرایش آدرس
|
|
* PUT/PATCH /api/v1/addresses/{entry}
|
|
*/
|
|
public function update(Request $request, AddressBookEntry $address): JsonResponse
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if ($address->user_id !== $user->id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'دسترسی غیرمجاز.',
|
|
], 403);
|
|
}
|
|
|
|
$validated = $this->validateEntry($request, $address->id);
|
|
|
|
// نقش رکورد قابل تغییر نیست (جلوگیری از ناسازگاری با فرم)
|
|
unset($validated['role']);
|
|
|
|
$address->update($validated);
|
|
|
|
if (!empty($validated['is_default'])) {
|
|
$address->makeDefault();
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'آدرس بروزرسانی شد.',
|
|
'data' => $this->formatEntry($address->refresh()),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* حذف آدرس
|
|
* DELETE /api/v1/addresses/{entry}
|
|
*/
|
|
public function destroy(Request $request, AddressBookEntry $address): JsonResponse
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if ($address->user_id !== $user->id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'دسترسی غیرمجاز.',
|
|
], 403);
|
|
}
|
|
|
|
$address->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'آدرس حذف شد.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* تنظیم بهعنوان پیشفرض
|
|
* POST /api/v1/addresses/{address}/default
|
|
*/
|
|
public function setDefault(Request $request, AddressBookEntry $address): JsonResponse
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if ($address->user_id !== $user->id) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'دسترسی غیرمجاز.',
|
|
], 403);
|
|
}
|
|
|
|
$address->makeDefault();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => 'این آدرس بهعنوان پیشفرض تنظیم شد.',
|
|
'data' => $this->formatEntry($address->refresh()),
|
|
]);
|
|
}
|
|
|
|
// === Helpers ===
|
|
|
|
private function validateEntry(Request $request, ?int $ignoreId = null): array
|
|
{
|
|
try {
|
|
return $request->validate([
|
|
'role' => ['required', Rule::in([AddressBookEntry::ROLE_SENDER, AddressBookEntry::ROLE_RECEIVER])],
|
|
'label' => ['nullable', 'string', 'max:100'],
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'company' => ['nullable', 'string', 'max:255'],
|
|
'phone' => ['required', 'string', 'max:50'],
|
|
'email' => ['nullable', 'email', 'max:255'],
|
|
'state' => ['nullable', 'string', 'max:255'],
|
|
'city' => ['nullable', 'string', 'max:255'],
|
|
'postal_code' => ['nullable', 'string', 'max:20'],
|
|
'national_id' => ['nullable', 'string', 'max:50'],
|
|
'address' => ['required', 'string', 'max:1000'],
|
|
'is_default' => ['nullable', 'boolean'],
|
|
]);
|
|
} catch (ValidationException $e) {
|
|
abort(response()->json([
|
|
'success' => false,
|
|
'message' => 'خطا در اعتبارسنجی اطلاعات آدرس',
|
|
'errors' => $e->errors(),
|
|
], 422));
|
|
}
|
|
}
|
|
|
|
private function formatEntry(AddressBookEntry $e): array
|
|
{
|
|
return [
|
|
'id' => $e->id,
|
|
'role' => $e->role,
|
|
'label' => $e->label,
|
|
'display_label' => $e->display_label,
|
|
'name' => $e->name,
|
|
'company' => $e->company,
|
|
'phone' => $e->phone,
|
|
'email' => $e->email,
|
|
'state' => $e->state,
|
|
'city' => $e->city,
|
|
'postal_code' => $e->postal_code,
|
|
'national_id' => $e->national_id,
|
|
'address' => $e->address,
|
|
'is_default' => $e->is_default,
|
|
'use_count' => $e->use_count,
|
|
'last_used_at' => $e->last_used_at?->toIso8601String(),
|
|
'form_fields' => $e->toFormFieldData(),
|
|
'updated_at' => $e->updated_at->toIso8601String(),
|
|
];
|
|
}
|
|
}
|