Просмотр исходного кода

Merge branch 'variant' of http://gogs.hnwmzp.cn/chengwenliang/nshop into dev

chengwl 4 дней назад
Родитель
Сommit
d61fbc14b4

+ 1 - 1
packages/Webkul/Admin/src/Http/Requests/ProductForm.php

@@ -73,7 +73,7 @@ class ProductForm extends FormRequest
         $this->product = $this->productRepository->find($this->id);
 
         $this->rules = array_merge($this->product->getTypeInstance()->getTypeValidationRules(), [
-            'sku'                  => ['required', 'unique:products,sku,'.$this->id, new Slug],
+            'sku'                  => ['required', 'unique:products,sku,'.$this->id],
             'url_key'              => ['required', new ProductCategoryUniqueSlug('products', $this->id)],
             'images.files.*'       => ['nullable', 'mimes:bmp,jpeg,jpg,png,webp,gif'],
             'images.positions.*'   => ['nullable', 'integer'],

+ 6 - 0
packages/Webkul/BagistoApi/src/Dto/PaymentReplayInput.php

@@ -20,6 +20,12 @@ class PaymentReplayInput
     #[SerializedName('orderId')]
     public ?int $orderId = null;
 
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Payment method code for replay')]
+    #[SerializedName('paymentMethod')]
+    public ?string $paymentMethod = null;
+
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Optional override of payment success URL')]
     #[SerializedName('paymentSuccessUrl')]

+ 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('paymentMethod')]
+    public ?string $paymentMethod = null;
+}

+ 2 - 2
packages/Webkul/BagistoApi/src/Jobs/ReconcilePendingPaymentJob.php

@@ -52,7 +52,7 @@ class ReconcilePendingPaymentJob implements ShouldQueue
         if (! $order) {
             return;
         }
-
+     
         if (! in_array($order->status, [Order::STATUS_PENDING, Order::STATUS_PENDING_PAYMENT], true)) {
             return;
         }
@@ -113,7 +113,7 @@ class ReconcilePendingPaymentJob implements ShouldQueue
                     return false;
                 }
 
-                return (bool) $orderRepository->cancel($lockedOrder);
+                return (bool) $orderRepository->cancel($lockedOrder,true);
             });
 
             if ($cancelled) {

+ 1 - 1
packages/Webkul/BagistoApi/src/Models/Country.php

@@ -21,7 +21,7 @@ use Webkul\Core\Models\Country as BaseCountry;
     ],
     graphQlOperations: [
         new Query(resolver: BaseQueryItemResolver::class),
-        new QueryCollection(provider: CursorAwareCollectionProvider::class, paginationEnabled: false),
+        new QueryCollection( paginationEnabled: false),
     ]
 )]
 class Country extends BaseCountry

+ 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 {}

+ 8 - 0
packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php

@@ -38,6 +38,7 @@ use Webkul\BagistoApi\Services\TokenHeaderService;
 use Webkul\BagistoApi\State\PaymentCallbackProcessor;
 use Webkul\BagistoApi\State\PaymentInitiateProcessor;
 use Webkul\BagistoApi\State\PaymentReplayProcessor;
+use Webkul\BagistoApi\State\SaveCheckoutCartProcessor;
 use Webkul\BagistoApi\State\BookingSlotProvider;
 use Webkul\BagistoApi\State\PageProvider;
 use Webkul\BagistoApi\State\AttributeCollectionProvider;
@@ -172,6 +173,7 @@ class BagistoApiServiceProvider extends ServiceProvider
         $this->app->tag(CancelOrderProcessor::class, ProcessorInterface::class);
         $this->app->tag(ReorderProcessor::class, ProcessorInterface::class);
         $this->app->tag(ContactUsProcessor::class, ProcessorInterface::class);
+        $this->app->tag(SaveCheckoutCartProcessor::class, ProcessorInterface::class);
 
         $this->app->tag(TokenHeaderDenormalizer::class, 'serializer.normalizer');
 
@@ -218,6 +220,12 @@ class BagistoApiServiceProvider extends ServiceProvider
             );
         });
 
