chengwl пре 1 недеља
родитељ
комит
02a2abb0ae

+ 33 - 0
packages/Webkul/BagistoApi/src/Dto/SaveCheckoutCartInput.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\SerializedName;
+
+/**
+ * SaveCheckoutCartInput - GraphQL Input DTO for saving checkout selections.
+ *
+ * All fields are optional. Any field whose value equals the sentinel "-1" is
+ * left untouched, so the frontend can selectively update only the fields it
+ * cares about. Authentication token is passed via Authorization: Bearer header,
+ * NOT as an input parameter.
+ */
+class SaveCheckoutCartInput
+{
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping method code. Pass "-1" to leave unchanged.')]
+    #[SerializedName('shippingMethod')]
+    public ?string $shippingMethod = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Coupon code. Pass "-1" to leave unchanged, empty string to remove the applied coupon.')]
+    #[SerializedName('couponCode')]
+    public ?string $couponCode = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Payment method code. Pass "-1" to leave unchanged.')]
+    #[SerializedName('code')]
+    public ?string $code = null;
+}

+ 40 - 0
packages/Webkul/BagistoApi/src/Models/SaveCheckoutCart.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Webkul\BagistoApi\Models;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GraphQl\Mutation;
+use Webkul\BagistoApi\Dto\CartData;
+use Webkul\BagistoApi\Dto\SaveCheckoutCartInput;
+use Webkul\BagistoApi\State\SaveCheckoutCartProcessor;
+
+/**
+ * SaveCheckoutCart - GraphQL API Resource for persisting checkout selections.
+ *
+ * Single mutation that saves shipping method, coupon code and payment method in
+ * one call and returns the full cart. Any input field equal to "-1" is left
+ * untouched. The cart is resolved from the Authorization: Bearer token.
+ */
+#[ApiResource(
+    routePrefix: '/api/shop',
+    shortName: 'SaveCheckoutCart',
+    uriTemplate: '/save-checkout-carts',
+    operations: [],
+    graphQlOperations: [
+        new Mutation(
+            name: 'create',
+            input: SaveCheckoutCartInput::class,
+            output: CartData::class,
+            processor: SaveCheckoutCartProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+                'groups'                 => ['mutation'],
+            ],
+            normalizationContext: [
+                'groups'                 => ['mutation'],
+            ],
+            description: 'Save shipping method, coupon code and payment method for the cart and return the full cart. Pass "-1" for any field to leave it unchanged.',
+        ),
+    ]
+)]
+class SaveCheckoutCart {}

+ 163 - 0
packages/Webkul/BagistoApi/src/State/SaveCheckoutCartProcessor.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use Illuminate\Support\Facades\Request;
+use Webkul\BagistoApi\Dto\CartData;
+use Webkul\BagistoApi\Dto\SaveCheckoutCartInput;
+use Webkul\BagistoApi\Exception\AuthenticationException;
+use Webkul\BagistoApi\Exception\OperationFailedException;
+use Webkul\BagistoApi\Exception\ResourceNotFoundException;
+use Webkul\BagistoApi\Facades\CartTokenFacade;
+use Webkul\BagistoApi\Facades\TokenHeaderFacade;
+use Webkul\CartRule\Repositories\CartRuleCouponRepository;
+use Webkul\Checkout\Facades\Cart;
+use Webkul\Shipping\Facades\Shipping;
+
+/**
+ * GraphQL processor for the SaveCheckoutCart mutation.
+ *
+ * Resolves the cart from the Authorization Bearer token, persists the supplied
+ * shipping method / coupon code / payment method (skipping any field equal to
+ * the "-1" sentinel) and returns the full cart as CartData.
+ */
+class SaveCheckoutCartProcessor implements ProcessorInterface
+{
+    /**
+     * Sentinel: a field equal to this value is left unchanged.
+     */
+    private const SKIP = '-1';
+
+    public function __construct(
+        protected CartRuleCouponRepository $cartRuleCouponRepository,
+    ) {}
+
+    /**
+     * Process the SaveCheckoutCart mutation.
+     */
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
+    {
+        if (! $data instanceof SaveCheckoutCartInput) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-input'));
+        }
+
+        $request = Request::instance() ?? ($context['request'] ?? null);
+        $token = $request ? TokenHeaderFacade::getAuthorizationBearerToken($request) : null;
+
+        if (! $token) {
+            throw new AuthenticationException(__('bagistoapi::app.graphql.cart.authentication-required'));
+        }
+
+        $cart = CartTokenFacade::getCartByToken($token);
+
+        if (! $cart) {
+            throw new ResourceNotFoundException(__('bagistoapi::app.graphql.cart.invalid-token'));
+        }
+
+        Cart::setCart($cart);
+
+        try {
+            if ($this->shouldUpdate($data->shippingMethod)) {
+                $this->applyShippingMethod((string) $data->shippingMethod);
+            }
+
+            if ($this->shouldUpdate($data->code)) {
+                $this->applyPaymentMethod((string) $data->code);
+            }
+
+            if ($this->shouldUpdate($data->couponCode)) {
+                $this->applyCoupon((string) $data->couponCode);
+            }
+
+            Cart::collectTotals();
+        } catch (OperationFailedException $e) {
+            throw $e;
+        } catch (\Exception $e) {
+            throw new OperationFailedException($e->getMessage(), 0, $e);
+        }
+
+        return CartData::fromModel(Cart::getCart());
+    }
+
+    /**
+     * A value is updatable when it is present and not the skip sentinel.
+     */
+    private function shouldUpdate(?string $value): bool
+    {
+        return $value !== null && $value !== self::SKIP;
+    }
+
+    /**
+     * Validate and persist the shipping method.
+     */
+    private function applyShippingMethod(string $code): void
+    {
+        if (! Cart::getCart()?->haveStockableItems()) {
+            return;
+        }
+
+        Shipping::collectRates();
+
+        if (! Shipping::isMethodCodeExists($code)) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-shipping-method'));
+        }
+
+        if (! Cart::saveShippingMethod($code)) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.shipping-method-save-failed'));
+        }
+
+        Cart::collectTotals();
+    }
+
+    /**
+     * Validate and persist the payment method.
+     */
+    private function applyPaymentMethod(string $code): void
+    {
+        $paymentMethodConfig = config('payment_methods.'.$code);
+
+        if (! $paymentMethodConfig || ! isset($paymentMethodConfig['class'])) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-payment-method'));
+        }
+
+        if (! Cart::savePaymentMethod(['method' => $code])) {
+            throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.payment-method-save-failed'));
+        }
+    }
+
+    /**
+     * Apply or remove a coupon. An empty string removes the active coupon.
+     */
+    private function applyCoupon(string $code): void
+    {
+        $code = trim($code);
+
+        if ($code === '') {
+            Cart::removeCouponCode()->collectTotals();
+
+            return;
+        }
+
+        if (Cart::getCart()?->coupon_code === $code) {
+            return;
+        }
+
+        $coupon = $this->cartRuleCouponRepository->findOneByField('code', $code);
+
+        if (! $coupon) {
+            throw new OperationFailedException(trans('shop::app.checkout.coupon.invalid'));
+        }
+
+        if (! $coupon->cart_rule->status) {
+            throw new OperationFailedException(trans('shop::app.checkout.coupon.error'));
+        }
+
+        Cart::setCouponCode($coupon->code)->collectTotals();
+
+        if (Cart::getCart()?->coupon_code !== $coupon->code) {
+            throw new OperationFailedException(trans('shop::app.checkout.coupon.error'));
+        }
+    }
+}