From c27ae1969f920512fa77b81a31e98be07ee7047a Mon Sep 17 00:00:00 2001 From: kusowl Date: Fri, 13 Mar 2026 18:08:45 +0530 Subject: [PATCH] feature: add address api - users can save, edi and delete addresses - each user can have multiple address - used shallow routes for address --- .../app/Actions/DeleteUserAddressAction.php | 19 ++++++ backend/app/Actions/SaveUserAddressAction.php | 22 +++++++ .../app/Actions/UpdateUserAddressAction.php | 21 +++++++ backend/app/Data/AddUserAddressRequestDTO.php | 45 ++++++++++++++ .../app/Data/UpdateUserAddressRequestDTO.php | 47 ++++++++++++++ backend/app/Data/UserAddressResponseDTO.php | 48 +++++++++++++++ .../Controllers/UserAddressController.php | 61 +++++++++++++++++++ .../Http/Requests/AddUserAddressRequest.php | 34 +++++++++++ .../Requests/UpdateUserAddressRequest.php | 35 +++++++++++ .../app/Http/Resources/AddressCollection.php | 19 ++++++ .../app/Http/Resources/AddressResource.php | 31 ++++++++++ backend/app/Models/Address.php | 49 +++++++++++++++ backend/app/Models/User.php | 29 ++++++--- ...26_03_12_141247_create_addresses_table.php | 33 ++++++++++ .../2026_03_12_141909_address_user.php | 29 +++++++++ backend/routes/api.php | 3 + 16 files changed, 517 insertions(+), 8 deletions(-) create mode 100644 backend/app/Actions/DeleteUserAddressAction.php create mode 100644 backend/app/Actions/SaveUserAddressAction.php create mode 100644 backend/app/Actions/UpdateUserAddressAction.php create mode 100644 backend/app/Data/AddUserAddressRequestDTO.php create mode 100644 backend/app/Data/UpdateUserAddressRequestDTO.php create mode 100644 backend/app/Data/UserAddressResponseDTO.php create mode 100644 backend/app/Http/Controllers/UserAddressController.php create mode 100644 backend/app/Http/Requests/AddUserAddressRequest.php create mode 100644 backend/app/Http/Requests/UpdateUserAddressRequest.php create mode 100644 backend/app/Http/Resources/AddressCollection.php create mode 100644 backend/app/Http/Resources/AddressResource.php create mode 100644 backend/app/Models/Address.php create mode 100644 backend/database/migrations/2026_03_12_141247_create_addresses_table.php create mode 100644 backend/database/migrations/2026_03_12_141909_address_user.php diff --git a/backend/app/Actions/DeleteUserAddressAction.php b/backend/app/Actions/DeleteUserAddressAction.php new file mode 100644 index 0000000..40e0239 --- /dev/null +++ b/backend/app/Actions/DeleteUserAddressAction.php @@ -0,0 +1,19 @@ +users()->detach(); + $address->delete(); + + return response()->noContent(); + } +} diff --git a/backend/app/Actions/SaveUserAddressAction.php b/backend/app/Actions/SaveUserAddressAction.php new file mode 100644 index 0000000..96d2014 --- /dev/null +++ b/backend/app/Actions/SaveUserAddressAction.php @@ -0,0 +1,22 @@ +addresses() + ->create($data->toArray()) + ); + } +} diff --git a/backend/app/Actions/UpdateUserAddressAction.php b/backend/app/Actions/UpdateUserAddressAction.php new file mode 100644 index 0000000..ec8442d --- /dev/null +++ b/backend/app/Actions/UpdateUserAddressAction.php @@ -0,0 +1,21 @@ +update($data->toArray()); + $address->refresh(); + + return UserAddressResponseDTO::fromModel($address); + } +} diff --git a/backend/app/Data/AddUserAddressRequestDTO.php b/backend/app/Data/AddUserAddressRequestDTO.php new file mode 100644 index 0000000..57a16e5 --- /dev/null +++ b/backend/app/Data/AddUserAddressRequestDTO.php @@ -0,0 +1,45 @@ + + */ + public function toArray(): array + { + return [ + 'first_name' => $this->firstName, + 'last_name' => $this->lastName, + 'street' => $this->street, + 'city' => $this->city, + 'state' => $this->state, + 'pin' => $this->pinCode, + ]; + } + + public static function fromRequest(FormRequest $request): InputDataTransferObject + { + return new self( + firstName: $request->firstName, + lastName: $request->lastName, + street: $request->street, + city: $request->city, + state: $request->state, + pinCode: $request->pinCode + ); + } +} diff --git a/backend/app/Data/UpdateUserAddressRequestDTO.php b/backend/app/Data/UpdateUserAddressRequestDTO.php new file mode 100644 index 0000000..5817b4f --- /dev/null +++ b/backend/app/Data/UpdateUserAddressRequestDTO.php @@ -0,0 +1,47 @@ + + */ + public function toArray(): array + { + $data = [ + 'first_name' => $this->firstName, + 'last_name' => $this->lastName, + 'street' => $this->street, + 'city' => $this->city, + 'state' => $this->state, + 'pin' => $this->pinCode, + ]; + + return array_filter($data, fn ($value) => $value !== null); + } + + public static function fromRequest(FormRequest $request): InputDataTransferObject + { + return new self( + firstName: $request->input('firstName'), + lastName: $request->input('lastName'), + street: $request->input('street'), + city: $request->input('city'), + state: $request->input('state'), + pinCode: $request->input('pinCode'), + ); + } +} diff --git a/backend/app/Data/UserAddressResponseDTO.php b/backend/app/Data/UserAddressResponseDTO.php new file mode 100644 index 0000000..cb23fa0 --- /dev/null +++ b/backend/app/Data/UserAddressResponseDTO.php @@ -0,0 +1,48 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'firstName' => $this->firstName, + 'lastName' => $this->lastName, + 'street' => $this->street, + 'city' => $this->city, + 'state' => $this->state, + 'pinCode' => $this->pinCode, + ]; + } + + public static function fromModel(Address $address): OutputDataTransferObject + { + return new self( + id: $address->id, + firstName: $address->first_name, + lastName: $address->last_name, + street: $address->street, + city: $address->city, + state: $address->state, + pinCode: $address->pin + ); + } +} diff --git a/backend/app/Http/Controllers/UserAddressController.php b/backend/app/Http/Controllers/UserAddressController.php new file mode 100644 index 0000000..1f7f5d5 --- /dev/null +++ b/backend/app/Http/Controllers/UserAddressController.php @@ -0,0 +1,61 @@ +user()->addresses; + $data = $addresses->map(fn ($address) => UserAddressResponseDTO::fromModel($address)); + + return AddressResource::collection($data); + } + + /** + * Store a newly created resource in storage. + */ + public function store(AddUserAddressRequest $request, SaveUserAddressAction $action) + { + return new AddressResource($action->execute(AddUserAddressRequestDTO::fromRequest($request), $request->user())); + } + + /** + * Display the specified resource. + */ + public function show(Address $address) + { + return new AddressResource(UserAddressResponseDTO::fromModel($address)); + } + + /** + * Update the specified resource in storage. + */ + public function update(UpdateUserAddressRequest $request, Address $address, UpdateUserAddressAction $action) + { + return new AddressResource($action->execute(UpdateUserAddressRequestDTO::fromRequest($request), $address)); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Address $address, DeleteUserAddressAction $action) + { + return $action->execute($address); + } +} diff --git a/backend/app/Http/Requests/AddUserAddressRequest.php b/backend/app/Http/Requests/AddUserAddressRequest.php new file mode 100644 index 0000000..ffa42ed --- /dev/null +++ b/backend/app/Http/Requests/AddUserAddressRequest.php @@ -0,0 +1,34 @@ +|string> + */ + public function rules(): array + { + return [ + 'firstName' => 'required|string|min:3|max:50', + 'lastName' => 'required|string|min:3|max:50', + 'street' => 'required|string|min:3|max:100', + 'city' => 'required|string|min:3|max:20', + 'state' => 'required|string|min:3|max:40', + 'pinCode' => 'required|string|min:3|max:10', + ]; + } +} diff --git a/backend/app/Http/Requests/UpdateUserAddressRequest.php b/backend/app/Http/Requests/UpdateUserAddressRequest.php new file mode 100644 index 0000000..a10e92d --- /dev/null +++ b/backend/app/Http/Requests/UpdateUserAddressRequest.php @@ -0,0 +1,35 @@ +|string> + */ + public function rules(): array + { + return [ + 'firstName' => 'sometimes|string|min:3|max:50', + 'lastName' => 'sometimes|string|min:3|max:50', + 'street' => 'sometimes|string|min:3|max:100', + 'city' => 'sometimes|string|min:3|max:20', + 'state' => 'sometimes|string|min:3|max:40', + 'pinCode' => 'sometimes|string|min:3|max:10', + + ]; + } +} diff --git a/backend/app/Http/Resources/AddressCollection.php b/backend/app/Http/Resources/AddressCollection.php new file mode 100644 index 0000000..d0c11a0 --- /dev/null +++ b/backend/app/Http/Resources/AddressCollection.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/backend/app/Http/Resources/AddressResource.php b/backend/app/Http/Resources/AddressResource.php new file mode 100644 index 0000000..49242d5 --- /dev/null +++ b/backend/app/Http/Resources/AddressResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->resource->id, + 'firstName' => $this->resource->firstName, + 'lastName' => $this->resource->lastName, + 'street' => $this->resource->street, + 'city' => $this->resource->city, + 'state' => $this->resource->state, + 'pinCode' => $this->resource->pinCode, + ]; + } +} diff --git a/backend/app/Models/Address.php b/backend/app/Models/Address.php new file mode 100644 index 0000000..a797fdf --- /dev/null +++ b/backend/app/Models/Address.php @@ -0,0 +1,49 @@ +|Address newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Address newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Address query() + * + * @property-read Collection $users + * @property-read int|null $users_count + * @property int $id + * @property string $first_name + * @property string $last_name + * @property string $street + * @property string $city + * @property string $state + * @property string $pin + * @property Carbon|null $created_at + * @property Carbon|null $updated_at + * + * @method static \Illuminate\Database\Eloquent\Builder|Address whereCity($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address whereFirstName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address whereLastName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address wherePin($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address whereState($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address whereStreet($value) + * @method static \Illuminate\Database\Eloquent\Builder|Address whereUpdatedAt($value) + * + * @mixin \Eloquent + */ +class Address extends Model +{ + protected $fillable = ['first_name', 'last_name', 'street', 'city', 'state', 'pin']; + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class); + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index c80bca7..da8c296 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -4,28 +4,35 @@ // use Illuminate\Contracts\Auth\MustVerifyEmail; use App\Enums\UserRoles; +use Database\Factories\UserFactory; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Notifications\DatabaseNotification; +use Illuminate\Notifications\DatabaseNotificationCollection; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Carbon; /** * @property int $id * @property string $name * @property string $email - * @property \Illuminate\Support\Carbon|null $email_verified_at + * @property Carbon|null $email_verified_at * @property string $password * @property string|null $remember_token - * @property \Illuminate\Support\Carbon|null $created_at - * @property \Illuminate\Support\Carbon|null $updated_at + * @property Carbon|null $created_at + * @property Carbon|null $updated_at * @property string $mobile_number * @property string $city * @property UserRoles $role - * @property-read \Illuminate\Database\Eloquent\Collection $carts + * @property-read Collection $carts * @property-read int|null $carts_count - * @property-read \Illuminate\Database\Eloquent\Collection $favoriteProducts + * @property-read Collection $favoriteProducts * @property-read int|null $favorite_products_count - * @property-read \Illuminate\Notifications\DatabaseNotificationCollection $notifications + * @property-read DatabaseNotificationCollection $notifications * @property-read int|null $notifications_count + * * @method static \Database\Factories\UserFactory factory($count = null, $state = []) * @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|User newQuery() @@ -41,11 +48,12 @@ * @method static \Illuminate\Database\Eloquent\Builder|User whereRememberToken($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereRole($value) * @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value) + * * @mixin \Eloquent */ class User extends Authenticatable { - /** @use HasFactory<\Database\Factories\UserFactory> */ + /** @use HasFactory */ use HasFactory; use Notifiable; @@ -88,7 +96,7 @@ protected function casts(): array ]; } - public function favoriteProducts() + public function favoriteProducts(): BelongsToMany { return $this->belongsToMany(Product::class, 'favorite_products', 'user_id', 'product_id'); } @@ -102,4 +110,9 @@ public function carts() { return $this->hasMany(Cart::class); } + + public function addresses(): BelongsToMany + { + return $this->belongsToMany(Address::class); + } } diff --git a/backend/database/migrations/2026_03_12_141247_create_addresses_table.php b/backend/database/migrations/2026_03_12_141247_create_addresses_table.php new file mode 100644 index 0000000..9159499 --- /dev/null +++ b/backend/database/migrations/2026_03_12_141247_create_addresses_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('first_name'); + $table->string('last_name'); + $table->string('street'); + $table->string('city'); + $table->string('state'); + $table->string('pin'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('addresses'); + } +}; diff --git a/backend/database/migrations/2026_03_12_141909_address_user.php b/backend/database/migrations/2026_03_12_141909_address_user.php new file mode 100644 index 0000000..c17f88d --- /dev/null +++ b/backend/database/migrations/2026_03_12_141909_address_user.php @@ -0,0 +1,29 @@ +foreignIdFor(User::class)->constrained()->cascadeOnDelete(); + $table->foreignIdFor(Address::class)->constrained()->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('address_user'); + } +}; diff --git a/backend/routes/api.php b/backend/routes/api.php index c1dc546..84d0b54 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -7,6 +7,7 @@ use App\Http\Controllers\ProductController; use App\Http\Controllers\ProductImagesController; use App\Http\Controllers\RegisteredUserController; +use App\Http\Controllers\UserAddressController; use Illuminate\Support\Facades\Route; Route::middleware('guest')->group(function () { @@ -24,6 +25,8 @@ Route::apiSingleton('/cart', CartController::class) ->creatable() ->destroyable(); + + Route::apiResource('user.addresses', UserAddressController::class)->shallow(); }); Route::get('/categories', [ProductCategoryController::class, 'index']); Route::apiResource('products', ProductController::class);