Compare commits

..

42 Commits

Author SHA1 Message Date
kusowl
5d6f75bdc2 fix: refactor code to group into folder 2026-03-25 17:17:35 +05:30
kusowl
ca0aaaa84b feature: implement payment verification, make cart as converted after successful payment 2026-03-25 15:25:11 +05:30
kusowl
8e1fe1336e chore: format, add LaraDumps and ArchTests 2026-03-24 18:52:49 +05:30
kusowl
f5927b8d08 feature: implement payment method selector and checkout confirmation page 2026-03-24 18:52:06 +05:30
kusowl
14cb5a36ae feature: implement stripe webhook 2026-03-24 18:50:24 +05:30
kusowl
a51a2cd436 chore: ide helpers and formatting 2026-03-23 17:29:55 +05:30
kusowl
2aa76db042 feature: implement payment gateway
- implement stripe checkout gateway
- add payment gateway factory and service
2026-03-23 17:29:24 +05:30
kusowl
0799965212 wip: stripe implementation
- add model, dto
2026-03-20 19:03:33 +05:30
kusowl
783ae6925b Refactor: update address when order creation request is being sent with different address id. 2026-03-19 16:38:34 +05:30
kusowl
d065ef1db9 feature: order create endpoint 2026-03-18 18:59:45 +05:30
kusowl
a0e5cda432 Merge branch 'feature/address-ui' into staging 2026-03-17 18:22:42 +05:30
kusowl
11115c8dc0 Merge branch 'feature/address-api' into staging 2026-03-17 18:22:35 +05:30
kusowl
bb3aafd89e feature: show order summary on address page 2026-03-17 17:05:49 +05:30
kusowl
419e8281e2 BREAKING CHANGE: change obervable name from cartItem$ to cartItems$ 2026-03-17 16:10:05 +05:30
kusowl
63b3d06d3a BREAKING CHANGE: remove wrapping for individual address resource 2026-03-17 11:00:32 +05:30
kusowl
24bdfe9cc6 feature: fetch, edit and add address
- fetch existing addresses from api,
- user can edit existing address
- user can add new address
2026-03-17 10:58:22 +05:30
kusowl
3059a923b4 refactor: move auth service to core 2026-03-16 13:05:06 +05:30
kusowl
6d1cb81e6b wip: checkout page
- add button to go address page in cart ui
- add template for address page
2026-03-13 18:54:48 +05:30
kusowl
3ae3374eec chore: formatting
- updated format via pint
2026-03-13 18:09:06 +05:30
kusowl
c27ae1969f feature: add address api
- users can save, edi and delete addresses
- each user can have multiple address
- used shallow routes for address
2026-03-13 18:08:45 +05:30
kusowl
61ecbec994 chore: add command to generate dto
- add make:dto command which generates dto in App\Data namespace
- varient: --input (default), --output
2026-03-13 11:28:22 +05:30
kusowl
03f044b8d3 chore: add ide helper
- add laravel ide helper package for better LSP support
- update dependencies
2026-03-13 10:08:55 +05:30
kusowl
136d6cf97e Merge branch 'backend' into staging 2026-03-12 10:47:06 +05:30
kusowl
3b5bf80f39 Merge branch 'frontend' into staging 2026-03-12 10:45:49 +05:30
kusowl
50c956c051 fix: make header logo navigation by router 2026-03-12 10:43:38 +05:30
kusowl
ad957efcf0 feature: add to cart
- make the cart service dependable on BehavorialSubject, migrated from
siganls
- implement add to cart service
2026-03-11 19:00:24 +05:30
kusowl
27a04c6458 minor: some quick design and color changes
make the dropdown hover color from gradient to simple gray shade
make the button 3d
change the product card design in home page
add add to cart button in the home page
change design of button ghost
2026-03-11 11:25:07 +05:30
kusowl
2b88cee10b feature: user can change quantity and remove products 2026-03-10 19:07:42 +05:30
kusowl
5bbec0ee2b feature: update quantity and remove product from cart
add endpoint for update quantity of products (min:1, max:10)
add endpoint for removing product from cart
2026-03-10 19:01:52 +05:30
kusowl
1656739ecd feature: add and fetch products in cart api
- schema for cart and product
- define relationship, DTO, Resource and API collections
- Add post and get endpoint for cart api
2026-03-09 19:07:27 +05:30
kusowl
9000ea0052 feature: fetch cart products from api
- show total cart item count on header
- fetch and show cart items on cart modal
2026-03-09 19:04:31 +05:30
kusowl
0faccba476 Merge branch 'frontend' into staging 2026-03-05 18:28:08 +05:30
kusowl
3c2233d53e fix: make favorite button in product show page sync with db 2026-03-05 18:08:52 +05:30
kusowl
95afd46406 fix: add isFavorite on Product show response 2026-03-05 18:07:46 +05:30
kusowl
0f56303d59 fix sanctum and session environment variables 2026-03-05 14:43:21 +05:30
kusowl
a4eebef321 Merge branch 'feature/products' into staging 2026-03-05 13:48:30 +05:30
kusowl
a57566c1fe Merge branch 'backend' into staging 2026-03-05 13:48:24 +05:30
kusowl
7e1ecf35b9 make favorite state persistant with api 2026-03-05 13:34:37 +05:30
kusowl
ae008fbc9c chore: add isFavorite in produtcs response
- refactor code to use query
- add active column in products
2026-03-05 13:32:49 +05:30
kusowl
8ef4383bd9 feature: endpoint to favorite product 2026-03-05 10:32:40 +05:30
kusowl
b575b42f22 Merge remote-tracking branch 'origin/feature/products' into fix/history-issue
# Conflicts:
#	src/app/features/product/components/product-card/product-card.html
#	src/app/features/product/components/product-card/product-card.ts
#	src/app/features/product/services/product-service.ts
2026-03-03 17:40:12 +05:30
kusowl
553637d8e2 fix: commit whole changes 2026-03-03 17:27:30 +05:30
197 changed files with 36134 additions and 377 deletions

12
.ai/mcp/mcp.json Normal file
View File

@ -0,0 +1,12 @@
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": [
"-y",
"@angular/cli",
"mcp"
]
}
}
}

2
.gitignore vendored
View File

@ -38,6 +38,8 @@ yarn-error.log
testem.log
/typings
__screenshots__/
*.cache
.php-cs-fixer.dist.php
# System files
.DS_Store

3
.phpactor.json Normal file
View File

@ -0,0 +1,3 @@
{
"indexer.exclude_patterns": ["/node_modules/**/*", "/backend/**/*"]
}

1
.rgignore Normal file
View File