+        $this->app->singleton(SaveCheckoutCartProcessor::class, function ($app) {
+            return new SaveCheckoutCartProcessor(
+                $app->make('Webkul\CartRule\Repositories\CartRuleCouponRepository'),
+            );
+        });
+
         $this->app->singleton(CheckoutProcessor::class, function ($app) {
             return new CheckoutProcessor(
                 $app->make('Webkul\Customer\Repositories\CustomerRepository'),

+ 38 - 1
packages/Webkul/BagistoApi/src/Services/PaymentService.php

@@ -79,10 +79,27 @@ class PaymentService
         if ($express) {
             $this->validateExpressInitiation($cart, $input);
         } else {
+            if(!$cart->shipping_method){
+                
+                if ($input->shippingMethod) {
+
+                    if (! \Webkul\Shipping\Facades\Shipping::isMethodCodeExists($input->shippingMethod)) {
+                        throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-shipping-method'));
+                    }
+                    \Webkul\Shipping\Facades\Shipping::collectRates();
+
+                    if (! Cart::saveShippingMethod($input->shippingMethod)) {
+                        throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.shipping-method-save-failed'));
+                    }
+                }
+            }
             $this->validateStandardInitiation($cart, $input);
             Cart::collectTotals();
+            $cart = Cart::getCart();
         }
 
+      
+
         if (! $input->paymentMethod && ! $cart->payment?->method) {
             throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.payment-method-required'));
         }
@@ -91,7 +108,7 @@ class PaymentService
             if (! Cart::savePaymentMethod(['method' => $input->paymentMethod])) {
                 throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.payment-method-save-failed'));
             }
-            Cart::collectTotals();
+            // Cart::collectTotals();
             $cart = Cart::getCart();
         }
 
@@ -218,6 +235,26 @@ class PaymentService
             throw new OperationFailedException(__('bagistoapi::app.graphql.payment.not-pending', ['status' => $order->status]));
         }
 
+        if ($input->paymentMethod) {
+            if(!$order->payment->method!=$input->paymentMethod){
+
+                $paymentMethodConfig = config('payment_methods.'.$input->paymentMethod);
+                if (! $paymentMethodConfig || ! isset($paymentMethodConfig['class'])) {
+                    throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-payment-method'));
+                }
+    
+                if (! $order->payment) {
+                    throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.payment-method-required'));
+                }
+                $order->payment->method = $input->paymentMethod;
+                $order->payment->method_title = core()->getConfigData('sales.payment_methods.'.$input->paymentMethod.'.title');
+                $order->payment->save();
+                $order->refresh();
+
+            }
+           
+        }
+
         $gatewayOrderId = $this->createGatewayOrderForOrder($order);
 
         $this->writeGatewayOrderId($order, $gatewayOrderId);

+ 1 - 1
packages/Webkul/BagistoApi/src/State/CancelOrderProcessor.php

@@ -82,7 +82,7 @@ class CancelOrderProcessor implements ProcessorInterface
             );
         }
 
-        $result = $this->orderRepository->cancel($order);
+        $result = $this->orderRepository->cancel($order,true);
 
         if ($result && ! empty($strategy['reactivate_cart'])) {
             CartTokenFacade::reactivateCart((int) $strategy['reactivate_cart']);

+ 15 - 0
packages/Webkul/BagistoApi/src/State/CartTokenProcessor.php

@@ -907,9 +907,24 @@ class CartTokenProcessor implements ProcessorInterface
             \Webkul\Shipping\Facades\Shipping::collectRates();
         }
 
+        $cartId = $cart->id;
+
         CartFacade::collectTotals();
 
         $cart = CartFacade::getCart();
+
+        /**
+         * When cart has no items, some flows may leave facade cart as null
+         * after totals collection. Fallback to DB lookup by original cart id.
+         */
+        if (! $cart && $cartId) {
+            $cart = CartModel::find($cartId);
+        }
+
+        if (! $cart) {
+            throw new ResourceNotFoundException(__('bagistoapi::app.graphql.cart.cart-not-found'));
+        }
+
         $cart->load('items.product');
 
         $cartData = CartData::fromModel($cart);

+ 99 - 13
packages/Webkul/BagistoApi/src/State/CustomerOrderProvider.php

@@ -9,8 +9,11 @@ use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
 use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Request;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
+use Webkul\BagistoApi\Facades\CartTokenFacade;
+use Webkul\BagistoApi\Facades\TokenHeaderFacade;
 use Webkul\BagistoApi\Models\CustomerOrder;
 use Webkul\Customer\Models\Customer;
 
