diff --git a/src/Http/Controllers/Admin/ProductableAdminApiController.php b/src/Http/Controllers/Admin/ProductableAdminApiController.php index 797f714..4365790 100644 --- a/src/Http/Controllers/Admin/ProductableAdminApiController.php +++ b/src/Http/Controllers/Admin/ProductableAdminApiController.php @@ -4,7 +4,9 @@ use EscolaLms\Cart\Http\Requests\Admin\ProductableAttachRequest; use EscolaLms\Cart\Http\Requests\Admin\ProductableDetachRequest; +use EscolaLms\Cart\Http\Requests\Admin\ProductableProductRequest; use EscolaLms\Cart\Http\Requests\Admin\ProductableRegisteredListRequest; +use EscolaLms\Cart\Http\Resources\ProductResource; use EscolaLms\Cart\Http\Swagger\Admin\ProductableAdminSwagger; use EscolaLms\Cart\Services\Contracts\ProductServiceContract; use EscolaLms\Cart\Services\Contracts\ShopServiceContract; @@ -38,6 +40,16 @@ public function detach(ProductableDetachRequest $request): JsonResponse return $this->sendSuccess(__('Productable detached from user')); } + public function product(ProductableProductRequest $request): JsonResponse + { + $productable = $this->productService->findProductable($request->getProductableType(), $request->getProductableId()); + $product = $this->productService->findSingleProductForProductable($productable); + if ($product) { + return $this->sendResponseForResource(ProductResource::make($product), __('Single Product for Productable found')); + } + return $this->sendError(__('Single Product for this productable does not exist'), 404); + } + public function registered(ProductableRegisteredListRequest $request): JsonResponse { return $this->sendResponse($this->productService->listRegisteredProductableClasses(), __('List of registered Productable types')); diff --git a/src/Http/Requests/Admin/ProductableProductRequest.php b/src/Http/Requests/Admin/ProductableProductRequest.php new file mode 100644 index 0000000..72e1d3c --- /dev/null +++ b/src/Http/Requests/Admin/ProductableProductRequest.php @@ -0,0 +1,34 @@ +user()->can(CartPermissionsEnum::MANAGE_PRODUCTS); + } + + public function rules(): array + { + return [ + 'productable_id' => ['required', new ProductableExistsRule()], + 'productable_type' => ['required', 'string', new ProductableRegisteredRule()], + ]; + } + + public function getProductableId(): int + { + return $this->validated()['productable_id']; + } + + public function getProductableType(): string + { + return $this->validated()['productable_type']; + } +} diff --git a/src/Http/Resources/CartItemResource.php b/src/Http/Resources/CartItemResource.php index a70ca4a..7cae4a2 100644 --- a/src/Http/Resources/CartItemResource.php +++ b/src/Http/Resources/CartItemResource.php @@ -21,7 +21,7 @@ protected function getCartItem(): CartItem return $this->resource; } - public function toArray($request) + public function toArray($request): array { return self::apply([ 'id' => $this->getCartItem()->getKey(), diff --git a/src/Http/Resources/CartResource.php b/src/Http/Resources/CartResource.php index 602e893..f079a46 100644 --- a/src/Http/Resources/CartResource.php +++ b/src/Http/Resources/CartResource.php @@ -5,6 +5,7 @@ use EscolaLms\Auth\Traits\ResourceExtandable; use EscolaLms\Cart\Models\Cart; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\Json\ResourceCollection; class CartResource extends JsonResource { @@ -23,13 +24,18 @@ protected function getCart(): Cart return $this->resource; } - public function toArray($request) + protected function getCartItemsResourceCollection(): ResourceCollection + { + return CartItemResource::collection($this->getCart()->items); + } + + public function toArray($request): array { return self::apply([ 'total' => $this->getCart()->total, 'subtotal' => $this->getCart()->subtotal, 'tax' => $this->getCart()->getTaxAttribute($this->taxRate), - 'items' => CartItemResource::collection($this->getCart()->items) + 'items' => $this->getCartItemsResourceCollection() ], $this); } } diff --git a/src/Http/Resources/OrderItemResource.php b/src/Http/Resources/OrderItemResource.php new file mode 100644 index 0000000..f7a729d --- /dev/null +++ b/src/Http/Resources/OrderItemResource.php @@ -0,0 +1,39 @@ +resource; + } + + public function toArray($request): array + { + return self::apply([ + 'price' => $this->getOrderItem()->price, + 'quantity' => $this->getOrderItem()->quantity, + 'subtotal' => $this->getOrderItem()->subtotal, + 'tax' => $this->getOrderItem()->tax, + 'total' => $this->getOrderItem()->total, + 'total_with_tax' => $this->getOrderItem()->total_with_tax, + 'order_id' => $this->getOrderItem()->order_id, + 'product_id' => $this->getOrderItem()->buyable_id, + 'product_type' => $this->getOrderItem()->buyable_type, + $this->mergeWhen($this->getOrderItem()->buyable instanceof Product, fn () => ['product' => ProductResource::make($this->getOrderItem()->buyable)]), + ], $this); + } +} diff --git a/src/Http/Resources/OrderResource.php b/src/Http/Resources/OrderResource.php index 901d8ba..894172c 100644 --- a/src/Http/Resources/OrderResource.php +++ b/src/Http/Resources/OrderResource.php @@ -2,12 +2,16 @@ namespace EscolaLms\Cart\Http\Resources; +use EscolaLms\Auth\Traits\ResourceExtandable; use EscolaLms\Cart\Enums\OrderStatus; use EscolaLms\Cart\Models\Order; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Http\Resources\Json\ResourceCollection; class OrderResource extends JsonResource { + use ResourceExtandable; + public function __construct(Order $order) { parent::__construct($order); @@ -18,18 +22,17 @@ protected function getOrder(): Order return $this->resource; } - /** - * Transform the resource into an array. - * - * @param \Illuminate\Http\Request $request - * @return array - */ - public function toArray($request) + protected function getOrderItemsResourceCollection(): ResourceCollection + { + return OrderItemResource::collection($this->getOrder()->items); + } + + public function toArray($request): array { - return [ + return self::apply([ 'id' => $this->resource->getKey(), 'status' => OrderStatus::getName($this->status), - 'items' => $this->items->toArray(), + 'items' => $this->getOrderItemsResourceCollection(), 'total' => $this->total, 'subtotal' => $this->subtotal, 'tax' => $this->tax, @@ -42,6 +45,6 @@ public function toArray($request) 'client_country' => $this->client_country, 'client_company' => $this->client_company, 'client_taxid' => $this->client_taxid, - ]; + ], $this); } } diff --git a/src/Http/Resources/ProductResource.php b/src/Http/Resources/ProductResource.php index 1256425..e3a90fe 100644 --- a/src/Http/Resources/ProductResource.php +++ b/src/Http/Resources/ProductResource.php @@ -21,7 +21,7 @@ protected function getProduct(): Product return $this->resource; } - public function toArray($request) + public function toArray($request): array { $user = $request ? $request->user() : Auth::user(); return [ diff --git a/src/Http/Resources/ProductableGenericResource.php b/src/Http/Resources/ProductableGenericResource.php index bce7fc8..c518526 100644 --- a/src/Http/Resources/ProductableGenericResource.php +++ b/src/Http/Resources/ProductableGenericResource.php @@ -19,7 +19,7 @@ public function getProductable(): Productable return $this->resource; } - public function toArray($request) + public function toArray($request): array { return [ 'id' => $this->getProductable()->getKey(), diff --git a/src/Http/Swagger/Admin/ProductableAdminSwagger.php b/src/Http/Swagger/Admin/ProductableAdminSwagger.php index 2a1eca8..0c4c3cd 100644 --- a/src/Http/Swagger/Admin/ProductableAdminSwagger.php +++ b/src/Http/Swagger/Admin/ProductableAdminSwagger.php @@ -4,6 +4,7 @@ use EscolaLms\Cart\Http\Requests\Admin\ProductableAttachRequest; use EscolaLms\Cart\Http\Requests\Admin\ProductableDetachRequest; +use EscolaLms\Cart\Http\Requests\Admin\ProductableProductRequest; use EscolaLms\Cart\Http\Requests\Admin\ProductableRegisteredListRequest; use Illuminate\Http\JsonResponse; @@ -167,4 +168,64 @@ public function detach(ProductableDetachRequest $request): JsonResponse; * ) */ public function registered(ProductableRegisteredListRequest $request): JsonResponse; + + /** + * @OA\Get( + * path="/api/admin/productables/product", + * description="Get single product for this productable (if it exists)", + * tags={"Admin Product"}, + * security={ + * {"passport": {}}, + * }, + * @OA\Parameter( + * name="productable_type", + * description="Productable class", + * required=true, + * in="query", + * @OA\Schema( + * type="string", + * ), + * ), + * @OA\Parameter( + * name="productable_id", + * description="Productable id", + * required=true, + * in="query", + * @OA\Schema( + * type="integer", + * ), + * ), + * @OA\Response( + * response=200, + * description="successful operation", + * @OA\MediaType( + * mediaType="application/json", + * ), + * @OA\Schema( + * type="object", + * @OA\Property( + * property="data", + * type="object", + * @OA\Schema(ref="#/components/schemas/Product") + * ), + * @OA\Property( + * property="success", + * type="boolean" + * ), + * @OA\Property( + * property="message", + * type="string" + * ) + * ) + * ), + * @OA\Response( + * response=422, + * description="Bad request", + * @OA\MediaType( + * mediaType="application/json" + * ) + * ) + * ) + */ + public function product(ProductableProductRequest $request): JsonResponse; } diff --git a/src/Models/Cart.php b/src/Models/Cart.php index 90b976f..b986e01 100644 --- a/src/Models/Cart.php +++ b/src/Models/Cart.php @@ -15,6 +15,7 @@ * @property int|null $user_id * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read CartManager $cart_manager * @property-read int $subtotal * @property-read int $tax * @property-read int $total @@ -33,8 +34,6 @@ */ class Cart extends BaseCart { - private ?CartManager $cartManager = null; - public function user(): BelongsTo { return $this->belongsTo(User::class); @@ -45,24 +44,24 @@ public function items(): HasMany return $this->hasMany(CartItem::class); } - public function getCartManager(): CartManager + public function getCartManagerAttribute(): CartManager { - return $this->cartManager ?? ($this->cartManager = app(ShopServiceContract::class)->cartManagerForCart($this)); + return app(ShopServiceContract::class)->cartManagerForCart($this); } public function getSubtotalAttribute(): int { - return $this->getCartManager()->subtotalInt(); + return $this->cartManager->subtotalInt(); } public function getTotalAttribute(): int { - return $this->getCartManager()->total(); + return $this->cartManager->total(); } public function getTaxAttribute(?int $rate = null): int { - return $this->getCartManager()->taxInt($rate); + return $this->cartManager->taxInt($rate); } public function getTotalWithTaxAttribute(?int $rate = null): int diff --git a/src/Models/CartItem.php b/src/Models/CartItem.php index ede1600..1c210b9 100644 --- a/src/Models/CartItem.php +++ b/src/Models/CartItem.php @@ -3,6 +3,7 @@ namespace EscolaLms\Cart\Models; use EscolaLms\Cart\Models\Contracts\Base\Taxable; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\Config; use Treestoneit\ShoppingCart\Models\CartItem as BaseCartItem; @@ -18,6 +19,7 @@ * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @property-read \Illuminate\Database\Eloquent\Model|\Eloquent $buyable + * @property-read \EscolaLms\Cart\Models\Cart $cart * @property-read mixed $description * @property-read float|int $extra_fees * @property-read string $identifier @@ -44,6 +46,11 @@ */ class CartItem extends BaseCartItem { + public function cart(): BelongsTo + { + return $this->belongsTo(Cart::class); + } + public function getTaxRateAttribute(?int $rate = null): int { if (!$rate && Config::get('shopping-cart.tax.mode') == 'flat') { diff --git a/src/Models/OrderItem.php b/src/Models/OrderItem.php index 7f0e3fd..855ff85 100644 --- a/src/Models/OrderItem.php +++ b/src/Models/OrderItem.php @@ -5,6 +5,8 @@ use EscolaLms\Cart\QueryBuilders\OrderItemModelQueryBuilder; use EscolaLms\Cart\Support\OrderItemCollection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphTo; /** * EscolaLms\Cart\Models\OrderItem @@ -57,12 +59,12 @@ class OrderItem extends Model protected $casts = ['options' => 'array']; - public function buyable() + public function buyable(): MorphTo { return $this->morphTo('buyable'); } - public function order() + public function order(): BelongsTo { return $this->belongsTo(Order::class); } diff --git a/src/Services/CartManager.php b/src/Services/CartManager.php index be14ce9..d3159c6 100644 --- a/src/Services/CartManager.php +++ b/src/Services/CartManager.php @@ -2,7 +2,6 @@ namespace EscolaLms\Cart\Services; -use EscolaLms\Cart\Facades\Shop; use EscolaLms\Cart\Models\Cart; use EscolaLms\Cart\Models\CartItem; use EscolaLms\Cart\Models\Contracts\Base\Buyable; @@ -10,6 +9,7 @@ use EscolaLms\Cart\Services\Contracts\CartManagerContract; use Illuminate\Database\Eloquent\Model; use Treestoneit\ShoppingCart\CartManager as BaseCartManager; +use Treestoneit\ShoppingCart\Models\Cart as BaseCart; class CartManager extends BaseCartManager implements CartManagerContract { @@ -18,10 +18,16 @@ class CartManager extends BaseCartManager implements CartManagerContract public function __construct(Cart $cart) { parent::__construct($cart); + } + + public function refreshCart(?BaseCart $cart = null): self + { + parent::refreshCart($cart); $this->removeNonexistingBuyables(); + return $this; } - private function removeNonexistingBuyables(): void + protected function removeNonexistingBuyables(): void { /** @var CartItem $item */ foreach ($this->content() as $item) { @@ -52,7 +58,7 @@ public function taxInt(?int $rate = null): int } /** - * CartItem total = subtotal + additional fees independen from quantity; + * CartItem total = subtotal + additional fees independent from quantity; * Tax is NOT included in this, to get total with tax use `totalWithTax()` method */ public function total(): int diff --git a/src/Services/Contracts/OrderServiceContract.php b/src/Services/Contracts/OrderServiceContract.php index 76c8795..e546e3d 100644 --- a/src/Services/Contracts/OrderServiceContract.php +++ b/src/Services/Contracts/OrderServiceContract.php @@ -6,6 +6,7 @@ use EscolaLms\Cart\Dtos\OrdersSearchDto; use EscolaLms\Cart\Models\Cart; use EscolaLms\Cart\Models\Order; +use EscolaLms\Cart\Services\CartManager; use EscolaLms\Core\Dtos\OrderDto; use Illuminate\Database\Eloquent\Model; use Illuminate\Pagination\LengthAwarePaginator; @@ -17,6 +18,7 @@ public function searchAndPaginateOrders(OrdersSearchDto $searchDto, ?OrderDto $s public function find(int $id): Model; public function createOrderFromCart(Cart $cart, ?ClientDetailsDto $clientDetailsDto = null): Order; + public function createOrderFromCartManager(CartManager $cart, ?ClientDetailsDto $clientDetailsDto = null): Order; public function setPaid(Order $order): void; public function setCancelled(Order $order): void; diff --git a/src/Services/OrderService.php b/src/Services/OrderService.php index dd7aba6..eaa7a88 100644 --- a/src/Services/OrderService.php +++ b/src/Services/OrderService.php @@ -78,16 +78,21 @@ public function find($id): Model } public function createOrderFromCart(Cart $cart, ?ClientDetailsDto $clientDetailsDto = null): Order + { + return $this->createOrderFromCartManager(new CartManager($cart), $clientDetailsDto); + } + + public function createOrderFromCartManager(CartManager $cartManager, ?ClientDetailsDto $clientDetailsDto = null): Order { $optionalClientDetailsDto = optional($clientDetailsDto); + $cart = $cartManager->getModel(); + /** @var User $user */ $user = User::find($cart->user_id); $user->orders()->where('status', OrderStatus::PROCESSING)->update(['status' => OrderStatus::CANCELLED]); - $cartManager = new CartManager($cart); - $order = new Order($cart->getAttributes()); $order->total = $cartManager->totalWithTax(); $order->subtotal = $cartManager->total(); diff --git a/src/Services/ShopService.php b/src/Services/ShopService.php index 2847910..f1fdd68 100644 --- a/src/Services/ShopService.php +++ b/src/Services/ShopService.php @@ -43,7 +43,9 @@ public function cartManagerForCart(Cart $cart): CartManager public function purchaseCart(Cart $cart, ?ClientDetailsDto $clientDetailsDto = null, ?PaymentMethodContract $paymentMethod = null): void { - $order = $this->orderService->createOrderFromCart($cart, $clientDetailsDto); + $cartManager = $this->cartManagerForCart($cart); + + $order = $this->orderService->createOrderFromCartManager($cartManager, $clientDetailsDto); $paymentProcessor = $order->process(); $paymentProcessor->purchase($paymentMethod); @@ -55,7 +57,7 @@ public function purchaseCart(Cart $cart, ?ClientDetailsDto $clientDetailsDto = n $this->orderService->setCancelled($order); } - $this->cartManagerForCart($cart)->destroy(); + $cartManager->destroy(); } public function cartAsJsonResource(Cart $cart, ?int $taxRate = null): JsonResource diff --git a/src/routes.php b/src/routes.php index 3948230..ca39d3f 100644 --- a/src/routes.php +++ b/src/routes.php @@ -22,6 +22,7 @@ Route::post('/products/{id}/detach', [ProductAdminApiController::class, 'detach'])->whereNumber('id'); Route::get('/productables/registered', [ProductableAdminApiController::class, 'registered']); + Route::get('/productables/product', [ProductableAdminApiController::class, 'product']); Route::post('/productables/attach', [ProductableAdminApiController::class, 'attach']); Route::post('/productables/detach', [ProductableAdminApiController::class, 'detach']); }); diff --git a/tests/API/AdminApiTest.php b/tests/API/AdminApiTest.php index 29b68b5..573477a 100644 --- a/tests/API/AdminApiTest.php +++ b/tests/API/AdminApiTest.php @@ -304,4 +304,24 @@ public function test_get_registered_productables_list() ] ]); } + + public function test_find_single_product_for_productable() + { + /** @var Product $product */ + $product = Product::factory()->create(); + $productable = ExampleProductable::factory()->create(); + $product->productables()->save(new ProductProductable([ + 'productable_type' => ExampleProductable::class, + 'productable_id' => $productable->getKey() + ])); + + $this->response = $this->actingAs($this->user, 'api')->json('GET', '/api/admin/productables/product', [ + 'productable_type' => ExampleProductable::class, + 'productable_id' => $productable->getKey(), + ]); + $this->response->assertOk(); + $this->response->assertJsonFragment([ + 'data' => ProductResource::make($product->refresh())->toArray(null) + ]); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 98ee015..c23ef66 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,6 +8,7 @@ use EscolaLms\Cart\Models\User; use EscolaLms\Cart\Providers\AuthServiceProvider; use EscolaLms\Cart\Tests\Mocks\ExampleProductableMigration; +use EscolaLms\Categories\EscolaLmsCategoriesServiceProvider; use EscolaLms\Payments\Providers\PaymentsServiceProvider; use EscolaLms\Tags\EscolaLmsTagsServiceProvider; use Laravel\Passport\Passport; @@ -30,7 +31,7 @@ protected function getPackageProviders($app) PermissionServiceProvider::class, PassportServiceProvider::class, AuthServiceProvider::class, - EscolaLmsCartServiceProvider::class, + EscolaLmsCategoriesServiceProvider::class, EscolaLmsTagsServiceProvider::class, PaymentsServiceProvider::class, EscolaLmsCartServiceProvider::class,