@ -0,0 +1 @@
backend/

View File

@ -2,7 +2,8 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm"
"packageManager": "npm",
"analytics": false
},
"newProjectRoot": "projects",
"projects": {

View File

@ -31,7 +31,7 @@ SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
SESSION_DOMAIN=localhost
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
@ -64,3 +64,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
FRONTEND_URL=http://localhost:4200
SANCTUM_STATEFUL_DOMAINS=localhost:4200
STRIPE_SECRET_KEY=sk_test_51TCvFrJG0RVtUg4VTqHZC2szosam9Mf0Nq0Sh71tdIKdld5DnOUhUl4VvFBZVRWPM9G5hLPNVmH8YNXqm2R6fR5U00fsEsLb1d
STRIPE_WEBHOOK_KEY=

1
backend/.gitignore vendored
View File

@ -22,3 +22,4 @@
Homestead.json
Homestead.yaml
Thumbs.db
laradumps.yaml

2355
backend/.phpstorm.meta.php Normal file

File diff suppressed because it is too large Load Diff

28657
backend/_ide_helper.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,307 @@
<?php
// @formatter:off
// phpcs:ignoreFile
/**
* A helper file for your Eloquent Models
* Copy the phpDocs from this file to the correct Model,
* And remove them from this file, to prevent double declarations.
*
* @author Barry vd. Heuvel <barryvdh@gmail.com>
*/
namespace App\Models{
/**
* @property int $id
* @property string $first_name
* @property string $last_name
* @property string $street
* @property string $city
* @property string $state
* @property string $pin
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read int|null $users_count
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereCity($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereFirstName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereLastName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address wherePin($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereState($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereStreet($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Address whereUpdatedAt($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperAddress {}
}
namespace App\Models{
/**
* @property int $id
* @property int $user_id
* @property \App\Enums\Cart\CartStatus $status
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Order|null $order
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Product> $products
* @property-read int|null $products_count
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart active()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Cart withProducts()
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperCart {}
}
namespace App\Models{
/**
* @property int $id
* @property int $user_id
* @property int $cart_id
* @property string $status
* @property string $shipping_first_name
* @property string $shipping_last_name
* @property string $shipping_street
* @property string $shipping_city
* @property string $shipping_state
* @property string $shipping_pin
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Cart $cart
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Payment> $payments
* @property-read int|null $payments_count
* @property-read \App\Models\StripeSession|null $stripeSession
* @property-read mixed $total_amount
* @property-read \App\Models\User $user
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereCartId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereShippingCity($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereShippingFirstName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereShippingLastName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereShippingPin($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereShippingState($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereShippingStreet($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Order whereUserId($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperOrder {}
}
namespace App\Models{
/**
* @property int $id
* @property int $order_id
* @property string $transaction_id
* @property int $amount
* @property string $currency
* @property string $payment_method
* @property int $payment_status_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Order|null $order
* @property-read \App\Models\PaymentStatus|null $paymentStatus
* @property-read mixed $status
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereAmount($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereCurrency($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereOrderId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment wherePaymentMethod($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment wherePaymentStatusId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereTransactionId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Payment whereUpdatedAt($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperPayment {}
}
namespace App\Models{
/**
* @property int $id
* @property string $name
* @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentStatus newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentStatus newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentStatus query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentStatus whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PaymentStatus whereName($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperPaymentStatus {}
}
namespace App\Models{
/**
* @property int $id
* @property string $title
* @property string|null $slug
* @property string $description
* @property numeric $actual_price
* @property numeric $list_price
* @property int $product_category_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property bool $is_active
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Cart> $carts
* @property-read int|null $carts_count
* @property-read \App\Models\ProductCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $favoritedBy
* @property-read int|null $favorited_by_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProductImage> $images
* @property-read int|null $images_count
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product active()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereActualPrice($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereIsActive($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereListPrice($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereProductCategoryId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereSlug($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereTitle($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Product whereUpdatedAt($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperProduct {}
}
namespace App\Models{
/**
* @property int $id
* @property string $name
* @property string $slug
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory whereSlug($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductCategory whereUpdatedAt($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperProductCategory {}
}
namespace App\Models{
/**
* @property int $id
* @property string $path
* @property int $product_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Product|null $product
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage wherePath($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage whereProductId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ProductImage whereUpdatedAt($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperProductImage {}
}
namespace App\Models{
/**
* @property int $id
* @property string $session_id
* @property int $order_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Order|null $order
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession whereOrderId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession whereSessionId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|StripeSession whereUpdatedAt($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperStripeSession {}
}
namespace App\Models{
/**
* @property int $id
* @property string $name
* @property string $email
* @property \Illuminate\Support\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 string $mobile_number
* @property string $city
* @property \App\Enums\User\UserRoles $role
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Address> $addresses
* @property-read int|null $addresses_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Cart> $carts
* @property-read int|null $carts_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Product> $favoriteProducts
* @property-read int|null $favorite_products_count
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Order> $orders
* @property-read int|null $orders_count
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCity($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereMobileNumber($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRole($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class IdeHelperUser {}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Actions\Address;
use App\Models\Address;
final readonly class DeleteUserAddressAction
{
/**
* Execute the action.
*/
public function execute(Address $address)
{
$address->users()->detach();
$address->delete();
return response()->noContent();
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Actions\Address;
use App\Data\Address\AddUserAddressRequestDTO;
use App\Data\Address\UserAddressResponseDTO;
use App\Models\User;
final readonly class SaveUserAddressAction
{
/**
* Execute the action.
*/
public function execute(AddUserAddressRequestDTO $data, User $user)
{
return UserAddressResponseDTO::fromModel(
$user
->addresses()
->create($data->toArray())
);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Actions\Address;
use App\Data\Address\UpdateUserAddressRequestDTO;
use App\Data\Address\UserAddressResponseDTO;
use App\Models\Address;
final readonly class UpdateUserAddressAction
{
/**
* Execute the action.
*/
public function execute(UpdateUserAddressRequestDTO $data, Address $address)
{
$address->update($data->toArray());
$address->refresh();
return UserAddressResponseDTO::fromModel($address);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Actions\Cart;
use App\Data\Cart\AddToCartDTO;
use App\Data\Cart\CartDTO;
use App\Enums\Cart\CartStatus;
use App\Models\Product;
use App\Models\User;
final readonly class AddProductToCartAction
{
/**
* Execute the action.
*/
public function execute(AddToCartDTO $cartData, User $user)
{
$cart = $user->carts()->active()->firstOrCreate([
'status' => CartStatus::Active,
]);
$price = Product::query()->whereId($cartData->productId)->value('actual_price');
$productInCart = $cart->products()->find($cartData->productId);
if ($productInCart) {
$newQuantity = $productInCart->pivot->quantity + $cartData->quantity;
$cart->products()->updateExistingPivot($cartData->productId, [
'quantity' => $newQuantity,
'price' => $price,
]);
} else {
$cart->products()->attach($cartData->productId, [
'quantity' => $cartData->quantity,
'price' => $price,
]);
}
return CartDTO::fromModel($cart->load(['products' => function ($query) {
$query->withPivot('quantity', 'price');
}]));
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Actions\Cart;
use App\Data\Cart\CartDTO;
use App\Models\User;
final readonly class GetActiveUserCartAction
{
/**
* Execute the action.
*/
public function execute(User $user)
{
return CartDTO::fromModel($user->carts()->active()->withProducts()->first());
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Actions\Cart;
use App\Enums\Cart\CartStatus;
use App\Models\Cart;
final readonly class MarkCartAsConvertedAction
{
/**
* Execute the action.
*/
public function execute(Cart $cart): Cart
{
$cart->status = CartStatus::Converted;
$cart->save();
return $cart;
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Actions\Cart;
use App\Models\User;
final readonly class RemoveProductFromCartAction
{
public function __construct(private GetActiveUserCartAction $activeCartAction) {}
/**
* Execute the action.
*/
public function execute(int $productId, User $user)
{
$cart = $user->carts()->active()->sole();
$cart->products()->detach($productId);
return $this->activeCartAction->execute($user);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Actions\Cart;
use App\Data\Cart\AddToCartDTO;
use App\Data\Cart\CartDTO;
use App\Models\User;
use InvalidArgumentException;
final readonly class UpdateProductInCartAction
{
/**
* Execute the action.
*
* @throws InvalidArgumentException
*/
public function execute(AddToCartDTO $cartData, User $user)
{
$cart = $user->carts()->active()->sole();
$productInCart = $cart->products()->find($cartData->productId);
throw_if($productInCart === null, new InvalidArgumentException('Product not found'));
$cart->products()->updateExistingPivot($cartData->productId, [
'quantity' => $cartData->quantity,
]);
return CartDTO::fromModel($cart->load(['products' => function ($query) {
$query->withPivot('quantity', 'price');
}]));
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Actions\Order;
use App\Data\Order\OrderRequestDTO;
use App\Enums\Cart\CartStatus;
use App\Exceptions\StaleCartException;
use App\Models\Address;
use App\Models\Cart;
use App\Models\Order;
use App\Models\User;
final readonly class CreateOrderAction
{
/**
* Execute the action.
*
* @throws StaleCartException
*/
public function execute(OrderRequestDTO $dto, User $user): Order
{
/** @var Cart $cart */
$cart = $user->carts()->where('id', $dto->cartId)->sole();
if ($cart->status !== CartStatus::Active) {
throw new StaleCartException(userId: $user->id, cartId: $cart->id);
}
/** @var Address $address */
$address = $user->addresses()->where('id', $dto->addressId)->sole();
// Check if user has already created an order with the same cart. If yes, then take that order.
/** @var Order $order */
$order = $user->orders()->firstOrNew(['cart_id' => $cart->id]);
$order->cart_id = $cart->id;
$order->shipping_first_name = $address->first_name;
$order->shipping_last_name = $address->last_name;
$order->shipping_street = $address->street;
$order->shipping_city = $address->city;
$order->shipping_state = $address->state;
$order->shipping_pin = $address->pin;
$order->save();
return $order;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Actions\Payment;
use App\Actions\Cart\MarkCartAsConvertedAction;
use App\Enums\Payment\PaymentStatusEnum;
use App\Models\Payment;
use App\Models\PaymentStatus;
use DB;
use Log;
use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Throwable;
final readonly class MarkPaymentAsPaidAction
{
public function __construct(private MarkCartAsConvertedAction $cartAsConvertedAction) {}
/**
* Execute the action.
*
* @throws NotFoundResourceException|Throwable
*/
public function execute(Payment $payment): bool
{
try {
DB::beginTransaction();
// get the cart and make the status to converted
$cart = $payment->order->cart;
$this->cartAsConvertedAction->execute($cart);
$status = PaymentStatus::whereName(PaymentStatusEnum::Paid->value)->value('id');
if (! $status) {
throw new NotFoundResourceException('Paid Status not found');
}
$payment->payment_status_id = $status;
$payment->save();
DB::commit();
return true;
} catch (Throwable $e) {
Log::error('Cannot mark order payment as paid', [$e->getMessage()]);
DB::rollBack();
return false;
}
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Actions\Payment;
use App\Data\Payment\PaymentResponseDTO;
use App\Enums\Payment\PaymentModes;
use App\Enums\Payment\PaymentStatusEnum;
use App\Models\Order;
use App\Models\PaymentStatus;
use App\Services\Payment\PaymentGatewayFactory;
use DB;
use Log;
use Throwable;
final readonly class ProcessOrderPaymentAction
{
public function __construct(
private PaymentGatewayFactory $paymentGatewayFactory,
) {}
/**
* Execute the action.
*/
public function execute(Order $order, PaymentModes $mode): PaymentResponseDTO
{
$gateway = $this->paymentGatewayFactory->make($mode);
try {
DB::beginTransaction();
$response = $gateway->charge($order);
if ($response->isSuccess) {
$order->payments()->create(
[
'transaction_id' => $response->transactionId,
'amount' => $response->amount,
'currency' => $response->currency,
'payment_method' => $response->method->value,
'payment_status_id' => PaymentStatus::getIdByName(PaymentStatusEnum::Unpaid->value),
]
);
}
DB::commit();
return $response;
} catch (Throwable $e) {
DB::rollBack();
Log::error('Error occurred while processing the payment.', [$e->getMessage()]);
abort(500, 'Something went wrong. Please try again or contact us to get in touch with our support team. ');
}
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace App\Actions;
namespace App\Actions\Product;
use App\Data\UploadImageDTO;
use App\Data\Upload\UploadImageDTO;
final readonly class CreateProductAction
{

View File

@ -1,8 +1,8 @@
<?php
namespace App\Actions;
namespace App\Actions\Product;
use App\Data\ProductCategoryDTO;
use App\Data\Product\ProductCategoryDTO;
use App\Models\ProductCategory;
final readonly class GetAllProductCategory

View File

@ -0,0 +1,57 @@
<?php
namespace App\Actions\Stripe;
use App\Data\Payment\VerifiedCheckoutResponseDTO;
use App\Models\User;
use Exception;
use Log;
use Stripe\Exception\ApiErrorException;
use Stripe\StripeClient;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
final readonly class VerifyStripeSessionAction
{
public function __construct(private StripeClient $stripe) {}
/**
* Execute the action.
*/
public function execute(int $orderId, string $stripeSessionId, User $user): VerifiedCheckoutResponseDTO
{
/**
* Check if the order is actually made by user
*/
$order = $user->orders()
->whereId($orderId)
->first();
if (! $order) {
throw new AccessDeniedHttpException('Order is not made by you');
}
if (! $order->stripeSession()->where('session_id', $stripeSessionId)->exists()) {
throw new AccessDeniedHttpException('Stripe session is not made by you');
}
try {
$session = $this->stripe->checkout->sessions->retrieve($stripeSessionId);
} catch (ApiErrorException $e) {
Log::error('Stripe api is not available: ', [$e->getMessage()]);
throw new ServiceUnavailableHttpException('Stripe api is not available');
} catch (Exception $e) {
throw new NotFoundHttpException('Invalid Stripe session id');
}
if ($session->payment_status !== 'paid' || $session->status !== 'complete') {
return VerifiedCheckoutResponseDTO::failure('Payment Unsuccessful');
}
return VerifiedCheckoutResponseDTO::success(
message: 'Payment Successful',
amount: $session->amount_total,
transactionId: $session->payment_intent,
mode: $session->payment_method_types[0],
);
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace App\Actions;
namespace App\Actions\Upload;
use App\Data\UploadImageDTO;
use App\Data\Upload\UploadImageDTO;
final readonly class UploadImageAction
{

View File

@ -1,8 +1,8 @@
<?php
namespace App\Actions;
namespace App\Actions\User;
use App\Data\RegisterDTO;
use App\Data\User\RegisterDTO;
use App\Models\User;
final readonly class CreateUserAction

View File

@ -0,0 +1,63 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;
class MakeDtoCommand extends GeneratorCommand
{
/**
* The console command name and signature.
*
* @var string
*/
protected $name = 'make:dto';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new Data Transfer Object class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'DTO';
/**
* Get the stub file for the generator.
*/
protected function getStub(): string
{
if ($this->option('output')) {
return base_path('stubs/dto.output.stub');
}
return base_path('stubs/dto.input.stub');
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
*/
protected function getDefaultNamespace($rootNamespace): string
{
return $rootNamespace.'\Data';
}
/**
* Get the console command options.
*/
protected function getOptions(): array
{
return [
['input', 'i', InputOption::VALUE_NONE, 'Generate an Input DTO (default)'],
['output', 'o', InputOption::VALUE_NONE, 'Generate an Output DTO'],
];
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Contracts;
use App\Data\Payment\PaymentResponseDTO;
use App\Models\Order;
interface PaymentGateway
{
public function charge(Order $order): PaymentResponseDTO;
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Data\Address;
use App\Contracts\InputDataTransferObject;
use Illuminate\Foundation\Http\FormRequest;
final readonly class AddUserAddressRequestDTO implements InputDataTransferObject
{
public function __construct(
public string $firstName,
public string $lastName,
public string $street,
public string $city,
public string $state,
public string $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
);
}
/**
* @return array<string, mixed>
*/
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,
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Data\Address;
use App\Contracts\InputDataTransferObject;
use Illuminate\Foundation\Http\FormRequest;
final readonly class UpdateUserAddressRequestDTO implements InputDataTransferObject
{
public function __construct(
public ?string $firstName = null,
public ?string $lastName = null,
public ?string $street = null,
public ?string $city = null,
public ?string $state = null,
public ?string $pinCode = 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'),
);
}
/**
* @return array<string = null = null, mixed>
*/
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);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Data\Address;
use App\Contracts\OutputDataTransferObject;
use App\Models\Address;
final readonly class UserAddressResponseDTO implements OutputDataTransferObject
{
public function __construct(
public int $id,
public string $firstName,
public string $lastName,
public string $street,
public string $city,
public string $state,
public string $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
);
}
/**
* @return array<string, mixed>
*/
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,
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Data\Cart;
use App\Contracts\InputDataTransferObject;
use Illuminate\Foundation\Http\FormRequest;
final readonly class AddToCartDTO implements InputDataTransferObject
{
public function __construct(
public int $productId,
public int $quantity
) {}
public static function fromRequest(FormRequest $request): InputDataTransferObject
{
return new self(
productId: $request->productId,
quantity: $request->quantity
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'productId' => $this->productId,
'quantity' => $this->quantity,
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Data\Cart;
use App\Contracts\OutputDataTransferObject;
use App\Models\Cart;
final readonly class CartDTO implements OutputDataTransferObject
{
/**
* @param CartItemDTO[] $items
*/
public function __construct(
public int $id,
public ?int $itemsCount = null,
public ?int $totalPrice = null,
public array $items = []
) {}
public static function fromModel(Cart $cart)
{
return new self(
id: $cart->id,
itemsCount: $cart->products->count(),
totalPrice: $cart->products->sum(fn ($product) => $product->pivot->price * $product->pivot->quantity),
items: $cart->products->map(fn ($product) => new CartItemDTO(
id: $product->id,
title: $product->title,
quantity: $product->pivot->quantity,
price: $product->actual_price,
subtotal: $product->actual_price * $product->pivot->quantity,
image: $product->images->first()->path,
))->toArray()
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'itemsCount' => $this->itemsCount,
'totalPrice' => $this->totalPrice,
'items' => $this->items,
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Data\Cart;
use App\Contracts\OutputDataTransferObject;
final readonly class CartItemDTO implements OutputDataTransferObject
{
public function __construct(
public int $id,
public string $title,
public int $quantity,
public float $price,
public float $subtotal,
public string $image
) {}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'quantity' => $this->quantity,
'price' => $this->price,
'subtotal' => $this->subtotal,
'image' => $this->image,
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Data\Order;
use App\Contracts\OutputDataTransferObject;
use Illuminate\Database\Eloquent\Model;
final readonly class OrderCreatedResponseDTO implements OutputDataTransferObject
{
public function __construct(
// TODO: Define your properties here
) {}
public static function fromModel(Model $model): OutputDataTransferObject
{
return new self;
// TODO: Map model data to properties
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
// TODO: Map properties to array
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Data\Order;
use App\Contracts\InputDataTransferObject;
use Illuminate\Foundation\Http\FormRequest;
final readonly class OrderRequestDTO implements InputDataTransferObject
{
public function __construct(
public int $cartId,
public string $addressId,
) {}
public static function fromRequest(FormRequest $request): OrderRequestDTO
{
return new self(
cartId: $request->cartId,
addressId: $request->addressId,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'cart_id' => $this->cartId,
'address_id' => $this->addressId,
];
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Data\Payment;
use App\Contracts\OutputDataTransferObject;
use App\Enums\Payment\PaymentModes;
final readonly class PaymentResponseDTO implements OutputDataTransferObject
{
public function __construct(
public bool $isSuccess,
public int $amount,
public string $currency,
public PaymentModes $method,
public ?string $transactionId = null,
public ?string $errorMessage = null,
public ?string $redirectUrl = null,
) {}
public static function success(
string $transactionId,
int $amount,
string $currency,
PaymentModes $method,
?string $redirectUrl = null
): self {
return new self(
isSuccess: true,
amount: $amount,
currency: $currency,
method: $method,
transactionId: $transactionId,
redirectUrl: $redirectUrl,
);
}
public static function failure(int $amount, string $currency, PaymentModes $method, string $errorMessage): self
{
return new self(
isSuccess: false,
amount: $amount,
currency: $currency,
method: $method,
errorMessage: $errorMessage,
);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'isSuccess' => $this->isSuccess,
'transactionId' => $this->transactionId,
'amount' => $this->amount,
'currency' => $this->currency,
'method' => $this->method,
'redirectUrl' => $this->redirectUrl,
'errorMessage' => $this->errorMessage,
];
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Data\Payment;
use App\Contracts\OutputDataTransferObject;
final readonly class VerifiedCheckoutResponseDTO implements OutputDataTransferObject
{
public function __construct(
public bool $isSuccess,
public string $message,
public ?int $amount = null,
public ?string $transactionId = null,
public ?string $mode = null
) {}
public static function failure(string $message): VerifiedCheckoutResponseDTO
{
return new self(isSuccess: false, message: $message);
}
public static function success(string $message, int $amount, string $transactionId, string $mode): VerifiedCheckoutResponseDTO
{
return new self(isSuccess: true, message: $message, amount: $amount, transactionId: $transactionId,
mode: $mode);
}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'isSuccess' => $this->isSuccess,
'message' => $this->message,
'amount' => $this->amount,
'transactionId' => $this->transactionId,
'paymentMethod' => $this->mode,
];
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Data;
namespace App\Data\Product;
use App\Contracts\OutputDataTransferObject;
use App\Models\ProductCategory;
@ -13,6 +13,15 @@ public function __construct(
public string $slug,
) {}
public static function fromModel(ProductCategory $category): self
{
return new self(
id: $category->id,
name: $category->name,
slug: $category->slug,
);
}
/**
* @return array<string, mixed>
*/
@ -24,13 +33,4 @@ public function toArray(): array
'slug' => $this->slug,
];
}
public static function fromModel(ProductCategory $category): self
{
return new self(
id: $category->id,
name: $category->name,
slug: $category->slug,
);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Data;
namespace App\Data\Product;
use App\Contracts\OutputDataTransferObject;
use App\Models\Product;
@ -22,8 +22,27 @@ public function __construct(
public array $productImages,
public ?string $updatedAt = null,
public ?string $createdAt = null,
public ?bool $isFavorite = null
) {}
public static function fromModel(Product $product): self
{
return new self(
id: $product->id,
title: $product->title,
slug: $product->slug,
description: $product->description,
actualPrice: $product->actual_price,
listPrice: $product->list_price,
category: ProductCategoryDTO::fromModel($product->category),
productImages: $product->images->map(fn (ProductImage $productImage) => ProductImageDTO::fromModel($productImage))->all(),
updatedAt: $product->updated_at,
createdAt: $product->created_at,
// this column is added by where exists query
isFavorite: $product->favorited_by_exists,
);
}
/**
* @return array<string, mixed>
*/
@ -41,22 +60,7 @@ public function toArray(): array
$this->productImages),
'updatedAt' => $this->updatedAt,
'createdAt' => $this->createdAt,
'isFavorite' => $this->isFavorite,
];
}
public static function fromModel(Product $product): self
{
return new self(
id: $product->id,
title: $product->title,
slug: $product->slug,
description: $product->description,
actualPrice: $product->actual_price,
listPrice: $product->list_price,
category: ProductCategoryDTO::fromModel($product->category),
productImages: $product->images->map(fn (ProductImage $productImage) => ProductImageDTO::fromModel($productImage))->all(),
updatedAt: $product->updated_at,
createdAt: $product->created_at,
);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Data;
namespace App\Data\Product;
use App\Contracts\OutputDataTransferObject;
use App\Models\ProductImage;
@ -13,6 +13,15 @@ public function __construct(
public ?int $productId = null,
) {}
public static function fromModel(ProductImage $productImage): self
{
return new self(
$productImage->id,
$productImage->path,
$productImage->product_id
);
}
/**
* @return array<string, mixed>
*/
@ -24,13 +33,4 @@ public function toArray(): array
'productId' => $this->productId,
];
}
public static function fromModel(ProductImage $productImage): self
{
return new self(
$productImage->id,
$productImage->path,
$productImage->product_id
);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Data\Stripe;
use App\Contracts\OutputDataTransferObject;
use App\Enums\Stripe\StripeCurrency;
final readonly class StripeLineItemDTO implements OutputDataTransferObject
{
public function __construct(
public StripeCurrency $currency,
public int $price,
public string $productName,
public string $productDescription,
public int $quantity
) {}
/**
* @return array<string, int|string>
*/
public function toArray(): array
{
return [
'price_data' => [
'currency' => $this->currency->value,
'unit_amount' => $this->price,
'product_data' => [
'name' => $this->productName,
'description' => $this->productDescription,
],
],
'quantity' => $this->quantity,
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Data\Stripe;
use App\Contracts\OutputDataTransferObject;
use App\Enums\Stripe\StripePaymentMode;
final readonly class StripeSessionDataDTO implements OutputDataTransferObject
{
/**
* @param StripeLineItemDTO[] $lineItems
*/
public function __construct(
public array $lineItems,
public StripePaymentMode $mode,
public string $successUrl,
public string $cancelUrl
) {}
/**
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'line_items' => array_map(fn (StripeLineItemDTO $dto) => $dto->toArray(), $this->lineItems),
'mode' => $this->mode->value,
'success_url' => $this->successUrl,
'cancel_url' => $this->cancelUrl,
];
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Data;
namespace App\Data\Upload;
use App\Models\Product;
use Illuminate\Http\UploadedFile;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Data;
namespace App\Data\User;
use App\Contracts\InputDataTransferObject;
use Illuminate\Foundation\Http\FormRequest;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Data;
namespace App\Data\User;
use App\Contracts\OutputDataTransferObject;

View File

@ -0,0 +1,11 @@
<?php
namespace App\Enums\Cart;
enum CartStatus: string
{
case Active = 'active'; // freshly created
case Converted = 'converted'; // user ordered
case Abandoned = 'abandoned'; // older than 24hrs
case Expired = 'expired'; // left for a long period
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Enums\Order;
/**
* Open -> The order was placed or created. There is work to do for the order, which can include processing payment, fulfilling, or processing returns.
* Archived -> The order was manually or automatically archived. Usually, this means the order was fulfilled.
* Canceled -> The order was canceled. If a canceled order was not fully refunded, then there might be work remaining for the order.
*/
enum OrderStatus: string
{
case Open = 'open';
case Archived = 'archived';
case Closed = 'closed';
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Enums\Payment;
enum PaymentModes: string
{
case StripeCheckout = 'stripeCheckout';
case CashOnDelivery = 'cashOnDelivery';
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Enums\Payment;
enum PaymentStatusEnum: string
{
case Unpaid = 'unpaid';
case Paid = 'paid';
case Refunded = 'refunded';
case Failed = 'failed';
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Enums\Stripe;
enum StripeCurrency: string
{
case INR = 'inr';
case USD = 'usd';
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Enums\Stripe;
enum StripeEventType: string
{
case CheckoutSessionCompleted = 'checkout.session.completed';
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Enums\Stripe;
enum StripePaymentMode: string
{
case Payment = 'payment';
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Enums;
namespace App\Enums\User;
enum UserRoles: string
{

View File

@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
class StaleCartException extends Exception
{
public function __construct(
public readonly int|string $userId,
public readonly int|string $cartId,
public $message = 'Attempt to create a order with a stale cart',
) {
parent::__construct($message);
}
public function context(): array
{
return [
'user_id' => $this->userId,
'cart_id' => $this->cartId,
];
}
public function render(): JsonResponse
{
return response()->json(['message' => 'Cart is stale'], 409);
}
}

View File

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Data\UserDTO;
use App\Data\User\UserDTO;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

View File

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Cart\AddProductToCartAction;
use App\Actions\Cart\GetActiveUserCartAction;
use App\Actions\Cart\RemoveProductFromCartAction;
use App\Actions\Cart\UpdateProductInCartAction;
use App\Data\Cart\AddToCartDTO;
use App\Http\Requests\AddProductToCartRequest;
use App\Http\Requests\RemoveProductFromCartRequest;
use App\Http\Requests\UpdateProductInCartRequest;
use App\Http\Resources\Cart\CartResource;
use Illuminate\Support\Facades\Auth;
class CartController extends Controller
{
/**
* Store a newly created resource in storage.
*/
public function store(AddProductToCartRequest $request, AddProductToCartAction $addProductAction)
{
$addToCartData = AddToCartDTO::fromRequest($request);
$cart = $addProductAction->execute($addToCartData, Auth::user());
return new CartResource($cart);
}
/**
* Display the specified resource.
*/
public function show(GetActiveUserCartAction $action)
{
return new CartResource($action->execute(Auth::user()));
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateProductInCartRequest $request, UpdateProductInCartAction $action)
{
$updateCartData = AddToCartDTO::fromRequest($request);
$cart = $action->execute($updateCartData, $request->user());
return new CartResource($cart);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(RemoveProductFromCartRequest $request, RemoveProductFromCartAction $action)
{
$user = $request->user();
try {
$cart = $action->execute($request->productId, $user);
return new CartResource($cart);
} catch (ModelNotFoundException $e) {
Log::error('No active cart found when removing a product from cart.', [
'user' => $user->id,
'error' => $e->getMessage(),
]);
return response()->json([
'message' => 'No active cart found.',
], 404);
} catch (MultipleRecordsFoundException $e) {
Log::error('Multiple active carts found for the user', [
'user' => $user->id,
'error' => $e->getMessage(),
]);
return response()->json([
'message' => 'Multiple Active carts found.',
], 409);
}
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\Product\FavouriteProductResource;
use App\Models\FavouriteProduct;
use App\Models\Product;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class FavouriteProductController extends Controller
{
public function index()
{
return FavouriteProductResource::collection(FavouriteProduct::all());
}
public function toggle(Request $request, Product $product)
{
/**
* @var User $user
*/
$user = $request->user();
$changes = $user->favoriteProducts()->toggle($product);
Log::info('hi again');
// If changes has any item, that means a product has been attached.
$isFavorite = count($changes['attached']) > 0;
return response()->json([
'message' => $isFavorite ? 'Product added to favorites' : 'Product removed from favorites',
'isFavorite' => $isFavorite,
]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Order\CreateOrderAction;
use App\Data\Order\OrderRequestDTO;
use App\Exceptions\StaleCartException;
use App\Http\Requests\StoreOrderRequest;
use App\Http\Requests\UpdateOrderRequest;
use App\Models\Order;
class OrderController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*
* @throws StaleCartException
*/
public function store(StoreOrderRequest $request, CreateOrderAction $action)
{
$order = $action->execute(OrderRequestDTO::fromRequest($request), $request->user());
return response()->json([
'message' => 'Order created successfully',
'orderId' => $order->id,
], 201);
}
/**
* Display the specified resource.
*/
public function show(Order $order)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateOrderRequest $request, Order $order)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Order $order)
{
//
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Payment\ProcessOrderPaymentAction;
use App\Actions\Stripe\VerifyStripeSessionAction;
use App\Enums\Payment\PaymentModes;
use App\Http\Requests\PaymentRequest;
use App\Http\Requests\VerifyPaymentRequest;
use App\Http\Resources\Payment\PaymentResource;
use App\Models\Order;
use App\Models\Payment;
class PaymentController extends Controller
{
public function index()
{
return PaymentResource::collection(Payment::all());
}
public function store(PaymentRequest $request, Order $order, ProcessOrderPaymentAction $action)
{
$response = $action->execute($order, PaymentModes::tryFrom($request->mode));
return new PaymentResource($response);
}
public function verify(VerifyPaymentRequest $request, VerifyStripeSessionAction $action)
{
return $action->execute($request->orderId, $request->sessionId, $request->user())->toArray();
}
public function show(Payment $payment)
{
return new PaymentResource($payment);
}
public function update(PaymentRequest $request, Payment $payment)
{
$payment->update($request->validated());
return new PaymentResource($payment);
}
}

View File

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Actions\GetAllProductCategory;
use App\Actions\Product\GetAllProductCategory;
class ProductCategoryController extends Controller
{

View File

@ -2,18 +2,20 @@
namespace App\Http\Controllers;
use App\Data\ProductDTO;
use App\Data\Product\ProductDTO;
use App\Http\Requests\CreateProductRequest;
use App\Http\Resources\ProductResource;
use App\Http\Resources\Product\ProductResource;
use App\Models\Product;
use App\Queries\GetProductsQuery;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ProductController extends Controller
{
public function index()
public function index(GetProductsQuery $getProductsQuery)
{
$paginator = Product::query()->with(['category:id,name,slug', 'images:id,path,product_id'])->paginate();
$paginatedDtos = $paginator->through(fn ($product) => ProductDTO::fromModel($product));
$products = $getProductsQuery->get(Auth::user());
$paginatedDtos = $products->through(fn ($product) => ProductDTO::fromModel($product));
return ProductResource::collection($paginatedDtos);
}
@ -25,7 +27,10 @@ public function store(CreateProductRequest $request)
public function show(string $slug)
{
$product = Product::where('slug', $slug)->with(['category:id,name,slug', 'images:id,path,product_id'])->firstOrFail();
$product = Product::where('slug', $slug)
->with(['category:id,name,slug', 'images:id,path,product_id'])
->withExists('favoritedBy')
->firstOrFail();
return new ProductResource(ProductDTO::fromModel($product));
}

View File

@ -2,8 +2,8 @@
namespace App\Http\Controllers;
use App\Actions\UploadImageAction;
use App\Data\UploadImageDTO;
use App\Actions\Upload\UploadImageAction;
use App\Data\Upload\UploadImageDTO;
use App\Http\Requests\UploadImageRequest;
use App\Models\ProductImages;

View File

@ -2,8 +2,8 @@
namespace App\Http\Controllers;
use App\Actions\CreateUserAction;
use App\Data\RegisterDTO;
use App\Actions\User\CreateUserAction;
use App\Data\User\RegisterDTO;
use App\Http\Requests\RegisterUserRequest;
use Illuminate\Http\JsonResponse;

View File

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Payment\MarkPaymentAsPaidAction;
use App\Enums\Stripe\StripeEventType;
use App\Models\Payment;
use Illuminate\Http\Request;
use Log;
use Stripe\Exception\SignatureVerificationException;
use Stripe\Webhook;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use UnexpectedValueException;
class StripeWebhookController extends Controller
{
public function __construct(private readonly MarkPaymentAsPaidAction $paidAction) {}
public function __invoke(Request $request)
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
try {
$event = Webhook::constructEvent($payload, $sigHeader, config('services.stripe.webhook'));
} catch (SignatureVerificationException|UnexpectedValueException $e) {
Log::error('Stripe webhook signature verification error.', [$e->getMessage()]);
throw new BadRequestHttpException('Invalid Signature');
}
if ($event->type === StripeEventType::CheckoutSessionCompleted->value) {
$sessionId = $event->data->object->id ?? null;
if ($sessionId) {
$this->handleCheckoutSessionCompleted($sessionId);
} else {
throw new NotFoundHttpException('Session id not found in event');
}
}
}
private function handleCheckoutSessionCompleted(string $sessionId): void
{
$payment = Payment::where('transaction_id', $sessionId)->first();
if (! $payment) {
Log::error('Stripe Webhook: Payment record not found.', ['session_id' => $sessionId]);
throw new NotFoundHttpException('Payment record not found');
}
$this->paidAction->execute($payment);
Log::info('Stripe Webhook: Payment successfully marked as paid', ['order_id' => $payment->order_id]);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Address\DeleteUserAddressAction;
use App\Actions\Address\SaveUserAddressAction;
use App\Actions\Address\UpdateUserAddressAction;
use App\Data\Address\AddUserAddressRequestDTO;
use App\Data\Address\UpdateUserAddressRequestDTO;
use App\Data\Address\UserAddressResponseDTO;
use App\Http\Requests\AddUserAddressRequest;
use App\Http\Requests\UpdateUserAddressRequest;
use App\Http\Resources\Address\AddressResource;
use App\Models\Address;
use Illuminate\Http\Request;
class UserAddressController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$addresses = $request->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);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class AddProductToCartRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'productId' => 'required|exists:products,id',
'quantity' => 'required|numeric|min:1',
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class AddUserAddressRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|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',
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests;
use App\Enums\Payment\PaymentModes;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PaymentRequest extends FormRequest
{
public function rules(): array
{
return [
'mode' => ['required', Rule::enum(PaymentModes::class)],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
@ -18,7 +19,7 @@ public function authorize(): bool
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class RemoveProductFromCartRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'productId' => ['required', 'exists:products,id'],
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreOrderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'cartId' => 'required|exists:carts,id',
'addressId' => 'required|exists:addresses,id',
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateOrderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProductInCartRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'productId' => ['required', 'exists:products,id'],
'quantity' => ['required', 'min:0', 'max:10'],
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserAddressRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|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',
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UploadImageRequest extends FormRequest
@ -17,7 +18,7 @@ public function authorize(): bool
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class VerifyPaymentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'sessionId' => 'required|exists:stripe_sessions,session_id',
'orderId' => 'required|exists:orders,id',
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Address;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class AddressCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Resources\Address;
use App\Data\Address\UserAddressResponseDTO;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property UserAddressResponseDTO $resource
*/
class AddressResource extends JsonResource
{
public static $wrap = null;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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,
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Cart;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CartItemCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Resources\Cart;
use App\Data\Cart\CartItemDTO;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
/**
* @property CartItemDTO $resource
*/
class CartItemResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->resource->id,
'title' => $this->resource->title,
'quantity' => $this->resource->quantity,
'price' => $this->resource->price,
'subtotal' => $this->resource->subtotal,
'image' => Storage::disk('public')->url($this->resource->image),
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Resources\Cart;
use App\Data\Cart\CartDTO;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @property CartDTO $resource
*/
class CartResource extends JsonResource
{
public static $wrap = null;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->resource->id,
'itemsCount' => $this->resource->itemsCount,
'totalPrice' => $this->resource->totalPrice,
'items' => CartItemResource::collection($this->resource->items),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Order;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Resources\Payment;
use App\Data\Payment\PaymentResponseDTO;
use App\Models\Payment;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Payment
*
* @property PaymentResponseDTO $resource
*/
class PaymentResource extends JsonResource
{
public static $wrap = null;
public function toArray(Request $request): array
{
return [
'success' => $this->resource->isSuccess,
'amount' => $this->resource->amount,
'currency' => $this->resource->currency,
'method' => $this->resource->method,
'redirectUrl' => $this->resource->redirectUrl,
'errorMessage' => $this->resource->errorMessage,
];
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources\Product;
use App\Models\FavouriteProduct;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin FavouriteProduct */
class FavouriteProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'user_id' => $this->user_id,
'product_id' => $this->product_id,
];
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Resources;
namespace App\Http\Resources\Product;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

View File

@ -1,9 +1,9 @@
<?php
namespace App\Http\Resources;
namespace App\Http\Resources\Product;
use App\Data\ProductDTO;
use App\Data\ProductImageDTO;
use App\Data\Product\ProductDTO;
use App\Data\Product\ProductImageDTO;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
@ -30,6 +30,7 @@ public function toArray(Request $request): array
return Storage::disk('public')->url($productImage->path);
}, $this->resource->productImages),
'updatedAt' => $this->resource->updatedAt,
'isFavorite' => $this->resource->isFavorite,
];
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @mixin IdeHelperAddress
*/
class Address extends Model
{
protected $fillable = ['first_name', 'last_name', 'street', 'city', 'state', 'pin'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Models;
use App\Enums\Cart\CartStatus;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @mixin IdeHelperCart
*/
class Cart extends Model
{
protected $fillable = ['user_id', 'status'];
public function products(): BelongsToMany
{
return $this->belongsToMany(Product::class)
->withPivot('price', 'quantity')
->withTimestamps();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'status' => CartStatus::class,
];
}
#[Scope]
protected function active(Builder $query)
{
return $query->where('status', CartStatus::Active);
}
#[Scope]
protected function withProducts(Builder $query)
{
return $query->with(['products' => function ($product) {
$product->withPivot('quantity', 'price');
}]);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* @mixin IdeHelperOrder
*/
class Order extends Model
{
public $fillable = [
'user_id', 'cart_id', 'status', 'shipping_city', 'shipping_street', 'shipping_last_name', 'shipping_first_name',
'shipping_state', 'shipping_pin',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function cart(): BelongsTo
{
return $this->belongsTo(Cart::class);
}
/**
* Stripe session id helps to update status later from webhook
*
* @return HasOne<StripeSession>
*/
public function stripeSession(): HasOne
{
return $this->hasOne(StripeSession::class);
}
/**
* @return HasMany<Payment>
*/
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
protected function totalAmount(): Attribute
{
return Attribute::make(
fn () => $this->cart?->products->sum(fn ($product) => $product->pivot->price * $product->pivot->quantity) ?? 0
);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @mixin IdeHelperPayment
*/
class Payment extends Model
{
protected $fillable = [
'order_id',
'transaction_id',
'amount',
'currency',
'payment_method',
'payment_status_id',
'error_message',
];
protected $with = ['paymentStatus'];
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function paymentStatus(): BelongsTo
{
return $this->belongsTo(PaymentStatus::class);
}
protected function status(): Attribute
{
return Attribute::make(fn () => $this->paymentStatus?->name);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @mixin IdeHelperPaymentStatus
*/
class PaymentStatus extends Model
{
public $timestamps = false;
protected $fillable = [
'name',
];
public static function getIdByName(string $name): ?int
{
return static::where('name', $name)->value('id');
}
}

View File

@ -2,11 +2,17 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
/**
* @mixin IdeHelperProduct
*/
class Product extends Model
{
protected $fillable = [
@ -28,6 +34,22 @@ public function images(): HasMany
return $this->hasMany(ProductImage::class, 'product_id', 'id');
}
public function favoritedBy(): BelongsToMany
{
return $this->belongsToMany(User::class, 'favorite_products');
}
public function carts()
{
return $this->belongsToMany(Cart::class);
}
#[Scope]
protected function active(Builder $query): Builder
{
return $query->where('is_active', true);
}
protected static function booted(): void
{
static::saving(function ($product) {
@ -36,4 +58,11 @@ protected static function booted(): void
}
});
}
protected function casts()
{
return [
'is_active' => 'boolean',
];
}
}

View File

@ -4,6 +4,9 @@
use Illuminate\Database\Eloquent\Model;
/**
* @mixin IdeHelperProductCategory
*/
class ProductCategory extends Model
{
//

View File

@ -5,6 +5,9 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @mixin IdeHelperProductImage
*/
class ProductImage extends Model
{
protected $fillable = [

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
/**
* @mixin IdeHelperStripeSession
*/
class StripeSession extends Model
{
public $fillable = ['session_id', 'order_id'];
public function order(): HasOne
{
return $this->hasOne(Order::class);
}
}

View File

@ -3,15 +3,23 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserRoles;
use App\Enums\User\UserRoles;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
/**
* @mixin IdeHelperUser
*/
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/** @use HasFactory<UserFactory> */
use HasFactory;
use Notifiable;
/**
* The attributes that are mass assignable.
@ -37,6 +45,40 @@ class User extends Authenticatable
'remember_token',
];
public function hasFavorited(Product $product): bool
{
return $this->favoriteProducts()->where('product_id', $product->id)->exists();
}
public function favoriteProducts(): BelongsToMany
{
return $this->belongsToMany(Product::class, 'favorite_products', 'user_id', 'product_id');
}
/**
* @return HasMany<Cart>
*/
public function carts(): HasMany
{
return $this->hasMany(Cart::class);
}
/**
* @return BelongsToMany<Address>
*/
public function addresses(): BelongsToMany
{
return $this->belongsToMany(Address::class);
}
/**
* @return HasMany<Order>
*/
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
/**
* Get the attributes that should be cast.
*

View File

@ -3,6 +3,7 @@
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Stripe\StripeClient;
class AppServiceProvider extends ServiceProvider
{
@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
$this->app->singleton(StripeClient::class, fn () => new StripeClient(config('services.stripe.secret')));
}
/**

View File

@ -0,0 +1,27 @@
<?php
namespace App\Queries;
use App\Models\Product;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
final readonly class GetProductsQuery
{
public function get(?User $user = null): LengthAwarePaginator
{
return Product::query()
->active()
->when($user, function (Builder $query) use ($user) {
$query->withExists(
[
'favoritedBy' => fn (Builder $query) => $query->where('user_id', $user->id),
]
);
})
->with(['category:id,name,slug', 'images:id,path,product_id'])
->paginate();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Services\Payment;
use App\Contracts\PaymentGateway;
use App\Models\Order;
class CodPaymentGateway implements PaymentGateway
{
public function charge(Order $order)
{
// TODO: Implement charge() method
}
}

Some files were not shown because too many files have changed in this diff Show More