@@ -31,15 +34,23 @@ class CustomerOrderProvider implements ProviderInterface
      */
     public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
     {
+        $isCollection = $operation instanceof GetCollection
+            || $operation instanceof \ApiPlatform\Metadata\GraphQl\QueryCollection;
+
         $customer = Auth::guard('sanctum')->user();
 
-        if (! $customer) {
+        // Collection API remains customer-only.
+        if ($isCollection && ! $customer) {
             throw new AuthorizationException(__('bagistoapi::app.graphql.logout.unauthenticated'));
         }
 
         /** Single item — GET /api/shop/customer-orders/{id} */
-        if (! $operation instanceof GetCollection && ! ($operation instanceof \ApiPlatform\Metadata\GraphQl\QueryCollection)) {
-            return $this->provideItem($customer, $uriVariables);
+        if (! $isCollection) {
+            if ($customer) {
+                return $this->provideCustomerItem($customer, $uriVariables);
+            }
+
+            return $this->provideGuestItem($uriVariables);
         }
 
         return $this->provideCollection($customer, $context);
@@ -48,21 +59,39 @@ class CustomerOrderProvider implements ProviderInterface
     /**
      * Return a single order owned by the customer
      */
-    private function provideItem(object $customer, array $uriVariables): CustomerOrder
+    private function provideCustomerItem(object $customer, array $uriVariables): CustomerOrder
     {
-        $id = $uriVariables['id'] ?? null;
+        $order = $this->baseOrderDetailsQuery()
+            ->where('customer_id', $customer->id)
+            ->where('customer_type', Customer::class)
+            ->find($this->resolveOrderId($uriVariables));
 
-        if (! $id) {
-            throw new ResourceNotFoundException(__('bagistoapi::app.graphql.customer-order.id-required'));
+        if (! $order) {
+            throw new ResourceNotFoundException(
+                __('bagistoapi::app.graphql.customer-order.not-found', ['id' => $uriVariables['id'] ?? null])
+            );
         }
 
-        $orderQuery = CustomerOrder::with(['items', 'addresses', 'payment', 'shipments.items', 'shipments.shippingAddress'])
-            ->where('customer_id', $customer->id)
-            ->where('customer_type', Customer::class);
+        return $order;
+    }
 
-        $order = $orderQuery->find($id);
+    /**
+     * Return a single order for a guest token owner.
+     */
+    private function provideGuestItem(array $uriVariables): CustomerOrder
+    {
+        $id = $this->resolveOrderId($uriVariables);
 
-        if (! $order) {
+        $request = Request::instance();
+        $token = $request ? TokenHeaderFacade::getAuthorizationBearerToken($request) : null;
+
+        if (! $token || CartTokenFacade::getTokenType($token) !== 'guest') {
+            throw new AuthorizationException(__('bagistoapi::app.graphql.logout.unauthenticated'));
+        }
+
+        $order = $this->baseOrderDetailsQuery()->find($id);
+
+        if (! $order || ! $this->guestTokenOwnsOrder($token, $order)) {
             throw new ResourceNotFoundException(
                 __('bagistoapi::app.graphql.customer-order.not-found', ['id' => $id])
             );
@@ -71,6 +100,56 @@ class CustomerOrderProvider implements ProviderInterface
         return $order;
     }
 
+    /**
+     * Shared order detail relations for single-order APIs.
+     */
+    private function baseOrderDetailsQuery()
+    {
+        return CustomerOrder::with([
+            'items.product.images',
+            'items.children.product.images',
+            'addresses',
+            'payment',
+            'shipments.items',
+            'shipments.shippingAddress',
+        ]);
+    }
+
+    private function resolveOrderId(array $uriVariables): int
+    {
+        $id = $uriVariables['id'] ?? null;
+
+        if (! $id || ! is_numeric($id)) {
+            throw new ResourceNotFoundException(__('bagistoapi::app.graphql.customer-order.id-required'));
+        }
+
+        return (int) $id;
+    }
+
+    /**
+     * Same ownership check used by CancelOrder: guest can access only orders
+     * paid with their own cart token.
+     */
+    private function guestTokenOwnsOrder(string $token, CustomerOrder $order): bool
+    {
+        $additional = $order->payment?->additional ?? [];
+        $expectedToken = $additional['cart_token'] ?? null;
+
+        if ($expectedToken && hash_equals((string) $expectedToken, $token)) {
+            return true;
+        }
+
+        $tokenRecord = CartTokenFacade::getGuestTokenRecord($token);
+
+        if (! $tokenRecord) {
+            return false;
+        }
+
+        $expectedCartId = $additional['cart_id'] ?? null;
+
+        return $expectedCartId && (int) $tokenRecord->cart_id === (int) $expectedCartId;
+    }
+
     /**
      * Enable debug dumps only when explicitly requested via header:
      * X-DEBUG-CUSTOMER-ORDER: 1
@@ -113,7 +192,14 @@ class CustomerOrderProvider implements ProviderInterface
         $args = $context['args'] ?? [];
         $filters = $context['filters'] ?? [];
 
-        $query = CustomerOrder::with(['items', 'addresses', 'payment', 'shipments.items', 'shipments.shippingAddress'])
+        $query = CustomerOrder::with([
+            'items.product.images',
+            'items.children.product.images',
+            'addresses',
+            'payment',
+            'shipments.items',
+            'shipments.shippingAddress',
+        ])
             ->where('customer_id', $customer->id)
             ->where('customer_type', Customer::class);
 

+ 1 - 0
packages/Webkul/BagistoApi/src/State/PaymentCallbackProcessor.php

@@ -30,6 +30,7 @@ class PaymentCallbackProcessor implements ProcessorInterface
         $order = $result['order'];
 
         return (object) [
+            'id'            => (int) $order->id,
             'success'       => $result['status'] === 'success',
             'message'       => $result['message'],
             'orderId'       => (string) $order->id,

+ 1 - 0
packages/Webkul/BagistoApi/src/State/PaymentInitiateProcessor.php

@@ -50,6 +50,7 @@ class PaymentInitiateProcessor implements ProcessorInterface
         $order = $result['order'];
 
         return (object) [
+            'id'             => (int) $order->id,
             'success'        => true,
             'message'        => __('bagistoapi::app.graphql.payment.initiated'),
             'orderId'        => (string) $order->id,

+ 1 - 0
packages/Webkul/BagistoApi/src/State/PaymentReplayProcessor.php

@@ -38,6 +38,7 @@ class PaymentReplayProcessor implements ProcessorInterface
         $order = $result['order'];
 
         return (object) [
+            'id'             => (int) $order->id,
             'success'        => true,
             'message'        => __('bagistoapi::app.graphql.payment.replayed'),
             'orderId'        => (string) $order->id,

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

@@ -0,0 +1,164 @@
+<?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->paymentMethod)) {
+                $this->applyPaymentMethod((string) $data->paymentMethod);
+            }
+
+            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);
+        }
+        $cart = Cart::getCart();
+
+        return (array) CartData::fromModel($cart);
+    }
+
+    /**
+     * 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'));
+        }
+    }
+}

+ 4 - 4
packages/Webkul/Sales/src/Repositories/OrderRepository.php

@@ -117,13 +117,13 @@ class OrderRepository extends Repository
      * @param  \Webkul\Sales\Models\Order|int  $orderOrId
      * @return bool
      */
-    public function cancel($orderOrId)
+    public function cancel($orderOrId,$force=false)
     {
         /* order */
         $order = $this->resolveOrderInstance($orderOrId);
 
         /* check wether order can be cancelled or not */
-        if (! $order->canCancel()) {
+        if (! $order->canCancel() && ! $force) {
             return false;
         }
 
@@ -167,7 +167,7 @@ class OrderRepository extends Repository
             $this->downloadableLinkPurchasedRepository->updateStatus($item, 'expired');
         }
 
-        $this->updateOrderStatus($order);
+        $this->updateOrderStatus($order, $force ? Order::STATUS_CANCELED : null);
 
         Event::dispatch('sales.order.cancel.after', $order);
 
@@ -299,7 +299,7 @@ class OrderRepository extends Repository
 
         if (! empty($orderState)) {
             $status = $orderState;
-        } else {
+        } else {    
             $status = Order::STATUS_PROCESSING;
 
             if ($this->isInCompletedState($order)) {