Browse Source

同步官方api代码到4月23

chengwl 3 weeks ago
parent
commit
f93b330b11
71 changed files with 2710 additions and 740 deletions
  1. 2 2
      packages/Webkul/BagistoApi/config/api-platform.php
  2. 1 1
      packages/Webkul/BagistoApi/src/Console/Commands/ClearApiPlatformCacheCommand.php
  3. 17 19
      packages/Webkul/BagistoApi/src/Dto/CartData.php
  4. 50 13
      packages/Webkul/BagistoApi/src/Dto/CartItemData.php
  5. 27 0
      packages/Webkul/BagistoApi/src/Dto/CheckoutAddressInput.php
  6. 13 1
      packages/Webkul/BagistoApi/src/Dto/CreateCompareItemInput.php
  7. 13 1
      packages/Webkul/BagistoApi/src/Dto/CreateWishlistInput.php
  8. 6 20
      packages/Webkul/BagistoApi/src/Dto/ShippingRateOutput.php
  9. 100 0
      packages/Webkul/BagistoApi/src/Http/Controllers/DownloadSampleController.php
  10. 109 0
      packages/Webkul/BagistoApi/src/Http/Controllers/DownloadablePurchasedController.php
  11. 1 1
      packages/Webkul/BagistoApi/src/Http/Controllers/SwaggerUIController.php
  12. 36 11
      packages/Webkul/BagistoApi/src/Http/Middleware/SetLocaleChannel.php
  13. 12 6
      packages/Webkul/BagistoApi/src/Models/Attribute.php
  14. 8 1
      packages/Webkul/BagistoApi/src/Models/AttributeOption.php
  15. 4 2
      packages/Webkul/BagistoApi/src/Models/BookingProduct.php
  16. 40 0
      packages/Webkul/BagistoApi/src/Models/BookingProductEventTicket.php
  17. 21 3
      packages/Webkul/BagistoApi/src/Models/BookingProductEventTicketTranslation.php
  18. 32 0
      packages/Webkul/BagistoApi/src/Models/BookingProductRentalSlot.php
  19. 124 0
      packages/Webkul/BagistoApi/src/Models/BookingSlot.php
  20. 18 0
      packages/Webkul/BagistoApi/src/Models/CartToken.php
  21. 2 2
      packages/Webkul/BagistoApi/src/Models/Category.php
  22. 38 13
      packages/Webkul/BagistoApi/src/Models/Channel.php
  23. 2 1
      packages/Webkul/BagistoApi/src/Models/Country.php
  24. 2 1
      packages/Webkul/BagistoApi/src/Models/Currency.php
  25. 13 2
      packages/Webkul/BagistoApi/src/Models/CustomerOrderShipmentItem.php
  26. 2 1
      packages/Webkul/BagistoApi/src/Models/Locale.php
  27. 50 10
      packages/Webkul/BagistoApi/src/Models/Page.php
  28. 99 18
      packages/Webkul/BagistoApi/src/Models/Product.php
  29. 2 1
      packages/Webkul/BagistoApi/src/Models/ProductBundleOption.php
  30. 2 0
      packages/Webkul/BagistoApi/src/Models/ProductCustomerGroupPrice.php
  31. 16 0
      packages/Webkul/BagistoApi/src/Models/ProductCustomizableOptionPrice.php
  32. 29 13
      packages/Webkul/BagistoApi/src/Models/ProductDownloadableLink.php
  33. 3 11
      packages/Webkul/BagistoApi/src/Models/ProductDownloadableSample.php
  34. 2 0
      packages/Webkul/BagistoApi/src/Models/ProductImage.php
  35. 2 0
      packages/Webkul/BagistoApi/src/Models/ProductVideo.php
  36. 2 1
      packages/Webkul/BagistoApi/src/Models/ThemeCustomization.php
  37. 64 1
      packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php
  38. 2 1
      packages/Webkul/BagistoApi/src/Resolver/ProductCollectionResolver.php
  39. 16 10
      packages/Webkul/BagistoApi/src/Resources/lang/en/app.php
  40. 14 1
      packages/Webkul/BagistoApi/src/Routing/CustomIriConverter.php
  41. 33 32
      packages/Webkul/BagistoApi/src/State/AttributeCollectionProvider.php
  42. 33 32
      packages/Webkul/BagistoApi/src/State/AttributeOptionCollectionProvider.php
  43. 178 0
      packages/Webkul/BagistoApi/src/State/BookingSlotProvider.php
  44. 50 0
      packages/Webkul/BagistoApi/src/State/CancelOrderProcessor.php
  45. 47 0
      packages/Webkul/BagistoApi/src/State/CartProcessor.php
  46. 4 2
      packages/Webkul/BagistoApi/src/State/CartTokenMutationProvider.php
  47. 162 14
      packages/Webkul/BagistoApi/src/State/CartTokenProcessor.php
  48. 101 19
      packages/Webkul/BagistoApi/src/State/CompareItemProcessor.php
  49. 29 44
      packages/Webkul/BagistoApi/src/State/CompareItemProvider.php
  50. 31 38
      packages/Webkul/BagistoApi/src/State/CountryStateCollectionProvider.php
  51. 91 0
      packages/Webkul/BagistoApi/src/State/CursorAwareCollectionProvider.php
  52. 51 27
      packages/Webkul/BagistoApi/src/State/CustomerAddressProvider.php
  53. 37 14
      packages/Webkul/BagistoApi/src/State/CustomerDownloadableProductProvider.php
  54. 37 16
      packages/Webkul/BagistoApi/src/State/CustomerInvoiceProvider.php
  55. 37 17
      packages/Webkul/BagistoApi/src/State/CustomerOrderProvider.php
  56. 37 14
      packages/Webkul/BagistoApi/src/State/CustomerOrderShipmentProvider.php
  57. 13 20
      packages/Webkul/BagistoApi/src/State/CustomerProfileProcessor.php
  58. 37 14
      packages/Webkul/BagistoApi/src/State/CustomerReviewProvider.php
  59. 31 49
      packages/Webkul/BagistoApi/src/State/FilterableAttributesProvider.php
  60. 42 12
      packages/Webkul/BagistoApi/src/State/GetCheckoutAddressCollectionProvider.php
  61. 56 11
      packages/Webkul/BagistoApi/src/State/MoveWishlistToCartProcessor.php
  62. 153 0
      packages/Webkul/BagistoApi/src/State/PageProvider.php
  63. 50 24
      packages/Webkul/BagistoApi/src/State/ProductBagistoApiProvider.php
  64. 110 89
      packages/Webkul/BagistoApi/src/State/ProductGraphQLProvider.php
  65. 33 6
      packages/Webkul/BagistoApi/src/State/ProductReviewProvider.php
  66. 45 0
      packages/Webkul/BagistoApi/src/State/ReorderProcessor.php
  67. 6 6
      packages/Webkul/BagistoApi/src/State/ShippingRatesProvider.php
  68. 35 0
      packages/Webkul/BagistoApi/src/State/SnakeCaseLinksHandler.php
  69. 112 20
      packages/Webkul/BagistoApi/src/State/WishlistProcessor.php
  70. 29 41
      packages/Webkul/BagistoApi/src/State/WishlistProvider.php
  71. 4 11
      packages/Webkul/BagistoApi/src/Traits/HasRateLimit.php

+ 2 - 2
packages/Webkul/BagistoApi/config/api-platform.php

@@ -40,8 +40,8 @@ return [
             'Webkul\BagistoApi\Http\Middleware\HandleInvalidInputException',
             'Webkul\BagistoApi\Http\Middleware\SecurityHeaders',
             'Webkul\BagistoApi\Http\Middleware\LogApiRequests',
-            'Webkul\BagistoApi\Http\Middleware\SetLocaleChannel',
             'Webkul\BagistoApi\Http\Middleware\VerifyStorefrontKey',
+            'Webkul\BagistoApi\Http\Middleware\SetLocaleChannel',
             'Webkul\BagistoApi\Http\Middleware\BagistoApiDocumentationMiddleware',
             'Webkul\BagistoApi\Http\Middleware\ForceApiJson',
             'Spatie\ResponseCache\Middlewares\CacheResponse',
@@ -106,8 +106,8 @@ return [
         ],
         // GraphQL middleware for authentication and rate limiting
         'middleware' => [
-            'Webkul\BagistoApi\Http\Middleware\SetLocaleChannel',
             'Webkul\BagistoApi\Http\Middleware\VerifyGraphQLStorefrontKey',
+            'Webkul\BagistoApi\Http\Middleware\SetLocaleChannel',
         ],
     ],
 

+ 1 - 1
packages/Webkul/BagistoApi/src/Console/Commands/ClearApiPlatformCacheCommand.php

@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Cache;
 
 class ClearApiPlatformCacheCommand extends Command
 {
-    protected $signature = 'api-platform:clear-cache {--store= : Override cache store name to flush}';
+    protected $signature = 'bagisto-api-platform:clear-cache {--store= : Override cache store name to flush}';
 
     protected $description = 'Flush API Platform metadata/schema cache store(s) so GraphQL/OpenAPI schema updates are picked up.';
 

+ 17 - 19
packages/Webkul/BagistoApi/src/Dto/CartData.php

@@ -230,35 +230,35 @@ class CartData
 
         $data->items = $items->toArray();
 
-        $data->subtotal = (float) ($cart->sub_total ?? 0);
+        $data->subtotal = (float) core()->convertPrice($cart->base_sub_total ?? 0);
         $data->baseSubtotal = (float) ($cart->base_sub_total ?? 0);
-        $data->formattedSubtotal = core()->formatPrice($cart->sub_total ?? 0);
+        $data->formattedSubtotal = core()->currency($cart->base_sub_total ?? 0);
 
-        $data->subTotalInclTax = (float) ($cart->sub_total_incl_tax ?? $cart->sub_total ?? 0);
+        $data->subTotalInclTax = (float) core()->convertPrice($cart->base_sub_total_incl_tax ?? $cart->base_sub_total ?? 0);
         $data->baseSubTotalInclTax = (float) ($cart->base_sub_total_incl_tax ?? $cart->base_sub_total ?? 0);
-        $data->formattedSubTotalInclTax = core()->formatPrice($cart->sub_total_incl_tax ?? $cart->sub_total ?? 0);
+        $data->formattedSubTotalInclTax = core()->currency($cart->base_sub_total_incl_tax ?? $cart->base_sub_total ?? 0);
 
-        $data->taxAmount = (float) ($cart->tax_amount ?? 0);
+        $data->taxAmount = (float) core()->convertPrice($cart->base_tax_amount ?? 0);
         $data->baseTaxAmount = (float) ($cart->base_tax_amount ?? 0);
-        $data->taxTotal = (float) ($cart->tax_total ?? $cart->tax_amount ?? 0);
-        $data->formattedTaxTotal = core()->formatPrice($cart->tax_total ?? $cart->tax_amount ?? 0);
-        $data->formattedTaxAmount = core()->formatPrice($cart->tax_amount ?? 0);
+        $data->taxTotal = (float) core()->convertPrice($cart->base_tax_total ?? $cart->base_tax_amount ?? 0);
+        $data->formattedTaxTotal = core()->currency($cart->base_tax_total ?? $cart->base_tax_amount ?? 0);
+        $data->formattedTaxAmount = core()->currency($cart->base_tax_amount ?? 0);
 
-        $data->discountAmount = (float) ($cart->discount_amount ?? 0);
+        $data->discountAmount = (float) core()->convertPrice($cart->base_discount_amount ?? 0);
         $data->baseDiscountAmount = (float) ($cart->base_discount_amount ?? 0);
-        $data->formattedDiscountAmount = core()->formatPrice($cart->discount_amount ?? 0);
+        $data->formattedDiscountAmount = core()->currency($cart->base_discount_amount ?? 0);
 
-        $data->shippingAmount = (float) ($cart->shipping_amount ?? 0);
+        $data->shippingAmount = (float) core()->convertPrice($cart->base_shipping_amount ?? 0);
         $data->baseShippingAmount = (float) ($cart->base_shipping_amount ?? 0);
-        $data->formattedShippingAmount = core()->formatPrice($cart->shipping_amount ?? 0);
+        $data->formattedShippingAmount = core()->currency($cart->base_shipping_amount ?? 0);
 
-        $data->shippingAmountInclTax = (float) ($cart->shipping_amount_incl_tax ?? $cart->shipping_amount ?? 0);
+        $data->shippingAmountInclTax = (float) core()->convertPrice($cart->base_shipping_amount_incl_tax ?? $cart->base_shipping_amount ?? 0);
         $data->baseShippingAmountInclTax = (float) ($cart->base_shipping_amount_incl_tax ?? $cart->base_shipping_amount ?? 0);
-        $data->formattedShippingAmountInclTax = core()->formatPrice($cart->shipping_amount_incl_tax ?? $cart->shipping_amount ?? 0);
+        $data->formattedShippingAmountInclTax = core()->currency($cart->base_shipping_amount_incl_tax ?? $cart->base_shipping_amount ?? 0);
 
-        $data->grandTotal = (float) ($cart->grand_total ?? 0);
+        $data->grandTotal = (float) core()->convertPrice($cart->base_grand_total ?? 0);
         $data->baseGrandTotal = (float) ($cart->base_grand_total ?? 0);
-        $data->formattedGrandTotal = core()->formatPrice($cart->grand_total ?? 0);
+        $data->formattedGrandTotal = core()->currency($cart->base_grand_total ?? 0);
 
         $additional = $cart->additional ?
             (is_string($cart->additional) ? json_decode($cart->additional, true) : $cart->additional) : [];
@@ -308,9 +308,7 @@ class CartData
             $data->appliedTaxes = [];
         }
 
-        $data->haveStockableItems = $cart->items()->whereHas('product', function ($q) {
-            $q->where('type', 'simple');
-        })->count() > 0;
+        $data->haveStockableItems = $cart->haveStockableItems();
 
         if ($cart->selected_shipping_rate) {
             $data->selectedShippingRate = $cart->selected_shipping_rate->method ?? null;

+ 50 - 13
packages/Webkul/BagistoApi/src/Dto/CartItemData.php

@@ -120,36 +120,47 @@ class CartItemData
         $data->type = $item->type;
 
         // Base prices
-        $data->price = (float) ($item->price ?? 0);
+        $data->price = (float) core()->convertPrice($item->base_price ?? 0);
         $data->basePrice = (float) ($item->base_price ?? 0);
-        $data->formattedPrice = core()->formatPrice($item->price ?? 0);
+        $data->formattedPrice = core()->currency($item->base_price ?? 0);
 
         // Prices including tax
-        $data->priceInclTax = (float) ($item->price_incl_tax ?? $item->price ?? 0);
+        $data->priceInclTax = (float) core()->convertPrice($item->base_price_incl_tax ?? $item->base_price ?? 0);
         $data->basePriceInclTax = (float) ($item->base_price_incl_tax ?? $item->base_price ?? 0);
-        $data->formattedPriceInclTax = core()->formatPrice($item->price_incl_tax ?? $item->price ?? 0);
+        $data->formattedPriceInclTax = core()->currency($item->base_price_incl_tax ?? $item->base_price ?? 0);
 
         // Line totals
-        $data->total = (float) ($item->total ?? 0);
+        $data->total = (float) core()->convertPrice($item->base_total ?? 0);
         $data->baseTotal = (float) ($item->base_total ?? 0);
-        $data->formattedTotal = core()->formatPrice($item->total ?? 0);
+        $data->formattedTotal = core()->currency($item->base_total ?? 0);
 
         // Line totals including tax
-        $data->totalInclTax = (float) ($item->total_incl_tax ?? $item->total ?? 0);
+        $data->totalInclTax = (float) core()->convertPrice($item->base_total_incl_tax ?? $item->base_total ?? 0);
         $data->baseTotalInclTax = (float) ($item->base_total_incl_tax ?? $item->base_total ?? 0);
-        $data->formattedTotalInclTax = core()->formatPrice($item->total_incl_tax ?? $item->total ?? 0);
+        $data->formattedTotalInclTax = core()->currency($item->base_total_incl_tax ?? $item->base_total ?? 0);
 
         // Discounts
-        $data->discountAmount = (float) ($item->discount_amount ?? 0);
+        $data->discountAmount = (float) core()->convertPrice($item->base_discount_amount ?? 0);
         $data->baseDiscountAmount = (float) ($item->base_discount_amount ?? 0);
 
         // Tax
-        $data->taxAmount = (float) ($item->tax_amount ?? 0);
+        $data->taxAmount = (float) core()->convertPrice($item->base_tax_amount ?? 0);
         $data->baseTaxAmount = (float) ($item->base_tax_amount ?? 0);
 
-        // Product info
-        $data->options = $item->additional ?
-            (is_string($item->additional) ? json_decode($item->additional, true) : $item->additional) : null;
+        // Product info - extract formatted attributes (bundle options, configurable options, etc.)
+        $additional = $item->additional ?
+            (is_string($item->additional) ? json_decode($item->additional, true) : $item->additional) : [];
+
+        $attributes = ! empty($additional['attributes'])
+            ? array_values($additional['attributes'])
+            : null;
+
+        // For bundle products, enrich options with can_change_qty and is_required from DB
+        if ($attributes && $item->type === 'bundle') {
+            $attributes = self::enrichBundleOptions($attributes, $additional);
+        }
+
+        $data->options = $attributes;
 
         // Base image
         if ($item->product) {
@@ -164,4 +175,30 @@ class CartItemData
 
         return $data;
     }
+
+    /**
+     * Enrich bundle option attributes with can_change_qty and is_required from DB.
+     *
+     * Checkbox/Multiselect options: qty is fixed by admin (can_change_qty = false)
+     * Radio/Select options: qty can be changed by customer (can_change_qty = true)
+     */
+    private static function enrichBundleOptions(array $attributes, array $additional): array
+    {
+        $optionRepo = app(\Webkul\Product\Repositories\ProductBundleOptionRepository::class);
+
+        foreach ($attributes as &$attribute) {
+            $optionId = $attribute['option_id'] ?? null;
+
+            if (! $optionId) {
+                continue;
+            }
+
+            $bundleOption = $optionRepo->find($optionId);
+
+            $attribute['is_required'] = (bool) ($bundleOption?->is_required ?? false);
+            $attribute['can_change_qty'] = in_array($bundleOption?->type, ['radio', 'select']);
+        }
+
+        return $attributes;
+    }
 }

+ 27 - 0
packages/Webkul/BagistoApi/src/Dto/CheckoutAddressInput.php

@@ -4,6 +4,7 @@ namespace Webkul\BagistoApi\Dto;
 
 use ApiPlatform\Metadata\ApiProperty;
 use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\SerializedName;
 
 /**
  * CheckoutAddressInput - GraphQL Input DTO for Checkout Address
@@ -16,109 +17,135 @@ class CheckoutAddressInput
     // Billing Address
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing first name')]
+    #[SerializedName('billingFirstName')]
     public ?string $billingFirstName = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing last name')]
+    #[SerializedName('billingLastName')]
     public ?string $billingLastName = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing email')]
+    #[SerializedName('billingEmail')]
     public ?string $billingEmail = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing company name')]
+    #[SerializedName('billingCompanyName')]
     public ?string $billingCompanyName = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing address')]
+    #[SerializedName('billingAddress')]
     public ?string $billingAddress = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing country')]
+    #[SerializedName('billingCountry')]
     public ?string $billingCountry = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing state')]
+    #[SerializedName('billingState')]
     public ?string $billingState = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing city')]
+    #[SerializedName('billingCity')]
     public ?string $billingCity = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing postcode')]
+    #[SerializedName('billingPostcode')]
     public ?string $billingPostcode = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Billing phone number')]
+    #[SerializedName('billingPhoneNumber')]
     public ?string $billingPhoneNumber = null;
 
     // Shipping Address
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping first name')]
+    #[SerializedName('shippingFirstName')]
     public ?string $shippingFirstName = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping last name')]
+    #[SerializedName('shippingLastName')]
     public ?string $shippingLastName = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping email')]
+    #[SerializedName('shippingEmail')]
     public ?string $shippingEmail = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping company name')]
+    #[SerializedName('shippingCompanyName')]
     public ?string $shippingCompanyName = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping address')]
+    #[SerializedName('shippingAddress')]
     public ?string $shippingAddress = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping country')]
+    #[SerializedName('shippingCountry')]
     public ?string $shippingCountry = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping state')]
+    #[SerializedName('shippingState')]
     public ?string $shippingState = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping city')]
+    #[SerializedName('shippingCity')]
     public ?string $shippingCity = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping postcode')]
+    #[SerializedName('shippingPostcode')]
     public ?string $shippingPostcode = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping phone number')]
+    #[SerializedName('shippingPhoneNumber')]
     public ?string $shippingPhoneNumber = null;
 
     // Flags
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Use address for shipping')]
+    #[SerializedName('useForShipping')]
     public ?bool $useForShipping = null;
 
     // Additional fields for shipping and payment methods
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Shipping method code')]
+    #[SerializedName('shippingMethod')]
     public ?string $shippingMethod = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Payment method code')]
+    #[SerializedName('paymentMethod')]
     public ?string $paymentMethod = null;
 
     // Payment callback URLs (for headless frontends)
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Payment success callback URL')]
+    #[SerializedName('paymentSuccessUrl')]
     public ?string $paymentSuccessUrl = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Payment failure callback URL')]
+    #[SerializedName('paymentFailureUrl')]
     public ?string $paymentFailureUrl = null;
 
     #[Groups(['mutation'])]
     #[ApiProperty(description: 'Payment cancel callback URL')]
+    #[SerializedName('paymentCancelUrl')]
     public ?string $paymentCancelUrl = null;
 }

+ 13 - 1
packages/Webkul/BagistoApi/src/Dto/CreateCompareItemInput.php

@@ -3,6 +3,7 @@
 namespace Webkul\BagistoApi\Dto;
 
 use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * DTO for creating a compare item
@@ -16,5 +17,16 @@ class CreateCompareItemInput
      * Product ID to add to comparison
      */
     #[ApiProperty(description: 'The ID of the product to add to comparison')]
-    public ?int $productId = null;
+    #[Groups(['mutation'])]
+    public ?int $product_id = null;
+
+    public function getProduct_id(): ?int
+    {
+        return $this->product_id;
+    }
+
+    public function setProduct_id(?int $product_id): void
+    {
+        $this->product_id = $product_id;
+    }
 }

+ 13 - 1
packages/Webkul/BagistoApi/src/Dto/CreateWishlistInput.php

@@ -3,6 +3,7 @@
 namespace Webkul\BagistoApi\Dto;
 
 use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * DTO for creating a wishlist item
@@ -16,5 +17,16 @@ class CreateWishlistInput
      * Product ID to add to wishlist
      */
     #[ApiProperty(description: 'The ID of the product to add to wishlist')]
-    public ?int $productId = null;
+    #[Groups(['mutation'])]
+    public ?int $product_id = null;
+
+    public function getProduct_id(): ?int
+    {
+        return $this->product_id;
+    }
+
+    public function setProduct_id(?int $product_id): void
+    {
+        $this->product_id = $product_id;
+    }
 }

+ 6 - 20
packages/Webkul/BagistoApi/src/Dto/ShippingRateOutput.php

@@ -3,7 +3,6 @@
 namespace Webkul\BagistoApi\Dto;
 
 use ApiPlatform\Metadata\ApiProperty;
-use Symfony\Component\Serializer\Annotation\Groups;
 
 /**
  * ShippingRateOutput - GraphQL Output DTO for Shipping Rates
@@ -12,55 +11,42 @@ use Symfony\Component\Serializer\Annotation\Groups;
  */
 class ShippingRateOutput
 {
-    #[Groups(['query'])]
     #[ApiProperty(identifier: true, readable: true, writable: false)]
     public ?string $id = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
     public ?string $code = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
     public ?string $label = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
     public ?float $price = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
-    public ?string $formattedPrice = null;
+    public ?string $formatted_price = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
     public ?string $description = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
     public ?string $method = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
-    public ?string $methodTitle = null;
+    public ?string $method_title = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
-    public ?string $methodDescription = null;
+    public ?string $method_description = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
-    public ?float $basePrice = null;
+    public ?float $base_price = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
-    public ?string $baseFormattedPrice = null;
+    public ?string $base_formatted_price = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
     public ?string $carrier = null;
 
-    #[Groups(['query'])]
     #[ApiProperty(readable: true, writable: false)]
-    public ?string $carrierTitle = null;
+    public ?string $carrier_title = null;
 }

+ 100 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/DownloadSampleController.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Support\Facades\Storage;
+use Webkul\Product\Repositories\ProductDownloadableLinkRepository;
+use Webkul\Product\Repositories\ProductDownloadableSampleRepository;
+use Webkul\Product\Repositories\ProductRepository;
+
+class DownloadSampleController extends Controller
+{
+    public function __construct(
+        protected ProductRepository $productRepository,
+        protected ProductDownloadableLinkRepository $productDownloadableLinkRepository,
+        protected ProductDownloadableSampleRepository $productDownloadableSampleRepository,
+    ) {}
+
+    /**
+     * Download sample file for a downloadable product.
+     *
+     * @param  string  $type  "link" or "sample"
+     * @return \Illuminate\Http\Response
+     */
+    public function __invoke(string $type, int $id)
+    {
+        if ($type === 'link') {
+            return $this->downloadLinkSample($id);
+        }
+
+        return $this->downloadProductSample($id);
+    }
+
+    private function downloadLinkSample(int $id)
+    {
+        $productDownloadableLink = $this->productDownloadableLinkRepository->find($id);
+
+        if (! $productDownloadableLink) {
+            return response()->json(['message' => 'Downloadable link not found.', 'error' => 'not_found'], 404);
+        }
+
+        if ($productDownloadableLink->sample_type === 'file') {
+            $privateDisk = Storage::disk('private');
+
+            if ($privateDisk->exists($productDownloadableLink->sample_file)) {
+                return $privateDisk->download($productDownloadableLink->sample_file);
+            }
+
+            // Fallback to public disk
+            if (Storage::exists($productDownloadableLink->sample_file)) {
+                return Storage::download($productDownloadableLink->sample_file);
+            }
+
+            return response()->json(['message' => 'Sample file not found.', 'error' => 'file_not_found'], 404);
+        }
+
+        $fileName = basename($productDownloadableLink->sample_url);
+        $tempImage = tempnam(sys_get_temp_dir(), $fileName);
+        copy($productDownloadableLink->sample_url, $tempImage);
+
+        return response()->download($tempImage, $fileName);
+    }
+
+    private function downloadProductSample(int $id)
+    {
+        $productDownloadableSample = $this->productDownloadableSampleRepository->find($id);
+
+        if (! $productDownloadableSample) {
+            return response()->json(['message' => 'Downloadable sample not found.', 'error' => 'not_found'], 404);
+        }
+
+        $product = $this->productRepository->find($productDownloadableSample->product_id);
+
+        if (! $product || ! $product->visible_individually) {
+            return response()->json(['message' => 'Product not found.', 'error' => 'not_found'], 404);
+        }
+
+        if ($productDownloadableSample->type === 'file') {
+            // Check public disk first
+            if (Storage::exists($productDownloadableSample->file)) {
+                return Storage::download($productDownloadableSample->file);
+            }
+
+            // Fallback to private disk
+            $privateDisk = Storage::disk('private');
+
+            if ($privateDisk->exists($productDownloadableSample->file)) {
+                return $privateDisk->download($productDownloadableSample->file);
+            }
+
+            return response()->json(['message' => 'Sample file not found.', 'error' => 'file_not_found'], 404);
+        }
+
+        $fileName = basename($productDownloadableSample->url);
+        $tempImage = tempnam(sys_get_temp_dir(), $fileName);
+        copy($productDownloadableSample->url, $tempImage);
+
+        return response()->download($tempImage, $fileName);
+    }
+}

+ 109 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/DownloadablePurchasedController.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Storage;
+use Webkul\Sales\Repositories\DownloadableLinkPurchasedRepository;
+
+class DownloadablePurchasedController extends Controller
+{
+    public function __construct(
+        protected DownloadableLinkPurchasedRepository $downloadableLinkPurchasedRepository,
+    ) {}
+
+    /**
+     * Download a purchased downloadable product file.
+     *
+     * @param  int  $id  Downloadable link purchased ID (_id from customerDownloadableProducts)
+     * @return \Illuminate\Http\Response
+     */
+    public function __invoke(int $id)
+    {
+        $customer = Auth::guard('sanctum')->user();
+
+        if (! $customer) {
+            return response()->json([
+                'message' => 'Unauthorized: Customer authentication required.',
+                'error'   => 'unauthenticated',
+            ], 401);
+        }
+
+        $downloadableLinkPurchased = $this->downloadableLinkPurchasedRepository->findOneByField([
+            'id'          => $id,
+            'customer_id' => $customer->id,
+        ]);
+
+        if (! $downloadableLinkPurchased) {
+            return response()->json([
+                'message' => 'Downloadable product not found.',
+                'error'   => 'not_found',
+            ], 404);
+        }
+
+        if ($downloadableLinkPurchased->status === 'pending') {
+            return response()->json([
+                'message' => 'Download is pending. Please wait for the order to be invoiced.',
+                'error'   => 'download_pending',
+            ], 403);
+        }
+
+        $totalInvoiceQty = 0;
+
+        if (isset($downloadableLinkPurchased->order->invoices)) {
+            foreach ($downloadableLinkPurchased->order->invoices as $invoice) {
+                $totalInvoiceQty += $invoice->total_qty;
+            }
+        }
+
+        $orderedQty = $downloadableLinkPurchased->order->total_qty_ordered;
+        $totalInvoiceQty = $totalInvoiceQty * ($downloadableLinkPurchased->download_bought / $orderedQty);
+
+        if ($downloadableLinkPurchased->download_used >= $totalInvoiceQty) {
+            return response()->json([
+                'message' => 'Download limit exceeded.',
+                'error'   => 'download_limit_exceeded',
+            ], 403);
+        }
+
+        if (
+            $downloadableLinkPurchased->download_bought
+            && ($downloadableLinkPurchased->download_bought - ($downloadableLinkPurchased->download_used + $downloadableLinkPurchased->download_canceled)) <= 0
+        ) {
+            return response()->json([
+                'message' => 'No more downloads available.',
+                'error'   => 'no_downloads_remaining',
+            ], 403);
+        }
+
+        $remainingDownloads = $downloadableLinkPurchased->download_bought
+            - ($downloadableLinkPurchased->download_used + $downloadableLinkPurchased->download_canceled + 1);
+
+        if ($downloadableLinkPurchased->download_bought) {
+            $this->downloadableLinkPurchasedRepository->update([
+                'download_used' => $downloadableLinkPurchased->download_used + 1,
+                'status'        => $remainingDownloads <= 0 ? 'expired' : $downloadableLinkPurchased->status,
+            ], $downloadableLinkPurchased->id);
+        }
+
+        if ($downloadableLinkPurchased->type === 'file') {
+            $privateDisk = Storage::disk('private');
+
+            if (! $privateDisk->exists($downloadableLinkPurchased->file)) {
+                return response()->json([
+                    'message' => 'File not found.',
+                    'error'   => 'file_not_found',
+                ], 404);
+            }
+
+            return $privateDisk->download($downloadableLinkPurchased->file);
+        }
+
+        $fileName = basename($downloadableLinkPurchased->url);
+        $tempImage = tempnam(sys_get_temp_dir(), $fileName);
+        copy($downloadableLinkPurchased->url, $tempImage);
+
+        return response()->download($tempImage, $fileName);
+    }
+}

+ 1 - 1
packages/Webkul/BagistoApi/src/Http/Controllers/SwaggerUIController.php

@@ -136,7 +136,7 @@ class SwaggerUIController extends Controller
                 'openapi' => '3.0.0',
                 'info'    => [
                     'title'       => 'Error',
-                    'version'     => '1.0.0',
+                    'version'     => '1.0.3',
                     'description' => 'Failed to generate OpenAPI specification: '.$e->getMessage(),
                 ],
                 'paths'      => [],

+ 36 - 11
packages/Webkul/BagistoApi/src/Http/Middleware/SetLocaleChannel.php

@@ -7,15 +7,17 @@ use Illuminate\Http\Request;
 use Symfony\Component\HttpFoundation\Response;
 
 /**
- * Reads X-Locale and X-Channel headers from API requests and binds
- * them into the request attributes so providers/resolvers can use them.
+ * Reads optional X-Locale, X-Channel and X-Currency headers from API
+ * requests and configures the application accordingly.
  *
- * If the headers are not sent, the current application locale and
- * default channel are used — existing behaviour is preserved.
+ * All three headers are optional. When omitted the channel's default
+ * value is used. When provided but invalid for the current channel,
+ * the channel default is used as a fallback.
  *
  * Headers:
- *   X-Locale  — locale code, e.g. "en", "fr", "ar"
- *   X-Channel — channel code, e.g. "default"
+ *   X-LOCALE   — locale code, e.g. "en", "fr", "ar"
+ *   X-CHANNEL  — channel code, e.g. "default"
+ *   X-CURRENCY — currency code, e.g. "USD", "EUR", "INR"
  */
 class SetLocaleChannel
 {
@@ -24,16 +26,39 @@ class SetLocaleChannel
      */
     public function handle(Request $request, Closure $next): Response
     {
-        $locale  = $request->header('X-Locale');
-        $channel = $request->header('X-Channel');
+        $channel = core()->getCurrentChannel();
 
-        if ($locale) {
+        // --- Channel ---
+        $channelCode = $request->header('X-Channel');
+
+        if ($channelCode) {
+            $request->attributes->set('bagisto_channel', $channelCode);
+        }
+
+        // --- Locale (optional — defaults to channel's default locale) ---
+        $locale = $request->header('X-Locale');
+        $availableLocales = $channel->locales->pluck('code')->toArray();
+        $defaultLocale = $channel->default_locale->code;
+
+        if ($locale && in_array($locale, $availableLocales)) {
             app()->setLocale($locale);
             $request->attributes->set('bagisto_locale', $locale);
+        } else {
+            app()->setLocale($defaultLocale);
+            $request->attributes->set('bagisto_locale', $defaultLocale);
         }
 
-        if ($channel) {
-            $request->attributes->set('bagisto_channel', $channel);
+        // --- Currency (optional — defaults to channel's base currency) ---
+        $currency = $request->header('X-Currency');
+        $availableCurrencies = $channel->currencies->pluck('code')->toArray();
+        $defaultCurrency = $channel->base_currency->code;
+
+        if ($currency && in_array($currency, $availableCurrencies)) {
+            core()->setCurrentCurrency($currency);
+            $request->attributes->set('bagisto_currency', $currency);
+        } else {
+            core()->setCurrentCurrency($defaultCurrency);
+            $request->attributes->set('bagisto_currency', $defaultCurrency);
         }
 
         return $next($request);

+ 12 - 6
packages/Webkul/BagistoApi/src/Models/Attribute.php

@@ -4,14 +4,16 @@ namespace Webkul\BagistoApi\Models;
 
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\GraphQl\Query;
+use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use GraphQL\Error\UserError;
+use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model as EloquentModel;
 use Illuminate\Database\Eloquent\Relations\HasMany;
-use ApiPlatform\Metadata\GraphQl\Query;
-use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
-use ApiPlatform\Metadata\GetCollection;
-use ApiPlatform\Metadata\Get;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 /**
  * Simple Attribute model for GraphQL without TranslatableModel
@@ -22,7 +24,7 @@ use ApiPlatform\Metadata\Get;
     description: 'Product attribute resource',
     routePrefix: '/api/shop',
     graphQlOperations: [
-        new QueryCollection(),
+        new QueryCollection(provider: ::class),
         new Query(resolver: BaseQueryItemResolver::class),
     ],
     operations: [
@@ -36,7 +38,11 @@ use ApiPlatform\Metadata\Get;
 )]
 class Attribute extends \Webkul\Attribute\Models\Attribute
 {
-    protected $hidden = ['translation'];
+    #[ApiProperty(readableLink: true, description: 'Current locale translation')]
+    public function getTranslation(?string $locale = null, ?bool $withFallback = null): ?Model
+    {
+        return $this->translation;
+    }
 
     public static function boot()
     {

+ 8 - 1
packages/Webkul/BagistoApi/src/Models/AttributeOption.php

@@ -1,9 +1,10 @@
 <?php
 
 namespace Webkul\BagistoApi\Models;
- 
+
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
+use Illuminate\Database\Eloquent\Model;
 
 #[ApiResource(
     shortName: 'AttributeOption',
@@ -19,6 +20,12 @@ class AttributeOption extends \Webkul\Attribute\Models\AttributeOption
         return $this->translations;
     }
 
+    #[ApiProperty(readableLink: true, description: 'Current locale translation')]
+    public function getTranslation(?string $locale = null, ?bool $withFallback = null): ?Model
+    {
+        return $this->translation;
+    }
+
     /**
      * API Platform identifier
      */

+ 4 - 2
packages/Webkul/BagistoApi/src/Models/BookingProduct.php

@@ -14,7 +14,8 @@ use Webkul\BookingProduct\Models\BookingProduct as BaseBookingProduct;
     operations: [],
     graphQlOperations: []
 )]
-class BookingProduct extends BaseBookingProduct {
+class BookingProduct extends BaseBookingProduct
+{
     /**
      * Get default slot (for default booking type)
      */
@@ -84,7 +85,8 @@ class BookingProduct extends BaseBookingProduct {
     #[ApiProperty(writable: false, readable: true, required: false)]
     public function event_tickets(): HasMany
     {
-        return $this->hasMany(BookingProductEventTicket::class, 'booking_product_id');
+        return $this->hasMany(BookingProductEventTicket::class, 'booking_product_id')
+            ->with(['translation', 'translations']);
     }
 
     /**

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

@@ -5,11 +5,14 @@ namespace Webkul\BagistoApi\Models;
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
 use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
 use Webkul\BookingProduct\Models\BookingProductEventTicket as BaseModel;
 
 #[ApiResource(routePrefix: '/api/shop', operations: [], graphQlOperations: [])]
 class BookingProductEventTicket extends BaseModel
 {
+    protected $appends = ['formatted_price', 'formatted_special_price'];
+
     /**
      * Get the translations for the event ticket using BagistoApi model
      */
@@ -18,6 +21,16 @@ class BookingProductEventTicket extends BaseModel
         return $this->hasMany(BookingProductEventTicketTranslation::class, 'booking_product_event_ticket_id');
     }
 
+    public function getPriceAttribute($value)
+    {
+        return $value !== null ? (float) core()->convertPrice((float) $value) : null;
+    }
+
+    public function getSpecialPriceAttribute($value)
+    {
+        return $value !== null ? (float) core()->convertPrice((float) $value) : null;
+    }
+
     #[ApiProperty(writable: false, readable: true, required: false)]
     public function getPrice()
     {
@@ -36,6 +49,28 @@ class BookingProductEventTicket extends BaseModel
         return $this->special_price;
     }
 
+    public function getFormattedPriceAttribute(): ?string
+    {
+        return $this->price !== null ? core()->formatPrice($this->price) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true)]
+    public function getFormatted_price(): ?string
+    {
+        return $this->getFormattedPriceAttribute();
+    }
+
+    public function getFormattedSpecialPriceAttribute(): ?string
+    {
+        return $this->special_price !== null ? core()->formatPrice($this->special_price) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true)]
+    public function getFormatted_special_price(): ?string
+    {
+        return $this->getFormattedSpecialPriceAttribute();
+    }
+
     #[ApiProperty(writable: false, readable: true, required: false)]
     public function getSpecialPriceFrom()
     {
@@ -53,4 +88,9 @@ class BookingProductEventTicket extends BaseModel
     {
         return $this->booking_product_id;
     }
+
+    public function translation(): HasOne
+    {
+        return $this->hasOne(BookingProductEventTicketTranslation::class, 'booking_product_event_ticket_id');
+    }
 }

+ 21 - 3
packages/Webkul/BagistoApi/src/Models/BookingProductEventTicketTranslation.php

@@ -4,11 +4,29 @@ namespace Webkul\BagistoApi\Models;
 
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
-use Webkul\BookingProduct\Models\BookingProductEventTicketTranslation as BaseModel;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
-#[ApiResource(routePrefix: '/api/shop', operations: [], graphQlOperations: [])]
-class BookingProductEventTicketTranslation extends BaseModel
+#[ApiResource(operations: [], graphQlOperations: [])]
+class BookingProductEventTicketTranslation extends Model
 {
+    protected $table = 'booking_product_event_ticket_translations';
+
+    public $timestamps = false;
+
+    protected $fillable = ['name', 'description', 'locale', 'booking_product_event_ticket_id'];
+
+    public function eventTicket(): BelongsTo
+    {
+        return $this->belongsTo(BookingProductEventTicket::class, 'booking_product_event_ticket_id');
+    }
+
+    #[ApiProperty(writable: false, readable: true, required: false)]
+    public function getBookingProductEventTicketId()
+    {
+        return $this->booking_product_event_ticket_id;
+    }
+
     #[ApiProperty(writable: false, readable: true, required: false)]
     public function getName()
     {

+ 32 - 0
packages/Webkul/BagistoApi/src/Models/BookingProductRentalSlot.php

@@ -20,6 +20,16 @@ class BookingProductRentalSlot extends BaseModel
         return $this->renting_type;
     }
 
+    public function getDailyPriceAttribute($value)
+    {
+        return $value !== null ? (float) core()->convertPrice((float) $value) : null;
+    }
+
+    public function getHourlyPriceAttribute($value)
+    {
+        return $value !== null ? (float) core()->convertPrice((float) $value) : null;
+    }
+
     #[ApiProperty(writable: false, readable: true, required: false)]
     public function getDailyPrice()
     {
@@ -32,6 +42,28 @@ class BookingProductRentalSlot extends BaseModel
         return $this->hourly_price;
     }
 
+    public function getFormattedDailyPriceAttribute(): ?string
+    {
+        return $this->daily_price !== null ? core()->formatPrice($this->daily_price) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true)]
+    public function getFormatted_daily_price(): ?string
+    {
+        return $this->getFormattedDailyPriceAttribute();
+    }
+
+    public function getFormattedHourlyPriceAttribute(): ?string
+    {
+        return $this->hourly_price !== null ? core()->formatPrice($this->hourly_price) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true)]
+    public function getFormatted_hourly_price(): ?string
+    {
+        return $this->getFormattedHourlyPriceAttribute();
+    }
+
     #[ApiProperty(writable: false, readable: true, required: false)]
     public function getSameSlotAllDays()
     {

+ 124 - 0
packages/Webkul/BagistoApi/src/Models/BookingSlot.php

@@ -0,0 +1,124 @@
+<?php
+
+namespace Webkul\BagistoApi\Models;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GraphQl\QueryCollection;
+use Webkul\BagistoApi\State\BookingSlotProvider;
+
+/**
+ * Booking Slot GraphQL API Resource
+ *
+ * Provides available booking slots for a product on a specific date.
+ *
+ * For non-rental types (default, appointment, table, event):
+ *   Each BookingSlot is a single flat slot with from/to/timestamp/qty.
+ *
+ * For rental hourly type:
+ *   Each BookingSlot is a time-range group with a nested `slots` array.
+ *   The `time` field is the group label (e.g., "10:00 AM - 12:00 PM"),
+ *   and `slots` contains the individual hourly sub-slots.
+ */
+#[ApiResource(
+    routePrefix: '/api/shop',
+    shortName: 'BookingSlot',
+    uriTemplate: '/booking-slots',
+    operations: [],
+    graphQlOperations: [
+        new QueryCollection(
+            args: [
+                'id' => [
+                    'type'        => 'Int!',
+                    'description' => 'The booking product ID',
+                ],
+                'date' => [
+                    'type'        => 'String!',
+                    'description' => 'The date for which to get slots (YYYY-MM-DD)',
+                ],
+            ],
+            provider: BookingSlotProvider::class,
+            paginationEnabled: false,
+            description: 'Get available booking slots for a product on a specific date',
+        ),
+    ]
+)]
+class BookingSlot
+{
+    /**
+     * @var int|string|null Internal identifier (hidden from API)
+     */
+    #[ApiProperty(identifier: true, writable: false, readable: false)]
+    public $_id = null;
+
+    /**
+     * @var string|null Slot identifier (timestamp for flat slots, sequential index for groups)
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $slotId = null;
+
+    /**
+     * @var string|null Time range group label for rental hourly slots (e.g., "10:00 AM - 12:00 PM").
+     *                  Null for non-rental booking types.
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $time = null;
+
+    /**
+     * @var string|null Start time of the slot (e.g., "12:00 PM"). Used by non-rental types.
+     *                  Null for rental hourly (data is in `slots` array instead).
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $from = null;
+
+    /**
+     * @var string|null End time of the slot (e.g., "12:45 PM"). Used by non-rental types.
+     *                  Null for rental hourly (data is in `slots` array instead).
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $to = null;
+
+    /**
+     * @var string|null Timestamp range (e.g., "1774247400-1774250100"). Used by non-rental types.
+     *                  Null for rental hourly (data is in `slots` array instead).
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $timestamp = null;
+
+    /**
+     * @var string|null Indicates if the slot is available or qty remaining. Used by non-rental types.
+     *                  Null for rental hourly (data is in `slots` array instead).
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $qty = null;
+
+    /**
+     * @var iterable|null Nested hourly sub-slots for rental hourly type.
+     *                    Each element: { from, to, timestamp, qty }
+     *                    Null for non-rental booking types.
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public ?iterable $slots = null;
+
+    /**
+     * Create a new BookingSlot instance
+     */
+    public function __construct(
+        ?string $slotId = null,
+        ?string $time = null,
+        ?string $from = null,
+        ?string $to = null,
+        ?string $timestamp = null,
+        ?string $qty = null,
+        ?array $slots = null
+    ) {
+        $this->_id = $slotId ?? uniqid('slot_');
+        $this->slotId = $slotId;
+        $this->time = $time;
+        $this->from = $from;
+        $this->to = $to;
+        $this->timestamp = $timestamp;
+        $this->qty = $qty;
+        $this->slots = $slots;
+    }
+}

+ 18 - 0
packages/Webkul/BagistoApi/src/Models/CartToken.php

@@ -6,6 +6,7 @@ use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\Get;
 use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Post;
 use ApiPlatform\Metadata\GraphQl\Mutation;
 use Symfony\Component\Serializer\Annotation\Groups;
 use Webkul\BagistoApi\Dto\CartInput;
@@ -33,13 +34,30 @@ use Webkul\BagistoApi\State\CartTokenProcessor;
     paginationEnabled: false,
     uriTemplate: '/cart-tokens/{id}',
     operations: [
+        // new Post(
+        //     name: 'create',
+        //     uriTemplate: '/cart-tokens',
+        //     input: CartInput::class,
+        //     output: CartToken::class,
+        //     processor: CartTokenProcessor::class,
+        //     denormalizationContext: [
+        //         'allow_extra_attributes' => true,
+        //         'groups'                 => ['mutation'],
+        //     ],
+        //     normalizationContext: [
+        //         'groups'                 => ['mutation'],
+        //     ],
+        //     description: 'Create new guest cart with unique UUID token or get authenticated customer cart.',
+        // ),
         new GetCollection(uriTemplate: '/cart-tokens',
+            provider: CartTokenMutationProvider::class,
             normalizationContext: [
                 'groups' => ['query'],
             ],
             description: 'Get all customer carts',
         ),
         new Get(uriTemplate: '/cart-tokens/{id}',
+            provider: CartTokenMutationProvider::class,
             normalizationContext: [
                 'groups' => ['query'],
             ],

+ 2 - 2
packages/Webkul/BagistoApi/src/Models/Category.php

@@ -10,7 +10,7 @@ use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
 use Webkul\BagistoApi\Resolver\CategoryCollectionResolver;
-use Webkul\BagistoApi\State\RestCategoryTreeProvider;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 use Webkul\Category\Models\Category as BaseCategory;
 
 #[ApiResource(
@@ -25,7 +25,7 @@ use Webkul\Category\Models\Category as BaseCategory;
     ],
     graphQlOperations: [
         new Query(resolver: BaseQueryItemResolver::class),
-        new QueryCollection,
+        new QueryCollection(provider: CursorAwareCollectionProvider::class),
         new QueryCollection(
             name: 'tree',
             args: [

+ 38 - 13
packages/Webkul/BagistoApi/src/Models/Channel.php

@@ -8,6 +8,8 @@ use ApiPlatform\Metadata\Get;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
 use Webkul\BagistoApi\State\ChannelProvider;
 
@@ -34,29 +36,52 @@ class Channel extends \Webkul\Core\Models\Channel
     }
 
     /**
-     * Expose locales relationship as array for API (read-only)
+     * Override locales relationship to return API resource model
      */
-    #[ApiProperty(writable: false, readable: true)]
-    public ?array $_locales = null;
+    public function locales(): BelongsToMany
+    {
+        return $this->belongsToMany(Locale::class, 'channel_locales', 'channel_id', 'locale_id');
+    }
 
     /**
-     * Expose currencies relationship as array for API (read-only)
+     * Override currencies relationship to return API resource model
      */
-    #[ApiProperty(writable: false, readable: true)]
-    public ?array $_currencies = null;
+    public function currencies(): BelongsToMany
+    {
+        return $this->belongsToMany(Currency::class, 'channel_currencies', 'channel_id', 'currency_id');
+    }
 
     /**
-     * Expose default locale for API with custom name
+     * Override default locale relationship to return API resource model
+     */
+    public function default_locale(): BelongsTo
+    {
+        return $this->belongsTo(Locale::class);
+    }
+
+    /**
+     * Override base currency relationship to return API resource model
+     */
+    public function base_currency(): BelongsTo
+    {
+        return $this->belongsTo(Currency::class);
+    }
+
+    /**
+     * Expose logo URL for API
      */
     #[ApiProperty(writable: false, readable: true)]
-    public ?object $defaultLocaleData = null;
+    public function getLogoUrl(): ?string
+    {
+        return $this->logo_url();
+    }
 
     /**
-     * Expose base currency for API with custom name
+     * Expose favicon URL for API
      */
     #[ApiProperty(writable: false, readable: true)]
-    public ?object $baseCurrencyData = null;
+    public function getFaviconUrl(): ?string
+    {
+        return $this->favicon_url();
+    }
 }
-
-
-

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

@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 use Webkul\Core\Models\Country as BaseCountry;
 
 #[ApiResource(
@@ -19,7 +20,7 @@ use Webkul\Core\Models\Country as BaseCountry;
     ],
     graphQlOperations: [
         new Query(resolver: BaseQueryItemResolver::class),
-        new QueryCollection,
+        new QueryCollection(provider: CursorAwareCollectionProvider::class),
     ]
 )]
 class Country extends BaseCountry

+ 2 - 1
packages/Webkul/BagistoApi/src/Models/Currency.php

@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 #[ApiResource(
     routePrefix: '/api/shop',
@@ -18,7 +19,7 @@ use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
     ],
     graphQlOperations: [
         new Query(resolver: BaseQueryItemResolver::class),
-        new QueryCollection,
+        new QueryCollection(provider: CursorAwareCollectionProvider::class),
     ]
 )]
 class Currency extends \Webkul\Core\Models\Currency

+ 13 - 2
packages/Webkul/BagistoApi/src/Models/CustomerOrderShipmentItem.php

@@ -4,6 +4,10 @@ namespace Webkul\BagistoApi\Models;
 
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\GraphQl\Query;
+use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
@@ -15,8 +19,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
  */
 #[ApiResource(
     shortName: 'CustomerOrderShipmentItem',
-    operations: [],
-    graphQlOperations: [],
+    operations: [
+        new Get,
+        new GetCollection,
+    ],
+    graphQlOperations: [
+        new Query,
+        new QueryCollection,
+    ],
 )]
 class CustomerOrderShipmentItem extends Model
 {
@@ -117,6 +127,7 @@ class CustomerOrderShipmentItem extends Model
         $array['description'] = $this->description;
         $array['qty'] = $this->qty;
         $array['weight'] = $this->weight;
+
         return $array;
     }
 }

+ 2 - 1
packages/Webkul/BagistoApi/src/Models/Locale.php

@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 #[ApiResource(
     routePrefix: '/api/shop',
@@ -18,7 +19,7 @@ use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
     ],
     graphQlOperations: [
         new Query(resolver: BaseQueryItemResolver::class),
-        new QueryCollection,
+        new QueryCollection(provider: CursorAwareCollectionProvider::class),
     ]
 )
 ]

+ 50 - 10
packages/Webkul/BagistoApi/src/Models/Page.php

@@ -8,21 +8,25 @@ use ApiPlatform\Metadata\Get;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
-use ApiPlatform\Metadata\QueryParameter;
-use ApiPlatform\Laravel\Eloquent\Filter\SearchFilter;
-use Webkul\CMS\Models\Page as BasePage;
 use Webkul\BagistoApi\Resolver\PageByUrlKeyResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
+use Webkul\BagistoApi\State\PageProvider;
+use Webkul\CMS\Models\Page as BasePage;
 
 #[ApiResource(
     routePrefix: '/api/shop',
     shortName: 'page',
     operations: [
-        new Get,
-        new GetCollection,
+        new Get(
+            provider: PageProvider::class,
+        ),
+        new GetCollection(
+            provider: PageProvider::class,
+        ),
     ],
     graphQlOperations: [
-        new Query(),
-        new QueryCollection(),
+        new Query(resolver: \Webkul\BagistoApi\Resolver\BaseQueryItemResolver::class),
+        new QueryCollection(provider: CursorAwareCollectionProvider::class),
         new QueryCollection(
             name: 'pageByUrlKey',
             args: [
@@ -36,15 +40,51 @@ use Webkul\BagistoApi\Resolver\PageByUrlKeyResolver;
         ),
     ],
 )]
-#[QueryParameter(key: 'url_key', filter: SearchFilter::class)]
 class Page extends BasePage
 {
     /**
-     * API Platform identifier
+     * Get unique page identifier for API Platform
      */
     #[ApiProperty(identifier: true, writable: false)]
     public function getId(): int
     {
-        return $this->id;
+        return (int) $this->id;
+    }
+
+    /**
+     * Get layout
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public function getLayout(): ?string
+    {
+        return $this->layout;
+    }
+
+    /**
+     * Get created at
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public function getCreatedAt(): ?\DateTime
+    {
+        return $this->created_at;
+    }
+
+    /**
+     * Get updated at
+     */
+    #[ApiProperty(writable: false, readable: true)]
+    public function getUpdatedAt(): ?\DateTime
+    {
+        return $this->updated_at;
+    }
+
+    /**
+     * Get current locale translation for API
+     */
+    #[ApiProperty(readable: true, writable: false, description: 'Current locale translation')]
+    public function getCurrentTranslation(): ?\Illuminate\Database\Eloquent\Model
+    {
+        return $this->translations->firstWhere('locale', app()->getLocale())
+            ?? $this->translations->first();
     }
 }

+ 99 - 18
packages/Webkul/BagistoApi/src/Models/Product.php

@@ -525,6 +525,8 @@ class Product extends BaseProduct
         'cost', 'meta_description', 'length', 'width', 'height',
         'color', 'size', 'brand', 'locale', 'channel', 'description_html',
         'minimum_price', 'maximum_price', 'regular_minimum_price', 'regular_maximum_price',
+        'formatted_price', 'formatted_special_price', 'formatted_minimum_price',
+        'formatted_maximum_price', 'formatted_regular_minimum_price', 'formatted_regular_maximum_price',
         'index', 'combinations', 'super_attribute_options',
         'product_options',
     ];
@@ -1166,6 +1168,7 @@ class Product extends BaseProduct
 
             if (! empty($options)) {
                 $result[] = [
+                    'id'      => $attribute->id,
                     'code'    => $attribute->code,
                     'label'   => $attribute->admin_name,
                     'options' => $options,
@@ -1497,7 +1500,8 @@ class Product extends BaseProduct
 
     public function getPriceAttribute(): ?float
     {
-        return floatval($this->getSystemAttributeValue('price'));
+
+        return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price')));
     }
 
     #[ApiProperty(writable: true, readable: true)]
@@ -1513,7 +1517,9 @@ class Product extends BaseProduct
 
     public function getSpecialPriceAttribute(): ?float
     {
-        return floatval($this->getSystemAttributeValue('special_price'));
+        $value = floatval($this->getSystemAttributeValue('special_price'));
+
+        return $value ? (float) core()->convertPrice($value) : null;
     }
 
     #[ApiProperty(writable: true, readable: true)]
@@ -2001,7 +2007,11 @@ class Product extends BaseProduct
                 }
             }
         }
-        
+          // Fallback to the channel's default locale when the requested locale has no translation
+          $defaultLocale = core()->getCurrentChannel()->default_locale?->code;
+          if ($defaultLocale && ! in_array($defaultLocale, $localeVariants)) {
+              $localeVariants[] = $defaultLocale;
+          }
         $localeVariants[] = null;
         
         $channelVariants = [$currentChannel, null];
@@ -2173,14 +2183,14 @@ class Product extends BaseProduct
                 ->first();
 
             if ($priceIndex) {
-                return floatval($priceIndex->min_price);
+                return (float) core()->convertPrice(floatval($priceIndex->min_price));
             }
 
             // Fallback to base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         } catch (\Exception $e) {
             // If any error occurs, return base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         }
     }
 
@@ -2212,7 +2222,7 @@ class Product extends BaseProduct
             $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
 
             if (! $currentChannel || ! $customerGroup) {
-                return floatval($this->price ?? 0);
+                return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
             }
 
             // Get price index for current channel and customer group
@@ -2222,14 +2232,14 @@ class Product extends BaseProduct
                 ->first();
 
             if ($priceIndex) {
-                return floatval($priceIndex->max_price);
+                return (float) core()->convertPrice(floatval($priceIndex->max_price));
             }
 
             // Fallback to base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         } catch (\Exception $e) {
             // If any error occurs, return base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         }
     }
 
@@ -2261,7 +2271,7 @@ class Product extends BaseProduct
             $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
 
             if (! $currentChannel || ! $customerGroup) {
-                return floatval($this->price ?? 0);
+                return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
             }
 
             // Get price index for current channel and customer group
@@ -2271,14 +2281,14 @@ class Product extends BaseProduct
                 ->first();
 
             if ($priceIndex) {
-                return floatval($priceIndex->regular_min_price);
+                return (float) core()->convertPrice(floatval($priceIndex->regular_min_price));
             }
 
             // Fallback to base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         } catch (\Exception $e) {
             // If any error occurs, return base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         }
     }
 
@@ -2310,7 +2320,7 @@ class Product extends BaseProduct
             $customerGroup = resolve('Webkul\Customer\Repositories\CustomerRepository')->getCurrentGroup();
 
             if (! $currentChannel || ! $customerGroup) {
-                return floatval($this->price ?? 0);
+                return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
             }
 
             // Get price index for current channel and customer group
@@ -2320,14 +2330,14 @@ class Product extends BaseProduct
                 ->first();
 
             if ($priceIndex) {
-                return floatval($priceIndex->regular_max_price);
+                return (float) core()->convertPrice(floatval($priceIndex->regular_max_price));
             }
 
             // Fallback to base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         } catch (\Exception $e) {
             // If any error occurs, return base price
-            return floatval($this->price ?? 0);
+            return (float) core()->convertPrice(floatval($this->getSystemAttributeValue('price') ?? 0));
         }
     }
 
@@ -2340,4 +2350,75 @@ class Product extends BaseProduct
     {
         return $this->getRegularMaximumPriceAttribute();
     }
+    // ─── Formatted Price Accessors ──────────────────────────────────────
+
+    public function getFormattedPriceAttribute(): ?string
+    {
+        $price = $this->getPriceAttribute();
+
+        return $price !== null ? core()->formatPrice($price) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true, required: false)]
+    public function getFormatted_price(): ?string
+    {
+        return $this->getFormattedPriceAttribute();
+    }
+
+    public function getFormattedSpecialPriceAttribute(): ?string
+    {
+        $specialPrice = $this->getSpecialPriceAttribute();
+
+        return $specialPrice ? core()->formatPrice($specialPrice) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true, required: false)]
+    public function getFormatted_special_price(): ?string
+    {
+        return $this->getFormattedSpecialPriceAttribute();
+    }
+
+    public function getFormattedMinimumPriceAttribute(): ?string
+    {
+        return core()->formatPrice($this->getMinimumPriceAttribute());
+    }
+
+    #[ApiProperty(writable: false, readable: true, required: false)]
+    public function getFormatted_minimum_price(): ?string
+    {
+        return $this->getFormattedMinimumPriceAttribute();
+    }
+
+    public function getFormattedMaximumPriceAttribute(): ?string
+    {
+        return core()->formatPrice($this->getMaximumPriceAttribute());
+    }
+
+    #[ApiProperty(writable: false, readable: true, required: false)]
+    public function getFormatted_maximum_price(): ?string
+    {
+        return $this->getFormattedMaximumPriceAttribute();
+    }
+
+    public function getFormattedRegularMinimumPriceAttribute(): ?string
+    {
+        return core()->formatPrice($this->getRegularMinimumPriceAttribute());
+    }
+
+    #[ApiProperty(writable: false, readable: true, required: false)]
+    public function getFormatted_regular_minimum_price(): ?string
+    {
+        return $this->getFormattedRegularMinimumPriceAttribute();
+    }
+
+    public function getFormattedRegularMaximumPriceAttribute(): ?string
+    {
+        return core()->formatPrice($this->getRegularMaximumPriceAttribute());
+    }
+
+    #[ApiProperty(writable: false, readable: true, required: false)]
+    public function getFormatted_regular_maximum_price(): ?string
+    {
+        return $this->getFormattedRegularMaximumPriceAttribute();
+    }
 }

+ 2 - 1
packages/Webkul/BagistoApi/src/Models/ProductBundleOption.php

@@ -12,13 +12,14 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
 use Symfony\Component\Serializer\Annotation\Groups;
 use Webkul\Product\Models\ProductBundleOption as BaseProductBundleOption;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 #[ApiResource(
     routePrefix: '/api/shop',
     operations: [
     ],
     graphQlOperations: [
-        new QueryCollection,
+        new QueryCollection(provider: CursorAwareCollectionProvider::class),
         new Query(resolver: BaseQueryItemResolver::class),
     ]
 )]

+ 2 - 0
packages/Webkul/BagistoApi/src/Models/ProductCustomerGroupPrice.php

@@ -18,6 +18,7 @@ use Webkul\BagistoApi\State\ProductCustomerGroupPriceProcessor;
 use Webkul\BagistoApi\State\ProductCustomerGroupPriceProvider;
 use Webkul\Product\Models\ProductCustomerGroupPrice as BaseProductCustomerGroupPrice;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 #[ApiResource(
     routePrefix: '/api/admin',
@@ -66,6 +67,7 @@ use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
     ],
     graphQlOperations: [
         new QueryCollection(
+            provider: CursorAwareCollectionProvider::class,
             args: [
                 'product_id' => ['type' => 'Int', 'description' => 'Filter by product ID'],
             ]

+ 16 - 0
packages/Webkul/BagistoApi/src/Models/ProductCustomizableOptionPrice.php

@@ -49,6 +49,11 @@ class ProductCustomizableOptionPrice extends BaseProductCustomizableOptionPrice
         return $this->label;
     }
 
+    public function getPriceAttribute($value)
+    {
+        return $value !== null ? (float) core()->convertPrice((float) $value) : null;
+    }
+
     /**
      * Get price
      */
@@ -62,6 +67,17 @@ class ProductCustomizableOptionPrice extends BaseProductCustomizableOptionPrice
         return $this->price ? (float) $this->price : null;
     }
 
+    public function getFormattedPriceAttribute(): ?string
+    {
+        return $this->price !== null ? core()->formatPrice($this->price) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true)]
+    public function getFormatted_price(): ?string
+    {
+        return $this->getFormattedPriceAttribute();
+    }
+
     /**
      * Get sort_order
      */

+ 29 - 13
packages/Webkul/BagistoApi/src/Models/ProductDownloadableLink.php

@@ -12,9 +12,9 @@ use ApiPlatform\Metadata\Link;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Support\Facades\Storage;
 use Symfony\Component\Serializer\Annotation\Groups;
+use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
 use Webkul\BagistoApi\State\DownloadableLinksProvider;
 use Webkul\Product\Models\ProductDownloadableLink as BaseProductDownloadableLink;
-use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
 
 #[ApiResource(
     routePrefix: '/api/shop',
@@ -57,6 +57,14 @@ class ProductDownloadableLink extends BaseProductDownloadableLink
         return $array;
     }
 
+    /**
+     * Eloquent accessor to convert price to the selected currency.
+     */
+    public function getPriceAttribute($value): ?float
+    {
+        return $value !== null ? (float) core()->convertPrice((float) $value) : null;
+    }
+
     #[ApiProperty(identifier: true, writable: false)]
     public function getId(): ?int
     {
@@ -74,7 +82,9 @@ class ProductDownloadableLink extends BaseProductDownloadableLink
     }
 
     /**
-     * Override parent's sample_file_url to handle null values properly
+     * Override parent's sample_file_url to return the REST download endpoint.
+     * Sample files for links are stored on the private disk,
+     * so a direct public Storage URL would 404.
      */
     public function sample_file_url(): string
     {
@@ -82,14 +92,7 @@ class ProductDownloadableLink extends BaseProductDownloadableLink
             return '';
         }
 
-        $url = Storage::url($this->sample_file);
-
-        // Ensure full URL with domain if not already absolute
-        if (! $this->isAbsoluteUrl($url)) {
-            $url = config('app.url').$url;
-        }
-
-        return $url;
+        return url('/api/downloadable/download-sample/link/'.$this->id);
     }
 
     #[ApiProperty(writable: true, readable: true)]
@@ -115,6 +118,19 @@ class ProductDownloadableLink extends BaseProductDownloadableLink
         $this->price = $value;
     }
 
+    public function getFormattedPriceAttribute(): ?string
+    {
+        $price = $this->getPrice();
+
+        return $price !== null ? core()->formatPrice($price) : null;
+    }
+
+    #[ApiProperty(writable: false, readable: true)]
+    public function getFormatted_price(): ?string
+    {
+        return $this->getFormattedPriceAttribute();
+    }
+
     #[ApiProperty(writable: true, readable: true)]
     #[Groups(['mutation'])]
     public function getUrl(): ?string
@@ -127,7 +143,7 @@ class ProductDownloadableLink extends BaseProductDownloadableLink
         $this->url = $value;
     }
 
-    #[ApiProperty(writable: true, readable: true)]
+    #[ApiProperty(writable: true, readable: false)]
     #[Groups(['mutation'])]
     public function getFile(): ?string
     {
@@ -139,7 +155,7 @@ class ProductDownloadableLink extends BaseProductDownloadableLink
         $this->file = $value;
     }
 
-    #[ApiProperty(writable: true, readable: true)]
+    #[ApiProperty(writable: true, readable: false)]
     #[Groups(['mutation'])]
     public function getFileName(): ?string
     {
@@ -246,7 +262,7 @@ class ProductDownloadableLink extends BaseProductDownloadableLink
         $this->product_id = $value;
     }
 
-    #[ApiProperty(writable: false, readable: true)]
+    #[ApiProperty(writable: false, readable: false)]
     public function getFileUrl(): ?string
     {
         if (! $this->file) {

+ 3 - 11
packages/Webkul/BagistoApi/src/Models/ProductDownloadableSample.php

@@ -10,11 +10,10 @@ use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use ApiPlatform\Metadata\Link;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
-use Illuminate\Support\Facades\Storage;
 use Symfony\Component\Serializer\Annotation\Groups;
+use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
 use Webkul\BagistoApi\State\DownloadableSamplesProvider;
 use Webkul\Product\Models\ProductDownloadableSample as BaseProductDownloadableSample;
-use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
 
 #[ApiResource(
     routePrefix: '/api/shop',
@@ -74,7 +73,7 @@ class ProductDownloadableSample extends BaseProductDownloadableSample
     }
 
     /**
-     * Get the file URL with full domain.
+     * Get the file URL via the REST download endpoint.
      */
     public function file_url(): ?string
     {
@@ -82,14 +81,7 @@ class ProductDownloadableSample extends BaseProductDownloadableSample
             return null;
         }
 
-        $url = Storage::url($this->file);
-
-        // Ensure full URL with domain if not already absolute
-        if (! $this->isAbsoluteUrl($url)) {
-            $url = config('app.url').$url;
-        }
-
-        return $url;
+        return url('/api/downloadable/download-sample/sample/'.$this->id);
     }
 
     #[ApiProperty(writable: true, readable: true)]

+ 2 - 0
packages/Webkul/BagistoApi/src/Models/ProductImage.php

@@ -18,6 +18,7 @@ use Webkul\BagistoApi\State\ProductImageProcessor;
 use Webkul\BagistoApi\State\ProductImageProvider;
 use Webkul\Product\Models\ProductImage as BaseProductImage;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 #[ApiResource(
     routePrefix: '/api/admin',
@@ -66,6 +67,7 @@ use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
     ],
     graphQlOperations: [
         new QueryCollection(
+            provider: CursorAwareCollectionProvider::class,
             args: [
                 'product_id' => ['type' => 'Int', 'description' => 'Filter by product ID'],
             ]

+ 2 - 0
packages/Webkul/BagistoApi/src/Models/ProductVideo.php

@@ -15,6 +15,7 @@ use ApiPlatform\Metadata\Link;
 use ApiPlatform\Metadata\Patch;
 use Webkul\Product\Models\ProductVideo as BaseProductVideo;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 #[ApiResource(
     routePrefix: '/api/shop',
@@ -57,6 +58,7 @@ use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
     ],
     graphQlOperations: [
         new QueryCollection(
+            provider: CursorAwareCollectionProvider::class,
             args: [
                 'product_id' => ['type' => 'Int', 'description' => 'Filter by product ID'],
             ]

+ 2 - 1
packages/Webkul/BagistoApi/src/Models/ThemeCustomization.php

@@ -12,6 +12,7 @@ use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use ApiPlatform\Metadata\QueryParameter;
 use Illuminate\Database\Eloquent\Model;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\CursorAwareCollectionProvider;
 
 #[ApiResource(
     routePrefix: '/api/shop',
@@ -35,7 +36,7 @@ use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
     ],
     graphQlOperations: [
         new Query(resolver: BaseQueryItemResolver::class),
-        new QueryCollection,
+        new QueryCollection(provider: CursorAwareCollectionProvider::class),
     ],
 )]
 #[QueryParameter(key: 'type', filter: EqualsFilter::class)]

+ 64 - 1
packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php

@@ -34,6 +34,8 @@ use Webkul\BagistoApi\Serializer\TokenHeaderDenormalizer;
 use Webkul\BagistoApi\Services\CartTokenService;
 use Webkul\BagistoApi\Services\StorefrontKeyService;
 use Webkul\BagistoApi\Services\TokenHeaderService;
+use Webkul\BagistoApi\State\BookingSlotProvider;
+use Webkul\BagistoApi\State\PageProvider;
 use Webkul\BagistoApi\State\AttributeCollectionProvider;
 use Webkul\BagistoApi\State\AttributeOptionCollectionProvider;
 use Webkul\BagistoApi\State\AttributeOptionQueryProvider;
@@ -103,7 +105,9 @@ class BagistoApiServiceProvider extends ServiceProvider
         $this->app->singleton(IterableType::class);
         $this->app->tag(IterableType::class, 'api_platform.graphql.type');
 
-        $this->overrideApiPlatformLinksHandler();
+        // $this->overrideApiPlatformLinksHandler();
+        $this->registerSnakeCaseLinksHandlerFix();
+
 
         $this->app->singleton(StorefrontKeyService::class, function ($app) {
             return new StorefrontKeyService;
@@ -279,6 +283,9 @@ class BagistoApiServiceProvider extends ServiceProvider
         $this->app->tag(CountryStateCollectionProvider::class, ProviderInterface::class);
         $this->app->tag(CountryStateQueryProvider::class, ProviderInterface::class);
         $this->app->tag(CategoryTreeProvider::class, ProviderInterface::class);
+        $this->app->tag(BookingSlotProvider::class, ProviderInterface::class);
+        $this->app->tag(PageProvider::class, ProviderInterface::class);
+        $this->app->tag(\Webkul\BagistoApi\State\CursorAwareCollectionProvider::class, ProviderInterface::class);
         $this->app->tag(WishlistProvider::class, ProviderInterface::class);
         $this->app->tag(CompareItemProvider::class, ProviderInterface::class);
         $this->app->tag(CustomerReviewProvider::class, ProviderInterface::class);
@@ -602,6 +609,15 @@ class BagistoApiServiceProvider extends ServiceProvider
             ->where('id', '[0-9]+')
             ->middleware(['Webkul\BagistoApi\Http\Middleware\VerifyStorefrontKey'])
             ->name('bagistoapi.customer-invoice-pdf');
+        \Illuminate\Support\Facades\Route::get('/api/downloadable/download-sample/{type}/{id}', \Webkul\BagistoApi\Http\Controllers\DownloadSampleController::class)
+            ->where('type', 'link|sample')
+            ->where('id', '[0-9]+')
+            ->name('bagistoapi.download-sample');
+
+        \Illuminate\Support\Facades\Route::get('/api/shop/customer-downloadable-products/{id}/download', \Webkul\BagistoApi\Http\Controllers\DownloadablePurchasedController::class)
+            ->where('id', '[0-9]+')
+            ->middleware(['Webkul\BagistoApi\Http\Middleware\VerifyStorefrontKey'])
+            ->name('bagistoapi.customer-downloadable-product-download');
     }
 
     /**
@@ -679,7 +695,54 @@ class BagistoApiServiceProvider extends ServiceProvider
             \Webkul\BagistoApi\Console\Commands\ApiKeyMaintenanceCommand::class,
         ]);
     }
+    /**
+     * Override API Platform's ItemProvider and CollectionProvider to wrap the
+     * LinksHandler with SnakeCaseLinksHandler, fixing the camelCase/snake_case
+     * mismatch between GraphQL field names and Eloquent relationship names.
+     */
+    protected function registerSnakeCaseLinksHandlerFix(): void
+    {
+        $this->app->extend(
+            \ApiPlatform\Laravel\Eloquent\State\ItemProvider::class,
+            function ($original, $app) {
+                $linksHandler = new \Webkul\BagistoApi\State\SnakeCaseLinksHandler(
+                    new \ApiPlatform\Laravel\Eloquent\State\LinksHandler(
+                        $app,
+                        $app->make(\ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface::class)
+                    )
+                );
+
+                $tagged = iterator_to_array($app->tagged(\ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface::class));
+
+                return new \ApiPlatform\Laravel\Eloquent\State\ItemProvider(
+                    $linksHandler,
+                    new \ApiPlatform\Laravel\ServiceLocator($tagged),
+                    $app->tagged(\ApiPlatform\Laravel\Eloquent\State\QueryExtensionInterface::class)
+                );
+            }
+        );
+
+        $this->app->extend(
+            \ApiPlatform\Laravel\Eloquent\State\CollectionProvider::class,
+            function ($original, $app) {
+                $linksHandler = new \Webkul\BagistoApi\State\SnakeCaseLinksHandler(
+                    new \ApiPlatform\Laravel\Eloquent\State\LinksHandler(
+                        $app,
+                        $app->make(\ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface::class)
+                    )
+                );
 
+                $tagged = iterator_to_array($app->tagged(\ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface::class));
+
+                return new \ApiPlatform\Laravel\Eloquent\State\CollectionProvider(
+                    $app->make(\ApiPlatform\State\Pagination\Pagination::class),
+                    $linksHandler,
+                    $app->tagged(\ApiPlatform\Laravel\Eloquent\State\QueryExtensionInterface::class),
+                    new \ApiPlatform\Laravel\ServiceLocator($tagged)
+                );
+            }
+        );
+    }
     /**
      * Check if the package is running as a vendor package.
      */

+ 2 - 1
packages/Webkul/BagistoApi/src/Resolver/ProductCollectionResolver.php

@@ -23,7 +23,8 @@ class ProductCollectionResolver implements QueryCollectionResolverInterface
             $query->where(function ($q) use ($searchTerm) {
                 $q->where('sku', 'like', "%{$searchTerm}%")
                     ->orWhereHas('attribute_values', function ($attrQuery) use ($searchTerm) {
-                        $attrQuery->where('text_value', 'like', "%{$searchTerm}%");
+                        $attrQuery->where('attribute_id', 2)
+                            ->where('text_value', 'like', "%{$searchTerm}%");
                     });
             });
         }

+ 16 - 10
packages/Webkul/BagistoApi/src/Resources/lang/en/app.php

@@ -1,7 +1,7 @@
 <?php
 
 return [
-    
+
     'graphql' => [
         'cart' => [
             'authentication-required'           => 'Authentication token is required',
@@ -19,12 +19,16 @@ return [
             'cart-item-id-quantity-required'    => 'Cart item ID and quantity are required',
             'cart-item-id-required'             => 'Cart item ID is required',
             'item-ids-required'                 => 'Item IDs array is required',
-            'coupon-code-required'              => 'Coupon code is required',
-            'address-data-required'             => 'Country, state, and postcode are required',
-            'grouped-qty-required'             => 'Grouped product requires selected quantities. Pass groupedQty as JSON string, e.g. {"101":2,"102":1}.',
-            'grouped-qty-must-include-all'     => 'Grouped product requires quantities for all associated products. Missing IDs: :ids.',
-            'grouped-qty-invalid-associated'   => 'Grouped product quantities contain invalid associated product IDs: :ids.',
-            'grouped-qty-invalid-quantity'     => 'Invalid quantity provided for associated product ID :id. Quantity must be a non-negative integer.',
+
+            'event-booking-quantity-not-changeable'       => 'Event booking product quantity cannot be changed. Quantity is determined by ticket selection.',
+            'appointment-booking-quantity-not-changeable' => 'Appointment booking product quantity cannot be changed.',
+            'coupon-code-required'                        => 'Coupon code is required',
+            'address-data-required'                       => 'Country, state, and postcode are required',
+            'grouped-qty-required'                        => 'Grouped product requires selected quantities. Pass groupedQty as JSON string, e.g. {"101":2,"102":1}.',
+            'grouped-qty-must-include-all'                => 'Grouped product requires quantities for all associated products. Missing IDs: :ids.',
+            'grouped-qty-invalid-associated'              => 'Grouped product quantities contain invalid associated product IDs: :ids.',
+            'grouped-qty-invalid-quantity'                => 'Invalid quantity provided for associated product ID :id. Quantity must be a non-negative integer.',
+            'bundle-qty-not-changeable'                   => 'Quantity for bundle option ":option" cannot be changed. The allowed quantity is :qty.',
 
             'add-product-failed'                => 'Failed to add product to cart',
             'update-item-failed'                => 'Failed to update cart item',
@@ -35,6 +39,7 @@ return [
             'estimate-shipping-failed'          => 'Failed to estimate shipping',
 
             'product-added-successfully'         => 'Product added to cart successfully',
+            'cart-item-updated-successfully'     => 'Cart item updated successfully',
             'guest-cart-merged'                  => 'Guest cart merged successfully',
             'using-authenticated-cart'           => 'Using authenticated customer cart',
             'cart-item-not-found'                => 'Cart item not found',
@@ -92,7 +97,7 @@ return [
         'customer-profile' => [
             'authentication-required'           => 'Authentication token is required. Please provide token in query input',
             'invalid-token'                     => 'Invalid or expired token',
-            'profile-updated'                  => 'Customer profile updated successfully',
+            'profile-updated'                   => 'Customer profile updated successfully',
         ],
 
         'customer' => [
@@ -102,7 +107,7 @@ return [
             'id-required'                       => 'Customer ID is required',
             'invalid-id-format'                 => 'Invalid ID format. Expected IRI format like "/api/admin/customers/1" or numeric ID',
             'not-found'                         => 'Customer not found',
-            'phone-special-chars-not-allowed'  => 'Mobile number can only contain digits. Special characters are not allowed',
+            'phone-special-chars-not-allowed'   => 'Mobile number can only contain digits. Special characters are not allowed',
             'invalid-gender'                    => 'Invalid gender value ":gender". Allowed values are: :valid',
         ],
 
@@ -327,12 +332,13 @@ return [
             'product-id-required'               => 'Product ID is required',
             'customer-id-required'              => 'Customer ID is required',
             'product-not-found'                 => 'Product not found',
+            'product-disabled'                  => 'This product is currently disabled',
             'customer-not-found'                => 'Customer not found',
             'already-exists'                    => 'This product is already in your wishlist',
             'removed'                           => 'Item Successfully Removed From Wishlist',
             'product-removed'                   => 'Product has been removed',
             'wishlist-item-id-required'         => 'Wishlist item ID is required',
-            'invalid-quantity'                  => 'Quantity must be greater than 0',            
+            'invalid-quantity'                  => 'Quantity must be greater than 0',
             'move-to-cart-missing-options'      => 'Product has missing required options. Please configure it manually',
             'moved-to-cart-success'             => 'Item moved to cart successfully',
             'delete-all-success'                => 'All wishlist items have been removed successfully',

+ 14 - 1
packages/Webkul/BagistoApi/src/Routing/CustomIriConverter.php

@@ -19,6 +19,19 @@ class CustomIriConverter implements IriConverterInterface
 
     public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string
     {
+        // Handle non-model API resources that shouldn't generate IRIs
+        if (is_object($resource)) {
+            $className = class_basename($resource::class);
+            if (in_array($className, ['BookingSlot', 'CartToken', 'AddProductInCart'])) {
+                return null;
+            }
+        } elseif (is_string($resource) && class_exists($resource)) {
+            $className = class_basename($resource);
+            if (in_array($className, ['CartToken', 'AddProductInCart', 'BookingSlot'])) {
+                return null;
+            }
+        }
+
         if ($resource instanceof Model || (is_string($resource) && class_exists($resource) && is_subclass_of($resource, Model::class))) {
             try {
                 $resourceClass = is_string($resource) ? $resource : $resource::class;
@@ -51,7 +64,7 @@ class CustomIriConverter implements IriConverterInterface
 
         if ($resourceClass) {
             $className = class_basename($resourceClass);
-            if (in_array($className, ['CartToken', 'AddProductInCart'])) {
+            if (in_array($className, ['CartToken', 'AddProductInCart', 'BookingSlot'])) {
                 return new \stdClass;
             }
         }

+ 33 - 32
packages/Webkul/BagistoApi/src/State/AttributeCollectionProvider.php

@@ -6,6 +6,7 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Webkul\BagistoApi\Models\Attribute;
 
 class AttributeCollectionProvider implements ProviderInterface
@@ -34,44 +35,44 @@ class AttributeCollectionProvider implements ProviderInterface
             $perPage = $defaultPerPage;
         }
 
-        $query = Attribute::query();
+        $offset = 0;
 
-        // Handle cursor-based pagination
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '>', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '<', $beforeId);
-            // For 'before', we need to reverse order, paginate, then reverse results
-            $query->orderBy('id', 'desc');
-            $laravelPaginator = $query->paginate($perPage);
-
-            // Reverse the items to maintain proper cursor order
-            $items = $laravelPaginator->items();
-            $items = array_reverse($items);
-
-            // Load relations with translations
-            foreach ($items as $item) {
-                $item->load(['options', 'translations', 'options.translations']);
-            }
-
-            // Update items in paginator
-            $laravelPaginator->setCollection(collect($items));
-
-            return new Paginator($laravelPaginator);
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
         }
 
-        // Load relations with translations
-        $query->with(['options', 'translations', 'options.translations']);
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
+        }
+
+        $query = Attribute::query()
+            ->with(['options', 'translations', 'translation', 'options.translations', 'options.translation'])
+            ->orderBy('id', 'asc');
 
-        // Order by ID for consistent pagination (ascending for after/default)
-        $query->orderBy('id', 'asc');
+        $total = (clone $query)->count();
 
-        // Paginate with the specified per page amount
-        $laravelPaginator = $query->paginate($perPage);
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
+        }
 
-        // Return API Platform paginator which handles totalCount correctly
-        return new Paginator($laravelPaginator);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 33 - 32
packages/Webkul/BagistoApi/src/State/AttributeOptionCollectionProvider.php

@@ -6,6 +6,7 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Webkul\BagistoApi\Models\AttributeOption;
 
@@ -57,44 +58,44 @@ class AttributeOptionCollectionProvider implements ProviderInterface
             $perPage = $defaultPerPage;
         }
 
-        $query = AttributeOption::where('attribute_id', $attributeId)
-            ->orderBy('sort_order', 'asc');
+        $offset = 0;
 
-        // Handle cursor-based pagination
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '>', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '<', $beforeId);
-            // For 'before', we need to reverse order, paginate, then reverse results
-            $query->orderBy('id', 'desc');
-            $laravelPaginator = $query->paginate($perPage);
-
-            // Reverse the items to maintain proper cursor order
-            $items = $laravelPaginator->items();
-            $items = array_reverse($items);
-
-            // Load relations
-            foreach ($items as $item) {
-                $item->load('translations');
-            }
-
-            // Update items in paginator
-            $laravelPaginator->setCollection(collect($items));
-
-            return $laravelPaginator;
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
-        // Default order
-        $query->orderBy('sort_order', 'asc');
-        $laravelPaginator = $query->paginate($perPage);
+        $query = AttributeOption::where('attribute_id', $attributeId)
+            ->with('translations')
+            ->orderBy('sort_order', 'asc');
+
+        $total = (clone $query)->count();
 
-        // Load relations
-        foreach ($laravelPaginator as $item) {
-            $item->load('translations');
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
         }
 
-        return $laravelPaginator;
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 178 - 0
packages/Webkul/BagistoApi/src/State/BookingSlotProvider.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Webkul\BagistoApi\Models\BookingSlot;
+use Webkul\BookingProduct\Helpers\AppointmentSlot as AppointmentSlotHelper;
+use Webkul\BookingProduct\Helpers\DefaultSlot as DefaultSlotHelper;
+use Webkul\BookingProduct\Helpers\EventTicket as EventTicketHelper;
+use Webkul\BookingProduct\Helpers\RentalSlot as RentalSlotHelper;
+use Webkul\BookingProduct\Helpers\TableSlot as TableSlotHelper;
+use Webkul\BookingProduct\Repositories\BookingProductRepository;
+
+/**
+ * Provider for fetching booking slots via GraphQL
+ *
+ * Handles the bookingSlot query that accepts:
+ * - id: The booking product ID
+ * - date: The date for which to fetch available slots
+ */
+class BookingSlotProvider implements ProviderInterface
+{
+    /**
+     * Booking type to helper mapping
+     */
+    protected array $bookingHelpers = [];
+
+    public function __construct(
+        protected BookingProductRepository $bookingProductRepository,
+        protected DefaultSlotHelper $defaultSlotHelper,
+        protected AppointmentSlotHelper $appointmentSlotHelper,
+        protected RentalSlotHelper $rentalSlotHelper,
+        protected EventTicketHelper $eventTicketHelper,
+        protected TableSlotHelper $tableSlotHelper
+    ) {
+        $this->bookingHelpers = [
+            'default'     => $this->defaultSlotHelper,
+            'appointment' => $this->appointmentSlotHelper,
+            'rental'      => $this->rentalSlotHelper,
+            'event'       => $this->eventTicketHelper,
+            'table'       => $this->tableSlotHelper,
+        ];
+    }
+
+    /**
+     * Provide booking slots for GraphQL query
+     */
+    public function provide(
+        Operation $operation,
+        array $uriVariables = [],
+        array $context = []
+    ): ?array {
+        // Get arguments from GraphQL query
+        $args = $context['args'] ?? [];
+
+        $id = $args['id'] ?? null;
+        $date = $args['date'] ?? null;
+
+        if ($id === null) {
+            throw new BadRequestHttpException(
+                'bagistoapi::app.graphql.booking-slot.id-required'
+            );
+        }
+
+        if ($date === null) {
+            throw new BadRequestHttpException(
+                'bagistoapi::app.graphql.booking-slot.date-required'
+            );
+        }
+
+        // Find the booking product
+        $bookingProduct = $this->bookingProductRepository->find($id);
+
+        if (! $bookingProduct) {
+            throw new BadRequestHttpException(
+                'bagistoapi::app.graphql.booking-slot.product-not-found'
+            );
+        }
+
+        // Check if the booking type has a helper
+        if (! isset($this->bookingHelpers[$bookingProduct->type])) {
+            throw new BadRequestHttpException(
+                'bagistoapi::app.graphql.booking-slot.invalid-type'
+            );
+        }
+
+        // Get slots for the given date using the appropriate helper
+        $slots = $this->bookingHelpers[$bookingProduct->type]->getSlotsByDate($bookingProduct, $date);
+
+        if ($bookingProduct->type === 'rental') {
+            return $this->buildRentalSlots($slots);
+        }
+
+        return $this->buildFlatSlots($slots);
+    }
+
+    /**
+     * Build grouped slot response for rental hourly booking type.
+     *
+     * Each BookingSlot represents an admin-configured time range group
+     * (e.g., "10:00 AM - 12:00 PM") with a nested `slots` array
+     * containing the individual hourly sub-slots.
+     *
+     * Raw helper structure:
+     * [
+     *   ['time' => '10:00 AM - 12:00 PM', 'slots' => [['from', 'to', 'from_timestamp', 'to_timestamp', 'qty'], ...]],
+     *   ['time' => '12:00 PM - 09:00 PM', 'slots' => [...]],
+     * ]
+     */
+    private function buildRentalSlots(array $rawSlots): array
+    {
+        $result = [];
+        $index = 1;
+
+        foreach ($rawSlots as $group) {
+            if (! isset($group['slots']) || empty($group['slots'])) {
+                continue;
+            }
+
+            $subSlots = [];
+
+            foreach ($group['slots'] as $slot) {
+                $subSlots[] = [
+                    'from'      => $slot['from'] ?? null,
+                    'to'        => $slot['to'] ?? null,
+                    'timestamp' => $this->buildRentalTimestamp($slot),
+                    'qty'       => isset($slot['qty']) ? (string) $slot['qty'] : null,
+                ];
+            }
+
+            $result[] = new BookingSlot(
+                slotId: (string) $index++,
+                time: $group['time'] ?? null,
+                slots: $subSlots,
+            );
+        }
+
+        return $result;
+    }
+
+    /**
+     * Build flat slot response for default, appointment, table, and event types.
+     *
+     * Each BookingSlot is a single time slot with from/to/timestamp/qty.
+     */
+    private function buildFlatSlots(array $rawSlots): array
+    {
+        $result = [];
+        $index = 1;
+
+        foreach ($rawSlots as $slot) {
+            $result[] = new BookingSlot(
+                slotId: $slot['timestamp'] ?? (string) $index++,
+                from: $slot['from'] ?? null,
+                to: $slot['to'] ?? null,
+                timestamp: $slot['timestamp'] ?? null,
+                qty: isset($slot['qty']) ? (string) $slot['qty'] : null,
+            );
+        }
+
+        return $result;
+    }
+
+    /**
+     * Build "from_timestamp-to_timestamp" string for rental slots.
+     */
+    private function buildRentalTimestamp(array $slot): ?string
+    {
+        $from = $slot['from_timestamp'] ?? '';
+        $to = $slot['to_timestamp'] ?? '';
+
+        $timestamp = $from.'-'.$to;
+
+        return $timestamp !== '-' ? $timestamp : null;
+    }
+}

+ 50 - 0
packages/Webkul/BagistoApi/src/State/CancelOrderProcessor.php

@@ -5,6 +5,7 @@ namespace Webkul\BagistoApi\State;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Request;
 use Webkul\BagistoApi\Dto\CancelOrderInput;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\InvalidInputException;
@@ -34,6 +35,8 @@ class CancelOrderProcessor implements ProcessorInterface
     public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
     {
         if ($data instanceof CancelOrderInput) {
+            $this->hydrateInputFromContext($data, $context);
+
             return $this->handleCancel($data);
         }
 
@@ -86,4 +89,51 @@ class CancelOrderProcessor implements ProcessorInterface
             status: $order->status,
         );
     }
+
+    /**
+     * GraphQL input can reach the processor in slightly different shapes depending on
+     * whether the client sends variables or an inline literal. Normalize those shapes
+     * into the DTO before validation.
+     */
+    private function hydrateInputFromContext(CancelOrderInput $data, array $context): void
+    {
+        if (! empty($data->orderId)) {
+            return;
+        }
+
+        $input = $context['args']['input'] ?? $context['args'] ?? null;
+
+        $orderId = $this->extractOrderId($input);
+
+        if ($orderId === null) {
+            $request = Request::instance();
+
+            if ($request) {
+                $orderId = $this->extractOrderId($request->input('variables.input'))
+                    ?? $this->extractOrderId($request->input('input'))
+                    ?? $this->extractOrderId($request->input('extensions.variables.input'));
+            }
+        }
+
+        if ($orderId !== null) {
+            $data->orderId = $orderId;
+        }
+    }
+
+    private function extractOrderId(mixed $input): ?int
+    {
+        if (is_array($input)) {
+            $value = $input['orderId'] ?? $input['order_id'] ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        if (is_object($input)) {
+            $value = $input->orderId ?? $input->order_id ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        return null;
+    }
 }

+ 47 - 0
packages/Webkul/BagistoApi/src/State/CartProcessor.php

@@ -73,6 +73,12 @@ class CartProcessor implements ProcessorInterface
      */
     private function handleUpdateCartItem($data): CartModel
     {
+        $cart = Cart::getCart();
+
+        if ($cart) {
+            $this->guardBookingCartItemUpdate($cart, $data);
+        }
+
         try {
             Cart::updateItems((array) $data);
             Cart::collectTotals();
@@ -83,6 +89,47 @@ class CartProcessor implements ProcessorInterface
         }
     }
 
+    /**
+     * Prevent quantity updates for booking products that don't allow it.
+     *
+     * - Event booking: quantity is determined by ticket selection, not changeable after add-to-cart.
+     * - Appointment booking: always quantity 1, cannot be changed.
+     */
+    private function guardBookingCartItemUpdate($cart, $data): void
+    {
+        $qtyData = is_object($data) ? (array) $data : $data;
+        $qtyUpdates = $qtyData['qty'] ?? [];
+
+        foreach ($qtyUpdates as $cartItemId => $qty) {
+            $cartItem = $cart->items->firstWhere('id', (int) $cartItemId);
+
+            if (! $cartItem || $cartItem->type !== 'booking') {
+                continue;
+            }
+
+            // Check additional.booking.type first, then fall back to DB lookup
+            $bookingType = $cartItem->additional['booking']['type'] ?? null;
+
+            if (! $bookingType) {
+                $bookingType = \Webkul\BookingProduct\Models\BookingProduct::query()
+                    ->where('product_id', $cartItem->product_id)
+                    ->value('type');
+            }
+
+            if ($bookingType === 'event') {
+                throw new InvalidInputException(
+                    'Event booking product quantity cannot be changed. Quantity is determined by ticket selection.'
+                );
+            }
+
+            if ($bookingType === 'appointment') {
+                throw new InvalidInputException(
+                    'Appointment booking product quantity cannot be changed.'
+                );
+            }
+        }
+    }
+
     /**
      * Handle remove from cart (from CartController->destroy).
      */

+ 4 - 2
packages/Webkul/BagistoApi/src/State/CartTokenMutationProvider.php

@@ -2,6 +2,8 @@
 
 namespace Webkul\BagistoApi\State;
 
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProviderInterface;
 use Webkul\BagistoApi\Dto\CartData;
@@ -30,7 +32,7 @@ class CartTokenMutationProvider implements ProviderInterface
         $operationName = $operation->getName();
 
         // Handle collection operation (get all carts for authenticated user)
-        if ($operationName === 'get_collection') {
+        if ($operation instanceof GetCollection || $operationName === 'get_collection' || str_ends_with($operationName, '_get_collection')) {
             $request = $context['request'] ?? null;
             $token = null;
 
@@ -55,7 +57,7 @@ class CartTokenMutationProvider implements ProviderInterface
         }
 
         // Handle single item operation (get single cart)
-        if ($operationName === 'get') {
+        if ($operation instanceof Get || $operationName === 'get' || str_ends_with($operationName, '_get')) {
             $id = $uriVariables['id'] ?? null;
             if ($id) {
                 $cart = $this->cartRepository->find($id);

+ 162 - 14
packages/Webkul/BagistoApi/src/State/CartTokenProcessor.php

@@ -61,10 +61,8 @@ class CartTokenProcessor implements ProcessorInterface
 
         $customer = $token ? $this->getCustomerFromToken($token) : null;
 
-        
         $cart = $this->resolveCart($operationName, $data, $customer, $token);
-        
-        
+
         return $this->executeOperation($operationName, $cart, $customer, $data);
     }
 
@@ -136,7 +134,7 @@ class CartTokenProcessor implements ProcessorInterface
         // Handle GraphQL mutation input - data is wrapped in 'input' key
         if (isset($context['args']['input']) && is_array($context['args']['input'])) {
             $inputData = $context['args']['input'];
-            
+
             // Map input fields to CartInput object
             foreach ($inputData as $key => $value) {
                 if (property_exists($data, $key)) {
@@ -274,7 +272,7 @@ class CartTokenProcessor implements ProcessorInterface
      * Resolve cart based on operation and data
      */
     private function resolveCart(string $operationName, CartInput $data, ?Customer $customer, ?string $token): ?CartModel
-    {   
+    {
         if ($operationName === 'mergeGuest' && $data->cartId) {
             return CartTokenFacade::getCartById((int) $data->cartId);
         }
@@ -350,7 +348,7 @@ class CartTokenProcessor implements ProcessorInterface
         }
 
         if (! $product->status) {
-            throw new \Exception(__('shop::app.checkout.cart.inactive-add'));
+            throw new InvalidInputException(__('shop::app.checkout.cart.inactive-add'));
         }
 
         $groupedQty = $this->normalizeJsonFieldToArray($data->groupedQty, 'groupedQty')
@@ -428,7 +426,7 @@ class CartTokenProcessor implements ProcessorInterface
                     'is_active'  => 1,
                 ]);
                 $guestCartTokenDetail = $this->guestCartTokensRepository->createToken($cart->id);
-                
+
             }
         }
 
@@ -436,7 +434,7 @@ class CartTokenProcessor implements ProcessorInterface
             // Handle is_buy_now - deactivate cart and prepare for checkout
             if (! empty($data->isBuyNow)) {
                 CartFacade::deActivateCart();
-                
+
                 // Create a new cart for buy now
                 $channel = core()->getCurrentChannel();
                 if ($customer) {
@@ -464,6 +462,12 @@ class CartTokenProcessor implements ProcessorInterface
             $bundleOptionQty = $this->normalizeJsonFieldToArray($data->bundleOptionQty, 'bundleOptionQty');
             $booking = $this->normalizeJsonFieldToArray($data->booking, 'booking');
 
+            // For bundle products, enforce admin-defined quantities.
+            // Only allow user-provided qty where is_user_defined is true on the bundle option product.
+            if ($product->type === 'bundle' && is_array($bundleOptions) && is_array($bundleOptionQty)) {
+                $bundleOptionQty = $this->sanitizeBundleOptionQty($bundleOptions, $bundleOptionQty);
+            }
+
             $cartData = [
                 'quantity'   => $data->quantity,
                 'product_id' => $product->id,
@@ -472,7 +476,6 @@ class CartTokenProcessor implements ProcessorInterface
                 ...(is_array($bundleOptions) ? ['bundle_options' => $bundleOptions] : []),
                 ...(is_array($bundleOptionQty) ? ['bundle_option_qty' => $bundleOptionQty] : []),
                 ...(isset($data->selectedConfigurableOption) ? ['selected_configurable_option' => $data->selectedConfigurableOption] : []),
-                ...(isset($data->variantId) ? ['variant_id' => (int) $data->variantId] : []),
                 ...(is_array($data->superAttribute) ? ['super_attribute' => $data->superAttribute] : []),
                 ...(is_array($groupedQty) ? ['qty' => $groupedQty] : []),
                 ...(is_array($data->links) ? ['links' => $data->links] : []),
@@ -504,7 +507,7 @@ class CartTokenProcessor implements ProcessorInterface
 
             CartFacade::collectTotals();
 
-            $updatedCart  = CartFacade::getCart();
+            $updatedCart = CartFacade::getCart();
 
             Event::dispatch('cart.after.add', ['cart' => $updatedCart]);
         } catch (\Exception $e) {
@@ -560,6 +563,73 @@ class CartTokenProcessor implements ProcessorInterface
         return $decoded;
     }
 
+    /**
+     * Validate and sanitize bundle option quantities.
+     *
+     * Checkbox/Multiselect options: qty is fixed by admin — throws error if customer tries to change
+     * Radio/Select options: qty can be changed by customer
+     *
+     * @param  array  $bundleOptions  [optionId => [optionProductId, ...]]
+     * @param  array  $bundleOptionQty  [optionId => qty]
+     * @return array Validated bundle option quantities
+     *
+     * @throws InvalidInputException
+     */
+    private function sanitizeBundleOptionQty(array $bundleOptions, array $bundleOptionQty): array
+    {
+        $optionRepo = app(\Webkul\Product\Repositories\ProductBundleOptionRepository::class);
+        $optionProductRepo = app(\Webkul\Product\Repositories\ProductBundleOptionProductRepository::class);
+
+        $sanitized = [];
+
+        foreach ($bundleOptions as $optionId => $optionProductIds) {
+            if (! is_array($optionProductIds)) {
+                continue;
+            }
+
+            $bundleOption = $optionRepo->find($optionId);
+
+            if (! $bundleOption) {
+                continue;
+            }
+
+            // Checkbox/Multiselect: qty fixed by admin; Radio/Select: customer can change
+            $canChangeQty = in_array($bundleOption->type, ['radio', 'select']);
+
+            foreach ($optionProductIds as $optionProductId) {
+                if (! $optionProductId) {
+                    continue;
+                }
+
+                $optionProduct = $optionProductRepo->find($optionProductId);
+
+                if (! $optionProduct) {
+                    continue;
+                }
+
+                if ($canChangeQty) {
+                    $sanitized[$optionId] = $bundleOptionQty[$optionId] ?? $optionProduct->qty;
+                } else {
+                    $userQty = $bundleOptionQty[$optionId] ?? null;
+
+                    // If customer sent a qty that differs from admin-defined qty, throw error
+                    if ($userQty !== null && (int) $userQty !== (int) $optionProduct->qty) {
+                        throw new InvalidInputException(
+                            __('bagistoapi::app.graphql.cart.bundle-qty-not-changeable', [
+                                'option' => $bundleOption->label,
+                                'qty'    => $optionProduct->qty,
+                            ])
+                        );
+                    }
+
+                    $sanitized[$optionId] = $optionProduct->qty;
+                }
+            }
+        }
+
+        return $sanitized;
+    }
+
     /**
      * Normalize booking payload so clients can send user-friendly slot strings like "10:00 AM - 11:00 AM".
      */
@@ -732,6 +802,9 @@ class CartTokenProcessor implements ProcessorInterface
             throw new AuthorizationException(__('bagistoapi::app.graphql.cart.unauthorized-access'));
         }
 
+        // Prevent quantity update for event and appointment booking products
+        $this->guardBookingCartItemUpdate($cart, (int) $data->cartItemId);
+
         CartFacade::setCart($cart);
 
         Event::dispatch('cart.item.before.update', ['cartItem' => $data->cartItemId]);
@@ -756,7 +829,11 @@ class CartTokenProcessor implements ProcessorInterface
             throw new ResourceNotFoundException(__('bagistoapi::app.graphql.cart.cart-not-found'));
         }
 
-        return (array) CartData::fromModel($cart);
+        $cartData = CartData::fromModel($cart);
+        $cartData->success = true;
+        $cartData->message = __('bagistoapi::app.graphql.cart.cart-item-updated-successfully');
+
+        return (array) $cartData;
     }
 
     /**
@@ -824,6 +901,15 @@ class CartTokenProcessor implements ProcessorInterface
             throw new AuthorizationException(__('bagistoapi::app.graphql.cart.unauthorized-access'));
         }
 
+        CartFacade::setCart($cart);
+
+        if ($cart->shipping_method && $cart->shipping_address) {
+            \Webkul\Shipping\Facades\Shipping::collectRates();
+        }
+
+        CartFacade::collectTotals();
+
+        $cart = CartFacade::getCart();
         $cart->load('items.product');
 
         $cartData = CartData::fromModel($cart);
@@ -880,6 +966,8 @@ class CartTokenProcessor implements ProcessorInterface
             ]);
         }
 
+        $guestCart->load('items.child');
+
         foreach ($guestCart->items as $item) {
             try {
                 $cartItem = $customerCart->items()
@@ -892,9 +980,19 @@ class CartTokenProcessor implements ProcessorInterface
                         'quantity' => $cartItem->quantity + $item->quantity,
                     ]);
                 } else {
-                    $item->replicate()
-                        ->fill(['cart_id' => $customerCart->id])
-                        ->save();
+                    $newItem = $item->replicate()
+                        ->fill(['cart_id' => $customerCart->id]);
+                    $newItem->save();
+
+                    // Replicate child item for configurable products
+                    if ($item->type === 'configurable' && $item->child) {
+                        $item->child->replicate()
+                            ->fill([
+                                'cart_id'   => $customerCart->id,
+                                'parent_id' => $newItem->id,
+                            ])
+                            ->save();
+                    }
                 }
             } catch (\Exception $e) {
                 continue;
@@ -903,6 +1001,20 @@ class CartTokenProcessor implements ProcessorInterface
 
         $guestCart->update(['is_active' => 0]);
 
+        // Reload cart with relationships and remove invalid items
+        // (e.g., configurable items without child entries or deleted products)
+        $customerCart = CartModel::with('items.product', 'items.child.product')->find($customerCart->id);
+
+        foreach ($customerCart->items as $item) {
+            if (! $item->product
+                || ($item->type === 'configurable' && ! $item->child)
+            ) {
+                $item->delete();
+            }
+        }
+
+        $customerCart = CartModel::with('items.product')->find($customerCart->id);
+
         CartFacade::setCart($customerCart);
 
         CartFacade::collectTotals();
@@ -1191,4 +1303,40 @@ class CartTokenProcessor implements ProcessorInterface
             throw new OperationFailedException($e->getMessage(), 0, $e);
         }
     }
+
+    /**
+     * Prevent quantity updates for booking products that don't allow it.
+     *
+     * - Event booking: quantity is determined by ticket selection, not changeable after add-to-cart.
+     * - Appointment booking: always quantity 1, cannot be changed.
+     */
+    private function guardBookingCartItemUpdate(CartModel $cart, int $cartItemId): void
+    {
+        $cartItem = $cart->items->firstWhere('id', $cartItemId);
+
+        if (! $cartItem || $cartItem->type !== 'booking') {
+            return;
+        }
+
+        // Check additional.booking.type first, then fall back to DB lookup
+        $bookingType = $cartItem->additional['booking']['type'] ?? null;
+
+        if (! $bookingType) {
+            $bookingType = \Webkul\BookingProduct\Models\BookingProduct::query()
+                ->where('product_id', $cartItem->product_id)
+                ->value('type');
+        }
+
+        if ($bookingType === 'event') {
+            throw new InvalidInputException(
+                __('bagistoapi::app.graphql.cart.event-booking-quantity-not-changeable')
+            );
+        }
+
+        if ($bookingType === 'appointment') {
+            throw new InvalidInputException(
+                __('bagistoapi::app.graphql.cart.appointment-booking-quantity-not-changeable')
+            );
+        }
+    }
 }

+ 101 - 19
packages/Webkul/BagistoApi/src/State/CompareItemProcessor.php

@@ -4,6 +4,9 @@ namespace Webkul\BagistoApi\State;
 
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Request as RequestFacade;
 use Webkul\BagistoApi\Dto\CreateCompareItemInput;
 use Webkul\BagistoApi\Dto\DeleteCompareItemInput;
 use Webkul\BagistoApi\Exception\AuthorizationException;
@@ -11,8 +14,6 @@ use Webkul\BagistoApi\Exception\InvalidInputException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
 use Webkul\BagistoApi\Models\CompareItem;
 use Webkul\BagistoApi\Models\Product;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Http\Request;
 
 /**
  * CompareItemProcessor - Handles create/delete operations for compare items
@@ -30,18 +31,21 @@ class CompareItemProcessor implements ProcessorInterface
     public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
     {
         if ($data instanceof CreateCompareItemInput) {
-            return $this->handleCreate($data, $context);
+            $this->hydrateCreateInputFromContext($data, $context);
+
+            return $this->handleCreate($data);
         }
 
-        /** Handle REST POST — model received instead of DTO */
         if ($data instanceof CompareItem && $operation instanceof \ApiPlatform\Metadata\Post) {
-            $input = new CreateCompareItemInput();
-            $input->productId = request()->input('product_id') ?? request()->input('productId');
+            $input = new CreateCompareItemInput;
+            $input->product_id = request()->input('product_id') ?? request()->input('productId');
 
-            return $this->handleCreate($input, $context);
+            return $this->handleCreate($input);
         }
 
         if ($data instanceof DeleteCompareItemInput) {
+            $this->hydrateDeleteInputFromContext($data, $context);
+
             return $this->handleDelete($data);
         }
 
@@ -57,27 +61,26 @@ class CompareItemProcessor implements ProcessorInterface
     /**
      * Handle create operation for compare items
      */
-    private function handleCreate(CreateCompareItemInput $input, array $context = []): CompareItem
+    private function handleCreate(CreateCompareItemInput $input): CompareItem
     {
-        if (empty($input->productId)) {
+        if (empty($input->product_id)) {
             throw new InvalidInputException(__('bagistoapi::app.graphql.compare-item.product-id-required'));
         }
 
-        $product = Product::find($input->productId);
+        $product = Product::find($input->product_id);
+
         if (! $product) {
             throw new ResourceNotFoundException(__('bagistoapi::app.graphql.compare-item.product-not-found'));
         }
- 
+
         $user = Auth::guard('sanctum')->user();
-            
+
         if (! $user) {
             throw new AuthorizationException(__('bagistoapi::app.graphql.logout.unauthenticated'));
         }
 
-        $customerId = $user->id;
-
-        $existingItem = CompareItem::where('customer_id', $customerId)
-            ->where('product_id', $input->productId)
+        $existingItem = CompareItem::where('customer_id', $user->id)
+            ->where('product_id', $input->product_id)
             ->first();
 
         if ($existingItem) {
@@ -85,8 +88,8 @@ class CompareItemProcessor implements ProcessorInterface
         }
 
         $compareItem = CompareItem::create([
-            'product_id' => $input->productId,
-            'customer_id' => $customerId,
+            'product_id'  => $input->product_id,
+            'customer_id' => $user->id,
         ]);
 
         $compareItem->load(['product', 'customer']);
@@ -110,7 +113,6 @@ class CompareItemProcessor implements ProcessorInterface
         }
 
         $compareItemId = basename($input->id);
-
         $compareItem = CompareItem::find($compareItemId);
 
         if (! $compareItem) {
@@ -126,4 +128,84 @@ class CompareItemProcessor implements ProcessorInterface
 
         return $compareItem;
     }
+
+    private function hydrateCreateInputFromContext(CreateCompareItemInput $input, array $context): void
+    {
+        if (! empty($input->product_id)) {
+            return;
+        }
+
+        $productId = $this->extractProductId($context['args']['input'] ?? $context['args'] ?? null);
+
+        if ($productId === null) {
+            $request = $this->request ?? RequestFacade::instance();
+
+            if ($request) {
+                $productId = $this->extractProductId($request->input('variables.input'))
+                    ?? $this->extractProductId($request->input('input'))
+                    ?? $this->extractProductId($request->input('extensions.variables.input'));
+            }
+        }
+
+        if ($productId !== null) {
+            $input->product_id = $productId;
+        }
+    }
+
+    private function hydrateDeleteInputFromContext(DeleteCompareItemInput $input, array $context): void
+    {
+        if (! empty($input->id)) {
+            return;
+        }
+
+        $id = $this->extractId($context['args']['input'] ?? $context['args'] ?? null);
+
+        if ($id === null) {
+            $request = $this->request ?? RequestFacade::instance();
+
+            if ($request) {
+                $id = $this->extractId($request->input('variables.input'))
+                    ?? $this->extractId($request->input('input'))
+                    ?? $this->extractId($request->input('extensions.variables.input'));
+            }
+        }
+
+        if ($id !== null) {
+            $input->id = $id;
+        }
+    }
+
+    private function extractProductId(mixed $input): ?int
+    {
+        if (is_array($input)) {
+            $value = $input['product_id'] ?? $input['productId'] ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        if (is_object($input)) {
+            $value = $input->product_id ?? $input->productId ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        return null;
+    }
+
+    private function extractId(mixed $input): ?string
+    {
+        if (is_array($input)) {
+            $value = $input['id'] ?? null;
+
+            return $value !== null && $value !== '' ? (string) $value : null;
+        }
+
+        if (is_object($input)) {
+            $value = $input->id ?? null;
+
+            return $value !== null && $value !== '' ? (string) $value : null;
+        }
+
+        return null;
+    }
 }

+ 29 - 44
packages/Webkul/BagistoApi/src/State/CompareItemProvider.php

@@ -6,9 +6,10 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Facades\Auth;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Models\CompareItem;
-use Illuminate\Support\Facades\Auth;
 
 /**
  * CompareItemProvider - Handles retrieval of compare items for authenticated customers
@@ -21,14 +22,6 @@ class CompareItemProvider implements ProviderInterface
         private readonly Pagination $pagination
     ) {}
 
-    /**
-     * Retrieve compare items for the authenticated customer
-     *
-     * @param Operation $operation
-     * @param array $uriVariables
-     * @param array $context
-     * @return object|array|null
-     */
     public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
     {
         $customer = Auth::guard('sanctum')->user();
@@ -43,53 +36,45 @@ class CompareItemProvider implements ProviderInterface
         $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
-        $defaultPerPage = 30;
+        $perPage = $first ?? $last ?? 30;
+        $offset = 0;
+
+        if ($after) {
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
 
-        // Determine page size
-        if ($first !== null) {
-            $perPage = $first;
-        } elseif ($last !== null) {
-            $perPage = $last;
-        } else {
-            $perPage = $defaultPerPage;
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
         $query = CompareItem::where('customer_id', $customer->id)
             ->with(['product', 'customer'])
             ->orderBy('id', 'asc');
 
-        // Handle cursor-based pagination
-        if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '>', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '<', $beforeId);
-            // For 'before', reverse order, paginate, then reverse results
-            $query->orderBy('id', 'desc');
-            $laravelPaginator = $query->paginate($perPage);
-
-            // Reverse items to maintain proper cursor order
-            $items = $laravelPaginator->items();
-            $items = array_reverse($items);
+        $total = (clone $query)->count();
 
-            return new Paginator(
-                $laravelPaginator,
-                (int) $laravelPaginator->currentPage(),
-                $perPage,
-                $laravelPaginator->lastPage(),
-                $laravelPaginator->total(),
-            );
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
 
         return new Paginator(
-            $laravelPaginator,
-            (int) $laravelPaginator->currentPage(),
-            $perPage,
-            $laravelPaginator->lastPage(),
-            $laravelPaginator->total(),
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
         );
     }
 }

+ 31 - 38
packages/Webkul/BagistoApi/src/State/CountryStateCollectionProvider.php

@@ -6,6 +6,7 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Webkul\BagistoApi\Models\CountryState;
 
@@ -57,52 +58,44 @@ class CountryStateCollectionProvider implements ProviderInterface
             $perPage = $defaultPerPage;
         }
 
-        $query = CountryState::where('country_id', $countryId)
-            ->orderBy('id', 'asc');
+        $offset = 0;
 
-        // Handle cursor-based pagination
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '>', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '<', $beforeId);
-            // For 'before', we need to reverse order, paginate, then reverse results
-            $query->orderBy('id', 'desc');
-            $laravelPaginator = $query->paginate($perPage);
-
-            // Reverse the items to maintain proper cursor order
-            $items = $laravelPaginator->items();
-            $items = array_reverse($items);
-
-            // Load relations
-            foreach ($items as $item) {
-                $item->load('translations');
-            }
-
-            // Create a new paginator with reversed items
-            return new Paginator(
-                $laravelPaginator,
-                (int) $laravelPaginator->currentPage(),
-                $perPage,
-                $laravelPaginator->lastPage(),
-                $laravelPaginator->total(),
-            );
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $query = CountryState::where('country_id', $countryId)
+            ->with('translations')
+            ->orderBy('id', 'asc');
+
+        $total = (clone $query)->count();
 
-        // Load relations for all items
-        foreach ($laravelPaginator->items() as $item) {
-            $item->load('translations');
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
         }
 
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
         return new Paginator(
-            $laravelPaginator,
-            (int) $laravelPaginator->currentPage(),
-            $perPage,
-            $laravelPaginator->lastPage(),
-            $laravelPaginator->total(),
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
         );
     }
 }

+ 91 - 0
packages/Webkul/BagistoApi/src/State/CursorAwareCollectionProvider.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Laravel\Eloquent\Paginator;
+use ApiPlatform\Laravel\Eloquent\State\LinksHandler;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
+
+/**
+ * Generic cursor-aware collection provider for GraphQL queries.
+ *
+ * API Platform's default CollectionProvider uses page-based pagination
+ * which ignores cursor arguments (after/before). This provider properly
+ * decodes cursor arguments and applies offset-based pagination so that
+ * Relay-style cursor pagination works correctly.
+ *
+ * Can be used by any model that needs working cursor pagination without
+ * custom query logic.
+ */
+class CursorAwareCollectionProvider implements ProviderInterface
+{
+    public function __construct(
+        private readonly LinksHandler $linksHandler,
+    ) {}
+
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
+    {
+        $resourceClass = $operation->getClass();
+        $model = new $resourceClass;
+
+        $args = $context['args'] ?? [];
+
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
+        $before = $args['before'] ?? null;
+
+        $limit = $first ?? $last ?? 10;
+        $offset = 0;
+
+        if ($after) {
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $limit);
+        }
+
+        $query = $this->linksHandler->handleLinks(
+            $model->newQuery(),
+            $uriVariables,
+            ['operation' => $operation, 'modelClass' => $operation->getClass()] + $context
+        );
+
+        $filters = $context['filters'] ?? [];
+
+        foreach ($filters as $column => $value) {
+            if ($model->getConnection()->getSchemaBuilder()->hasColumn($model->getTable(), $column)) {
+                $query->where($column, $value);
+            }
+        }
+
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $limit);
+        }
+
+        $items = $query
+            ->offset($offset)
+            ->limit($limit)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $limit) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $limit,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
+    }
+}

+ 51 - 27
packages/Webkul/BagistoApi/src/State/CustomerAddressProvider.php

@@ -3,11 +3,10 @@
 namespace Webkul\BagistoApi\State;
 
 use ApiPlatform\Laravel\Eloquent\Paginator;
-use ApiPlatform\Laravel\Eloquent\PartialPaginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
-use Illuminate\Support\Facades\DB;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Request;
 use Webkul\BagistoApi\Exception\AuthenticationException;
 use Webkul\BagistoApi\Facades\TokenHeaderFacade;
@@ -41,20 +40,52 @@ class CustomerAddressProvider implements ProviderInterface
             throw new AuthenticationException(__('bagistoapi::app.graphql.customer-addresses.invalid-or-expired-token'));
         }
 
-        $query = CustomerAddress::where('customer_id', $authenticatedCustomerId);
+        $args = $context['args'] ?? [];
 
-        $isPartial = $operation->getPaginationPartial();
-        $collection = $query
-            ->{$isPartial ? 'simplePaginate' : 'paginate'}(
-                perPage: $this->pagination->getLimit($operation, $context),
-                page: $this->pagination->getPage($context),
-            );
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
+        $before = $args['before'] ?? null;
 
-        if ($isPartial) {
-            return new PartialPaginator($collection);
+        $perPage = $first ?? $last ?? 10;
+        $offset = 0;
+
+        if ($after) {
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
+        }
+
+        $query = CustomerAddress::where('customer_id', $authenticatedCustomerId)
+            ->orderBy('id', 'asc');
+
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
         }
 
-        return new Paginator($collection);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 
     /**
@@ -63,30 +94,23 @@ class CustomerAddressProvider implements ProviderInterface
     private function getCustomerIdFromToken(string $token): ?int
     {
         try {
-            $tokenParts = explode('|', $token);
-
-            if (count($tokenParts) !== 2) {
+            if (strpos($token, '|') === false) {
                 return null;
             }
 
-            $tokenId = $tokenParts[0];
-
-            $personalAccessToken = DB::table('personal_access_tokens')
-                ->where('id', $tokenId)
-                ->where('tokenable_type', Customer::class)
-                ->where(function ($query) {
-                    $query->whereNull('expires_at')
-                        ->orWhere('expires_at', '>', now());
-                })
-                ->first();
+            $personalAccessToken = \Laravel\Sanctum\PersonalAccessToken::findToken($token);
 
             if (! $personalAccessToken) {
                 return null;
             }
 
-            $customer = Customer::find($personalAccessToken->tokenable_id);
+            if (! $personalAccessToken->tokenable instanceof Customer) {
+                return null;
+            }
+
+            $customer = $personalAccessToken->tokenable;
 
-            if (! $customer || $customer->is_suspended) {
+            if ($customer->is_suspended) {
                 return null;
             }
 

+ 37 - 14
packages/Webkul/BagistoApi/src/State/CustomerDownloadableProductProvider.php

@@ -7,6 +7,7 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Auth;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
@@ -84,27 +85,49 @@ class CustomerDownloadableProductProvider implements ProviderInterface
             $query->where('status', (string) $status);
         }
 
-        /** Cursor-based pagination */
-        $first  = isset($args['first']) ? (int) $args['first'] : null;
-        $last   = isset($args['last']) ? (int) $args['last'] : null;
-        $after  = $args['after'] ?? null;
+        /** Cursor-based pagination (offset-based cursors from API Platform) */
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
         $perPage = $first ?? $last ?? 10;
-
-        $query->orderBy('id', 'desc');
+        $offset = 0;
 
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '<', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '>', $beforeId);
-            $query->orderBy('id', 'asc');
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $query->orderBy('id', 'desc');
+
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
+        }
 
-        return new Paginator($laravelPaginator);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 37 - 16
packages/Webkul/BagistoApi/src/State/CustomerInvoiceProvider.php

@@ -7,9 +7,8 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
-use Carbon\Traits\ToStringFormat;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Log;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
 use Webkul\BagistoApi\Models\CustomerInvoice;
@@ -96,27 +95,49 @@ class CustomerInvoiceProvider implements ProviderInterface
             $query->where('state', (string) $state);
         }
 
-        /** Cursor-based pagination */
-        $first  = isset($args['first']) ? (int) $args['first'] : null;
-        $last   = isset($args['last']) ? (int) $args['last'] : null;
-        $after  = $args['after'] ?? null;
+        /** Cursor-based pagination (offset-based cursors from API Platform) */
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
         $perPage = $first ?? $last ?? 10;
-
-        $query->orderBy('id', 'desc');
+        $offset = 0;
 
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '<', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '>', $beforeId);
-            $query->orderBy('id', 'asc');
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $query->orderBy('id', 'desc');
+
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
+        }
 
-        return new Paginator($laravelPaginator);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 37 - 17
packages/Webkul/BagistoApi/src/State/CustomerOrderProvider.php

@@ -7,6 +7,7 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Auth;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
@@ -32,7 +33,6 @@ class CustomerOrderProvider implements ProviderInterface
     {
         $customer = Auth::guard('sanctum')->user();
 
-
         if (! $customer) {
             throw new AuthorizationException(__('bagistoapi::app.graphql.logout.unauthenticated'));
         }
@@ -63,13 +63,11 @@ class CustomerOrderProvider implements ProviderInterface
         $order = $orderQuery->find($id);
 
         if (! $order) {
-            
             throw new ResourceNotFoundException(
                 __('bagistoapi::app.graphql.customer-order.not-found', ['id' => $id])
             );
         }
 
-
         return $order;
     }
 
@@ -125,27 +123,49 @@ class CustomerOrderProvider implements ProviderInterface
             $query->where('status', (string) $status);
         }
 
-        /** Cursor-based pagination */
-        $first  = isset($args['first']) ? (int) $args['first'] : null;
-        $last   = isset($args['last']) ? (int) $args['last'] : null;
-        $after  = $args['after'] ?? null;
+        /** Cursor-based pagination (offset-based cursors from API Platform) */
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
         $perPage = $first ?? $last ?? 10;
-
-        $query->orderBy('id', 'desc');
+        $offset = 0;
 
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '<', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '>', $beforeId);
-            $query->orderBy('id', 'asc');
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $query->orderBy('id', 'desc');
+
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
+        }
 
-        return new Paginator($laravelPaginator);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 37 - 14
packages/Webkul/BagistoApi/src/State/CustomerOrderShipmentProvider.php

@@ -7,6 +7,7 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Auth;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
@@ -94,27 +95,49 @@ class CustomerOrderShipmentProvider implements ProviderInterface
             $query->where('status', (string) $status);
         }
 
-        /** Cursor-based pagination */
-        $first  = isset($args['first']) ? (int) $args['first'] : null;
-        $last   = isset($args['last']) ? (int) $args['last'] : null;
-        $after  = $args['after'] ?? null;
+        /** Cursor-based pagination (offset-based cursors from API Platform) */
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
         $perPage = $first ?? $last ?? 10;
-
-        $query->orderBy('id', 'desc');
+        $offset = 0;
 
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '<', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '>', $beforeId);
-            $query->orderBy('id', 'asc');
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $query->orderBy('id', 'desc');
+
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
+        }
 
-        return new Paginator($laravelPaginator);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 13 - 20
packages/Webkul/BagistoApi/src/State/CustomerProfileProcessor.php

@@ -9,13 +9,13 @@ use Illuminate\Support\Facades\Event;
 use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Request;
 use Illuminate\Support\Facades\Storage;
-use Webkul\Customer\Models\Customer;
 use Webkul\BagistoApi\Dto\CustomerProfileOutput;
 use Webkul\BagistoApi\Exception\AuthenticationException;
 use Webkul\BagistoApi\Exception\InvalidInputException;
 use Webkul\BagistoApi\Helper\CustomerProfileHelper;
 use Webkul\BagistoApi\Models\CustomerProfile as CustomerProfileModel;
 use Webkul\BagistoApi\Validators\CustomerValidator;
+use Webkul\Customer\Models\Customer;
 
 class CustomerProfileProcessor implements ProcessorInterface
 {
@@ -29,13 +29,13 @@ class CustomerProfileProcessor implements ProcessorInterface
         // The denormalized object may not have all fields properly populated
         if (isset($context['args']['input']) && is_array($context['args']['input'])) {
             $inputData = $context['args']['input'];
-            
+
             // Merge with existing data, preferring args values
             if (is_object($data)) {
-                $dataArray = (array)$data;
-                $data = (object)array_merge($dataArray, $inputData);
+                $dataArray = (array) $data;
+                $data = (object) array_merge($dataArray, $inputData);
             } else {
-                $data = (object)$inputData;
+                $data = (object) $inputData;
             }
         }
 
@@ -222,28 +222,21 @@ class CustomerProfileProcessor implements ProcessorInterface
     private function getCustomerFromToken(string $token): ?Customer
     {
         try {
-            $tokenParts = explode('|', $token);
-
-            if (count($tokenParts) !== 2) {
+            if (strpos($token, '|') === false) {
                 return null;
             }
 
-            $tokenId = $tokenParts[0];
-
-            $personalAccessToken = DB::table('personal_access_tokens')
-                ->where('id', $tokenId)
-                ->where('tokenable_type', Customer::class)
-                ->where(function ($query) {
-                    $query->whereNull('expires_at')
-                        ->orWhere('expires_at', '>', now());
-                })
-                ->first();
+            $personalAccessToken = \Laravel\Sanctum\PersonalAccessToken::findToken($token);
 
             if (! $personalAccessToken) {
                 return null;
             }
 
-            return Customer::find($personalAccessToken->tokenable_id);
+            if (! $personalAccessToken->tokenable instanceof Customer) {
+                return null;
+            }
+
+            return $personalAccessToken->tokenable;
         } catch (\Exception $e) {
             return null;
         }
@@ -303,7 +296,7 @@ class CustomerProfileProcessor implements ProcessorInterface
 
         // Phone should only contain digits - remove all non-digit characters
         $cleanedPhone = preg_replace('/[^0-9]/', '', $phone);
-        
+
         // If the cleaned phone is different from original, it means special characters were present
         if ($cleanedPhone !== $phone) {
             throw new InvalidInputException(__('bagistoapi::app.graphql.customer.phone-special-chars-not-allowed'));

+ 37 - 14
packages/Webkul/BagistoApi/src/State/CustomerReviewProvider.php

@@ -7,6 +7,7 @@ use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Auth;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
@@ -89,27 +90,49 @@ class CustomerReviewProvider implements ProviderInterface
             $query->where('rating', (int) $rating);
         }
 
-        /** Cursor-based pagination */
-        $first  = isset($args['first']) ? (int) $args['first'] : null;
-        $last   = isset($args['last']) ? (int) $args['last'] : null;
-        $after  = $args['after'] ?? null;
+        /** Cursor-based pagination (offset-based cursors from API Platform) */
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
         $perPage = $first ?? $last ?? 10;
-
-        $query->orderBy('id', 'desc');
+        $offset = 0;
 
         if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '<', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '>', $beforeId);
-            $query->orderBy('id', 'asc');
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $query->orderBy('id', 'desc');
+
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
+        }
 
-        return new Paginator($laravelPaginator);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 31 - 49
packages/Webkul/BagistoApi/src/State/FilterableAttributesProvider.php

@@ -6,6 +6,7 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\DB;
 use Webkul\BagistoApi\Models\Filter\Attribute;
 use Webkul\BagistoApi\Models\Product;
@@ -31,9 +32,18 @@ class FilterableAttributesProvider implements ProviderInterface
 
         $defaultPerPage = 30;
         $perPage = $first ?? $last ?? $defaultPerPage;
+        $offset = 0;
 
-        $afterId = $after ? (int) base64_decode($after) : null;
-        $beforeId = $before ? (int) base64_decode($before) : null;
+        if ($after) {
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
+        }
 
         $query = Attribute::query();
 
@@ -51,13 +61,6 @@ class FilterableAttributesProvider implements ProviderInterface
             $query->where('is_filterable', 1);
         }
 
-        if ($after) {
-            $query->where('attributes.id', '>', $afterId);
-        }
-        if ($before) {
-            $query->where('attributes.id', '<', $beforeId);
-        }
-
         $query->with(['options', 'translations', 'options.translations']);
         $query->orderBy('attributes.id', 'asc');
 
@@ -75,55 +78,34 @@ class FilterableAttributesProvider implements ProviderInterface
 
         $maxPrice = $maxPriceQuery->max('min_price') ?? 0;
 
-        if ($last !== null) {
-            $reverse = Attribute::query();
-
-            if ($categorySlug) {
-                $reverse
-                    ->leftJoin('category_filterable_attributes as cfa', 'cfa.attribute_id', '=', 'attributes.id')
-                    ->where('cfa.category_id', $categoryId);
-            } else {
-                $reverse->where('is_filterable', 1);
-            }
+        $total = (clone $query)->count();
 
-            if ($before) {
-                $reverse->where('attributes.id', '<', $beforeId);
-            }
-
-            $reverse->with(['options', 'translations', 'options.translations']);
-            $reverse->orderBy('attributes.id', 'desc');
-
-            $totalCount = $reverse->count();
-
-            $items = $reverse->take($last)->get()->reverse()->values();
-
-            $items = $items->map(function ($item) use ($maxPrice) {
-                $item->maxPrice = (float) $maxPrice;
-                $item->minPrice = 0.0;
-
-                return $this->applyPriceValues($item);
-            });
-
-            $laravelPaginator = new \Illuminate\Pagination\LengthAwarePaginator(
-                $items,
-                $totalCount,
-                $last,
-                1,
-                ['path' => \Illuminate\Pagination\Paginator::resolveCurrentPath()]
-            );
-
-            return new Paginator($laravelPaginator);
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
         }
 
-        $laravelPaginator = $first !== null ? $query->paginate($first) : $query->paginate($defaultPerPage);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
 
-        $laravelPaginator->through(function ($item) use ($maxPrice) {
+        $items = $items->map(function ($item) use ($maxPrice) {
             $item->maxPrice = (float) $maxPrice;
             $item->minPrice = 0.0;
 
             return $item;
         });
 
-        return new Paginator($laravelPaginator);
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 42 - 12
packages/Webkul/BagistoApi/src/State/GetCheckoutAddressCollectionProvider.php

@@ -3,7 +3,6 @@
 namespace Webkul\BagistoApi\State;
 
 use ApiPlatform\Laravel\Eloquent\Paginator;
-use ApiPlatform\Laravel\Eloquent\PartialPaginator;
 use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface;
 use ApiPlatform\Laravel\Eloquent\State\Options;
 use ApiPlatform\Metadata\Exception\RuntimeException;
@@ -11,8 +10,8 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
 use ApiPlatform\State\Util\StateOptionsTrait;
-use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Request;
 use Psr\Container\ContainerInterface;
 use Webkul\BagistoApi\Exception\AuthenticationException;
@@ -49,7 +48,7 @@ class GetCheckoutAddressCollectionProvider implements ProviderInterface
         array $context = []
     ): object|array|null {
         $args = $context['args'] ?? [];
-        
+
         $request = Request::instance() ?? ($context['request'] ?? null);
 
         // Extract Bearer token from Authorization header
@@ -78,17 +77,48 @@ class GetCheckoutAddressCollectionProvider implements ProviderInterface
             return $query->get();
         }
 
-        $isPartial = $operation->getPaginationPartial();
-        $collection = $query
-            ->{$isPartial ? 'simplePaginate' : 'paginate'}(
-                perPage: $this->pagination->getLimit($operation, $context),
-                page: $this->pagination->getPage($context),
-            );
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
+        $before = $args['before'] ?? null;
+
+        $perPage = $first ?? $last ?? 10;
+        $offset = 0;
+
+        if ($after) {
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
+
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
+        }
+
+        $query->orderBy('id', 'asc');
+
+        $total = (clone $query)->count();
 
-        if ($isPartial) {
-            return new PartialPaginator($collection);
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
         }
 
-        return new Paginator($collection);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 56 - 11
packages/Webkul/BagistoApi/src/State/MoveWishlistToCartProcessor.php

@@ -4,17 +4,15 @@ namespace Webkul\BagistoApi\State;
 
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Event;
 use Webkul\BagistoApi\Dto\CartData;
 use Webkul\BagistoApi\Dto\MoveWishlistToCartInput;
-use Webkul\BagistoApi\Dto\MoveWishlistToCartOutput;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\InvalidInputException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
 use Webkul\BagistoApi\Models\Wishlist;
 use Webkul\Checkout\Facades\Cart;
-use Webkul\Checkout\Models\Cart as CartModel;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Event;
 
 /**
  * MoveWishlistToCartProcessor - Handles moving wishlist items to cart
@@ -32,9 +30,14 @@ class MoveWishlistToCartProcessor implements ProcessorInterface
     {
         if ($data instanceof MoveWishlistToCartInput) {
             /**
-             * REST fallback: the serializer's name converter may not populate camelCase DTO
-             * properties from snake_case JSON keys. Populate from request if needed.
+             * The serializer's name converter may not populate camelCase DTO properties.
+             * For GraphQL, read from $context['args']['input'] first (same pattern as WishlistProcessor).
+             * For REST, fall back to raw request input.
              */
+            if ($data->wishlistItemId === null) {
+                $this->hydrateInputFromContext($data, $context);
+            }
+
             if ($data->wishlistItemId === null) {
                 $data->wishlistItemId = (int) (request()->input('wishlist_item_id') ?? request()->input('wishlistItemId'));
                 $data->quantity = (int) (request()->input('quantity') ?? 1);
@@ -46,6 +49,43 @@ class MoveWishlistToCartProcessor implements ProcessorInterface
         return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
     }
 
+    /**
+     * Hydrate MoveWishlistToCartInput from GraphQL context args.
+     * Mirrors the pattern used in WishlistProcessor::hydrateCreateInputFromContext().
+     */
+    private function hydrateInputFromContext(MoveWishlistToCartInput $data, array $context): void
+    {
+        $args = $context['args']['input'] ?? $context['args'] ?? null;
+
+        if (is_array($args)) {
+            $id = $args['wishlistItemId'] ?? $args['wishlist_item_id'] ?? null;
+            if (is_numeric($id)) {
+                $data->wishlistItemId = (int) $id;
+            }
+
+            $qty = $args['quantity'] ?? null;
+            if (is_numeric($qty)) {
+                $data->quantity = (int) $qty;
+            }
+
+            return;
+        }
+
+        // Fallback: read from nested GraphQL variables in raw request
+        $input = request()->input('variables.input');
+        if (is_array($input)) {
+            $id = $input['wishlistItemId'] ?? $input['wishlist_item_id'] ?? null;
+            if (is_numeric($id)) {
+                $data->wishlistItemId = (int) $id;
+            }
+
+            $qty = $input['quantity'] ?? null;
+            if (is_numeric($qty)) {
+                $data->quantity = (int) $qty;
+            }
+        }
+    }
+
     /**
      * Handle move to cart operation for wishlist items
      * Returns cart data similar to handleAddProduct in CartTokenProcessor
@@ -73,13 +113,18 @@ class MoveWishlistToCartProcessor implements ProcessorInterface
         }
 
         try {
-            // Get the current cart first (if exists)
-            $cart = Cart::getCart();
-
-            // If no cart exists, create one
+            // Find the customer's existing active cart directly via repository
+            // because Cart::getCart() uses the default web guard (not sanctum)
+            // and returns null for API requests, causing a new empty cart to be created.
+            $cartRepository = app('Webkul\Checkout\Repositories\CartRepository');
+            $cart = $cartRepository->findOneWhere([
+                'customer_id' => $user->id,
+                'is_active'   => 1,
+            ]);
+
+            // Create a new cart only if the customer genuinely has none
             if (! $cart) {
                 $channel = core()->getCurrentChannel();
-                $cartRepository = app('Webkul\Checkout\Repositories\CartRepository');
                 $cart = $cartRepository->create([
                     'customer_id' => $user->id,
                     'channel_id'  => $channel->id,

+ 153 - 0
packages/Webkul/BagistoApi/src/State/PageProvider.php

@@ -0,0 +1,153 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use Webkul\BagistoApi\Models\Page;
+use Webkul\CMS\Repositories\PageRepository;
+
+/**
+ * Provider for fetching CMS pages
+ * Returns arrays for REST API and Eloquent models for GraphQL
+ */
+class PageProvider implements ProviderInterface
+{
+    public function __construct(
+        private readonly PageRepository $pageRepository
+    ) {}
+
+    public function provide(
+        Operation $operation,
+        array $uriVariables = [],
+        array $context = []
+    ): object|array|null {
+        // Check if it's a GraphQL operation (class name contains "GraphQl")
+        $isGraphQL = str_contains($operation::class, 'GraphQl');
+
+        // For GraphQL operations, return Eloquent models
+        if ($isGraphQL) {
+            return $this->provideForGraphQL($operation, $uriVariables, $context);
+        }
+
+        // For REST operations, return formatted arrays
+        return $this->provideForRest($operation, $uriVariables, $context);
+    }
+
+    /**
+     * Provide data for GraphQL - return Eloquent models
+     */
+    private function provideForGraphQL(Operation $operation, array $uriVariables, array $context): mixed
+    {
+        $name = $operation->getName();
+
+        // Handle pageByUrlKey - this goes through the resolver
+        if ($name === 'pageByUrlKey') {
+            return null; // Let the resolver handle it
+        }
+
+        // Check if it's an item query
+        $isItem = $operation instanceof Get;
+
+        if ($isItem) {
+            if (isset($uriVariables['id'])) {
+                return Page::with('translations')->find($uriVariables['id']);
+            }
+
+            return null;
+        }
+
+        // Collection query
+        return Page::with('translations')
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
+    /**
+     * Provide data for REST API - return formatted arrays
+     */
+    private function provideForRest(Operation $operation, array $uriVariables, array $context)
+    {
+        if ($operation instanceof Get) {
+            return $this->provideItem($uriVariables, $context);
+        }
+
+        if ($operation instanceof GetCollection) {
+            return $this->provideCollection($context);
+        }
+
+        return null;
+    }
+
+    /**
+     * Provide single page item
+     */
+    private function provideItem(array $uriVariables, array $context): ?array
+    {
+        if (isset($uriVariables['id'])) {
+            $id = $uriVariables['id'];
+            $page = Page::with('translations')->find($id);
+
+            if (! $page) {
+                return null;
+            }
+
+            return $this->formatPage($page);
+        }
+
+        return null;
+    }
+
+    /**
+     * Provide collection of pages
+     */
+    private function provideCollection(array $context): iterable
+    {
+        $pages = Page::with('translations')
+            ->orderBy('created_at', 'desc')
+            ->get();
+
+        return $pages->map(function ($page) {
+            return $this->formatPage($page);
+        });
+    }
+
+    /**
+     * Format page for REST API response
+     */
+    private function formatPage($page): array
+    {
+        $translation = $page->translations->firstWhere('locale', app()->getLocale())
+            ?? $page->translations->first();
+
+        return [
+            'id'          => '/api/shop/pages/'.$page->id,
+            '_id'         => $page->id,
+            'layout'      => $page->layout,
+            'createdAt'   => $page->created_at?->toIso8601String(),
+            'updatedAt'   => $page->updated_at?->toIso8601String(),
+            'translation' => $translation ? $this->formatTranslation($translation) : null,
+        ];
+    }
+
+    /**
+     * Format translation for REST API response
+     */
+    private function formatTranslation($translation): array
+    {
+        return [
+            'id'              => '/api/shop/page_translations/'.$translation->id,
+            '_id'             => $translation->id,
+            'pageTitle'       => $translation->page_title,
+            'urlKey'          => $translation->url_key,
+            'htmlContent'     => $translation->html_content,
+            'metaTitle'       => $translation->meta_title,
+            'metaDescription' => $translation->meta_description,
+            'metaKeywords'    => $translation->meta_keywords,
+            'locale'          => $translation->locale,
+            'cmsPageId'       => (string) $translation->cms_page_id,
+        ];
+    }
+}

+ 50 - 24
packages/Webkul/BagistoApi/src/State/ProductBagistoApiProvider.php

@@ -45,20 +45,32 @@ class ProductBagistoApiProvider implements ProviderInterface
         switch (strtoupper($sortKey)) {
             case 'TITLE':
             case 'NAME':
-                $query->leftJoin('product_attribute_values as pav_name', function ($join) {
-                    $join->on('products.id', '=', 'pav_name.product_id')
-                        ->where('pav_name.attribute_id', '=', 2);
-                })
-                    ->orderBy('pav_name.text_value', $direction)
-                    ->select('products.*');
+                $prefix = \DB::getTablePrefix();
 
-                if ($locale) {
-                    $query->where('pav_name.locale', $locale);
-                }
+                // Join for requested locale/channel
+                $query->leftJoin('product_attribute_values as pav_name_locale', function ($join) use ($locale, $channel) {
+                    $join->on('products.id', '=', 'pav_name_locale.product_id')
+                        ->where('pav_name_locale.attribute_id', '=', 2);
 
-                if ($channel) {
-                    $query->where('pav_name.channel', $channel);
-                }
+                    if ($locale) {
+                        $join->where('pav_name_locale.locale', $locale);
+                    }
+
+                    if ($channel) {
+                        $join->where('pav_name_locale.channel', $channel);
+                    }
+                });
+
+                // Fallback join for null locale/channel (default values)
+                $query->leftJoin('product_attribute_values as pav_name_fallback', function ($join) {
+                    $join->on('products.id', '=', 'pav_name_fallback.product_id')
+                        ->where('pav_name_fallback.attribute_id', '=', 2)
+                        ->whereNull('pav_name_fallback.locale')
+                        ->whereNull('pav_name_fallback.channel');
+                });
+
+                $query->orderBy(\DB::raw("COALESCE({$prefix}pav_name_locale.text_value, {$prefix}pav_name_fallback.text_value)"), $direction)
+                    ->select('products.*');
 
                 break;
 
@@ -73,20 +85,34 @@ class ProductBagistoApiProvider implements ProviderInterface
                 break;
 
             case 'PRICE':
-                $query->leftJoin('product_attribute_values as pav_price', function ($join) {
-                    $join->on('products.id', '=', 'pav_price.product_id')
-                        ->where('pav_price.attribute_id', '=', 11);
-                })
-                    ->orderBy('pav_price.float_value', $direction)
-                    ->select('products.*');
-
-                if ($locale) {
-                    $query->where('pav_price.locale', $locale);
+                if (! isset($prefix)) {
+                    $prefix = \DB::getTablePrefix();
                 }
 
-                if ($channel) {
-                    $query->where('pav_price.channel', $channel);
-                }
+                // Join for requested locale/channel
+                $query->leftJoin('product_attribute_values as pav_price_locale', function ($join) use ($locale, $channel) {
+                    $join->on('products.id', '=', 'pav_price_locale.product_id')
+                        ->where('pav_price_locale.attribute_id', '=', 11);
+
+                    if ($locale) {
+                        $join->where('pav_price_locale.locale', $locale);
+                    }
+
+                    if ($channel) {
+                        $join->where('pav_price_locale.channel', $channel);
+                    }
+                });
+
+                // Fallback join for null locale/channel (default values)
+                $query->leftJoin('product_attribute_values as pav_price_fallback', function ($join) {
+                    $join->on('products.id', '=', 'pav_price_fallback.product_id')
+                        ->where('pav_price_fallback.attribute_id', '=', 11)
+                        ->whereNull('pav_price_fallback.locale')
+                        ->whereNull('pav_price_fallback.channel');
+                });
+
+                $query->orderBy(\DB::raw("COALESCE({$prefix}pav_price_locale.float_value, {$prefix}pav_price_fallback.float_value)"), $direction)
+                    ->select('products.*');
 
                 break;
 

+ 110 - 89
packages/Webkul/BagistoApi/src/State/ProductGraphQLProvider.php

@@ -6,8 +6,8 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
-use Webkul\BagistoApi\Models\Product;
 use Illuminate\Pagination\LengthAwarePaginator as LaravelPaginator;
+use Webkul\BagistoApi\Models\Product;
 
 class ProductGraphQLProvider implements ProviderInterface
 {
@@ -17,25 +17,32 @@ class ProductGraphQLProvider implements ProviderInterface
 
     private ?array $attributeTypeCache = null;
 
+    private ?array $attributeScopeCache = null;
+
     public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
     {
-        // Cache attribute types once for the entire request
+        // Cache attribute types and scope flags once for the entire request
         $this->attributeTypeCache = \DB::table('attributes')
             ->pluck('type', 'code')
             ->toArray();
 
+        $this->attributeScopeCache = \DB::table('attributes')
+            ->get(['code', 'value_per_locale', 'value_per_channel'])
+            ->keyBy('code')
+            ->toArray();
+
         $args = $context['args'] ?? [];
 
         $query = Product::query();
 
         $query->whereHas('attribute_values', function ($q) {
             $q->where('attribute_id', 8)
-              ->where('boolean_value', 1);
+                ->where('boolean_value', 1);
         });
 
         $query->whereHas('attribute_values', function ($q) {
             $q->where('attribute_id', 7)
-              ->where('boolean_value', 1);
+                ->where('boolean_value', 1);
         });
 
         if (! empty($args['query'])) {
@@ -43,46 +50,59 @@ class ProductGraphQLProvider implements ProviderInterface
 
             $query->where(function ($q) use ($searchTerm) {
                 $q->where('sku', 'like', "%{$searchTerm}%")
-                  ->orWhereHas('attribute_values', function ($attr) use ($searchTerm) {
-                      $attr->where('text_value', 'like', "%{$searchTerm}%");
-                  });
+                    ->orWhereHas('attribute_values', function ($attr) use ($searchTerm) {
+                        $attr->where('attribute_id', 2)
+                            ->where('text_value', 'like', "%{$searchTerm}%");
+                    });
             });
         }
 
-        $sortKey   = strtoupper($args['sortKey'] ?? 'ID');
-        $reverse   = (bool) ($args['reverse'] ?? false);
+        $sortKey = strtoupper($args['sortKey'] ?? 'ID');
+        $reverse = (bool) ($args['reverse'] ?? false);
         $direction = $reverse ? 'desc' : 'asc';
-        $locale    = $args['locale'] ?? request()->attributes->get('bagisto_locale');
-        $channel   = $args['channel'] ?? request()->attributes->get('bagisto_channel');
+        $locale = $args['locale'] ?? request()->attributes->get('bagisto_locale');
+        $channel = $args['channel'] ?? request()->attributes->get('bagisto_channel');
 
         switch ($sortKey) {
             case 'TITLE':
             case 'NAME':
-                $query->leftJoin('product_attribute_values as pav_name', function ($join) {
-                    $join->on('products.id', '=', 'pav_name.product_id')
-                         ->where('pav_name.attribute_id', 2);
-                })
-                ->orderBy('pav_name.text_value', $direction)
-                ->orderBy('products.id', $direction)
-                ->select('products.*');
+                $prefix = \DB::getTablePrefix();
 
-                if ($locale) {
-                    $query->where('pav_name.locale', $locale);
-                }
+                // Join for requested locale/channel
+                $query->leftJoin('product_attribute_values as pav_name_locale', function ($join) use ($locale, $channel) {
+                    $join->on('products.id', '=', 'pav_name_locale.product_id')
+                        ->where('pav_name_locale.attribute_id', 2);
 
-                if ($channel) {
-                    $query->where('pav_name.channel', $channel);
-                }
+                    if ($locale) {
+                        $join->where('pav_name_locale.locale', $locale);
+                    }
+
+                    if ($channel) {
+                        $join->where('pav_name_locale.channel', $channel);
+                    }
+                });
+
+                // Fallback join for null locale/channel (default values)
+                $query->leftJoin('product_attribute_values as pav_name_fallback', function ($join) {
+                    $join->on('products.id', '=', 'pav_name_fallback.product_id')
+                        ->where('pav_name_fallback.attribute_id', 2)
+                        ->whereNull('pav_name_fallback.locale')
+                        ->whereNull('pav_name_fallback.channel');
+                });
+
+                $query->orderBy(\DB::raw("COALESCE({$prefix}pav_name_locale.text_value, {$prefix}pav_name_fallback.text_value)"), $direction)
+                    ->orderBy('products.id', $direction)
+                    ->select('products.*');
                 break;
 
             case 'CREATED_AT':
                 $query->orderBy('products.created_at', $direction)
-                      ->orderBy('products.id', $direction);
+                    ->orderBy('products.id', $direction);
                 break;
 
             case 'UPDATED_AT':
                 $query->orderBy('products.updated_at', $direction)
-                      ->orderBy('products.id', $direction);
+                    ->orderBy('products.id', $direction);
                 break;
 
             case 'PRICE':
@@ -93,11 +113,11 @@ class ProductGraphQLProvider implements ProviderInterface
 
                 $query->leftJoin('product_price_indices', function ($join) use ($customerGroup) {
                     $join->on('products.id', '=', 'product_price_indices.product_id')
-                         ->where('product_price_indices.customer_group_id', $customerGroup->id);
+                        ->where('product_price_indices.customer_group_id', $customerGroup->id);
                 })
-                ->orderBy('product_price_indices.min_price', $direction)
-                ->orderBy('products.id', $direction)
-                ->select('products.*');
+                    ->orderBy('product_price_indices.min_price', $direction)
+                    ->orderBy('products.id', $direction)
+                    ->select('products.*');
                 break;
 
             case 'ID':
@@ -130,17 +150,17 @@ class ProductGraphQLProvider implements ProviderInterface
 
         if (isset($filters['new'])) {
             $query->leftJoin('product_flat', 'products.id', '=', 'product_flat.product_id')
-                  ->where('product_flat.new', filter_var($filters['new'], FILTER_VALIDATE_BOOLEAN))
-                  ->select('products.*')
-                  ->distinct('products.id');
+                ->where('product_flat.new', filter_var($filters['new'], FILTER_VALIDATE_BOOLEAN))
+                ->select('products.*')
+                ->distinct('products.id');
             unset($filters['new']);
         }
 
         if (isset($filters['featured'])) {
             $query->leftJoin('product_flat', 'products.id', '=', 'product_flat.product_id')
-                  ->where('product_flat.featured', filter_var($filters['featured'], FILTER_VALIDATE_BOOLEAN))
-                  ->select('products.*')
-                  ->distinct('products.id');
+                ->where('product_flat.featured', filter_var($filters['featured'], FILTER_VALIDATE_BOOLEAN))
+                ->select('products.*')
+                ->distinct('products.id');
             unset($filters['featured']);
         }
 
@@ -154,7 +174,7 @@ class ProductGraphQLProvider implements ProviderInterface
 
         if (! empty($filters['price_from']) || ! empty($filters['price_to'])) {
             $from = isset($filters['price_from']) ? (float) $filters['price_from'] : null;
-            $to   = isset($filters['price_to']) ? (float) $filters['price_to'] : null;
+            $to = isset($filters['price_to']) ? (float) $filters['price_to'] : null;
 
             $query->whereHas('attribute_values', function ($q) use ($from, $to) {
                 $q->where('attribute_id', 11);
@@ -182,7 +202,7 @@ class ProductGraphQLProvider implements ProviderInterface
             }
 
             $attributeFilters[$attrCode] = [
-                'term' => $spec['match'] ?? $spec,
+                'term'      => $spec['match'] ?? $spec,
                 'matchType' => strtoupper($spec['match_type'] ?? ''),
             ];
         }
@@ -194,23 +214,23 @@ class ProductGraphQLProvider implements ProviderInterface
 
             // Join all attribute tables needed for both products and variants
             foreach ($attributeFilters as $attrCode => $filterData) {
-                $productAlias = 'pav_' . $attrCode . '_product';
-                $variantAlias = 'pav_' . $attrCode . '_variant';
+                $productAlias = 'pav_'.$attrCode.'_product';
+                $variantAlias = 'pav_'.$attrCode.'_variant';
 
                 // Join product attribute values
-                $query->leftJoin('product_attribute_values as ' . $productAlias, function ($join) use ($productAlias, $attrCode) {
-                    $join->on('products.id', '=', $productAlias . '.product_id')
-                         ->whereIn($productAlias . '.attribute_id', function ($sub) use ($attrCode) {
-                             $sub->select('id')->from('attributes')->where('code', $attrCode);
-                         });
+                $query->leftJoin('product_attribute_values as '.$productAlias, function ($join) use ($productAlias, $attrCode) {
+                    $join->on('products.id', '=', $productAlias.'.product_id')
+                        ->whereIn($productAlias.'.attribute_id', function ($sub) use ($attrCode) {
+                            $sub->select('id')->from('attributes')->where('code', $attrCode);
+                        });
                 });
 
                 // Join variant attribute values
-                $query->leftJoin('product_attribute_values as ' . $variantAlias, function ($join) use ($variantAlias, $attrCode) {
-                    $join->on('variants.id', '=', $variantAlias . '.product_id')
-                         ->whereIn($variantAlias . '.attribute_id', function ($sub) use ($attrCode) {
-                             $sub->select('id')->from('attributes')->where('code', $attrCode);
-                         });
+                $query->leftJoin('product_attribute_values as '.$variantAlias, function ($join) use ($variantAlias, $attrCode) {
+                    $join->on('variants.id', '=', $variantAlias.'.product_id')
+                        ->whereIn($variantAlias.'.attribute_id', function ($sub) use ($attrCode) {
+                            $sub->select('id')->from('attributes')->where('code', $attrCode);
+                        });
                 });
             }
 
@@ -219,13 +239,13 @@ class ProductGraphQLProvider implements ProviderInterface
                 // Check all filters against product attributes
                 $filterQuery->where(function ($productFilterQuery) use ($attributeFilters, $locale, $channel) {
                     foreach ($attributeFilters as $attrCode => $filterData) {
-                        $productAlias = 'pav_' . $attrCode . '_product';
+                        $productAlias = 'pav_'.$attrCode.'_product';
                         $term = $filterData['term'];
                         $matchType = $filterData['matchType'];
                         $attributeType = $this->attributeTypeCache[$attrCode] ?? 'text';
 
-                        $productFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $productAlias, $attributeType) {
-                            $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $productAlias, $attributeType);
+                        $productFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $productAlias, $attributeType, $attrCode) {
+                            $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $productAlias, $attributeType, $attrCode);
                         });
                     }
                 });
@@ -233,13 +253,13 @@ class ProductGraphQLProvider implements ProviderInterface
                 // OR check all filters against variant attributes
                 $filterQuery->orWhere(function ($variantFilterQuery) use ($attributeFilters, $locale, $channel) {
                     foreach ($attributeFilters as $attrCode => $filterData) {
-                        $variantAlias = 'pav_' . $attrCode . '_variant';
+                        $variantAlias = 'pav_'.$attrCode.'_variant';
                         $term = $filterData['term'];
                         $matchType = $filterData['matchType'];
                         $attributeType = $this->attributeTypeCache[$attrCode] ?? 'text';
 
-                        $variantFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $variantAlias, $attributeType) {
-                            $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $variantAlias, $attributeType);
+                        $variantFilterQuery->where(function ($q) use ($term, $matchType, $locale, $channel, $variantAlias, $attributeType, $attrCode) {
+                            $this->applyAttributeFilter($q, $term, $matchType, $locale, $channel, $variantAlias, $attributeType, $attrCode);
                         });
                     }
                 });
@@ -253,27 +273,26 @@ class ProductGraphQLProvider implements ProviderInterface
             'images',
             'attribute_values',
             'super_attributes',
-            'variants' => fn ($q) =>
-                $q->without(['variants', 'super_attributes', 'attribute_values', 'attribute_family']),
+            'variants' => fn ($q) => $q->without(['variants', 'super_attributes', 'attribute_values', 'attribute_family']),
         ]);
 
-        $first  = isset($args['first']) ? (int) $args['first'] : null;
-        $last   = isset($args['last']) ? (int) $args['last'] : null;
-        $after  = $args['after'] ?? null;
+        $first = isset($args['first']) ? (int) $args['first'] : null;
+        $last = isset($args['last']) ? (int) $args['last'] : null;
+        $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
-        $limit  = $first ?? $last ?? 30;
+        $limit = $first ?? $last ?? 30;
         $offset = 0;
 
         if ($after) {
             $decoded = base64_decode($after, true);
-            $offset  = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
         }
 
         if ($before) {
             $decoded = base64_decode($before, true);
-            $cursor  = ctype_digit((string) $decoded) ? (int) $decoded : 0;
-            $offset  = max(0, $cursor - $limit);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $limit);
         }
 
         $total = (clone $query)
@@ -315,25 +334,27 @@ class ProductGraphQLProvider implements ProviderInterface
         );
     }
 
-
     /**
      * Apply attribute filter logic to a query based on attribute type
      */
-    private function applyAttributeFilter($q, $term, $matchType, $locale, $channel, $alias, $attributeType = null)
+    private function applyAttributeFilter($q, $term, $matchType, $locale, $channel, $alias, $attributeType = null, $attrCode = null)
     {
         // Fallback to text_value if attribute type not provided
-        if (!$attributeType) {
+        if (! $attributeType) {
             $attributeType = 'text';
         }
 
-        // Apply locale filter if provided
-        if ($locale) {
-            $q->where($alias . '.locale', $locale);
+        // Only constrain locale/channel when the attribute is actually scoped —
+        // non-scoped attributes store NULL in locale/channel columns, so adding
+        // a WHERE locale = 'en' would exclude every match.
+        $scope = $attrCode ? ($this->attributeScopeCache[$attrCode] ?? null) : null;
+
+        if ($locale && ($scope->value_per_locale ?? false)) {
+            $q->where($alias.'.locale', $locale);
         }
 
-        // Apply channel filter if provided
-        if ($channel) {
-            $q->where($alias . '.channel', $channel);
+        if ($channel && ($scope->value_per_channel ?? false)) {
+            $q->where($alias.'.channel', $channel);
         }
 
         if ($matchType === 'PARTIAL') {
@@ -352,12 +373,12 @@ class ProductGraphQLProvider implements ProviderInterface
             'text', 'textarea'  => 'text_value',
             'select','multiselect','dropdown' => 'integer_value',
             'decimal', 'price' => 'float_value',
-            'integer' => 'integer_value',
-            'boolean' => 'boolean_value',
+            'integer'  => 'integer_value',
+            'boolean'  => 'boolean_value',
             'datetime' => 'datetime_value',
-            'date' => 'date_value',
-            'json' => 'json_value',
-            default => 'text_value',
+            'date'     => 'date_value',
+            'json'     => 'json_value',
+            default    => 'text_value',
         };
     }
 
@@ -367,7 +388,7 @@ class ProductGraphQLProvider implements ProviderInterface
     private function applyPartialFilter($q, $term, $alias, $attributeType)
     {
         $column = $this->getColumnForType($attributeType);
-        $q->where($alias . '.' . $column, 'like', "%{$term}%");
+        $q->where($alias.'.'.$column, 'like', "%{$term}%");
     }
 
     /**
@@ -376,18 +397,18 @@ class ProductGraphQLProvider implements ProviderInterface
     private function applyExactFilter($q, $term, $alias, $attributeType)
     {
         $column = $this->getColumnForType($attributeType);
-       
+
         if (is_string($term) && str_contains($term, ',')) {
             $values = array_filter(array_map('trim', explode(',', $term)));
-            
+
             // Convert values based on attribute type
             $convertedValues = $this->convertValuesForType($values, $attributeType);
-            
-            $q->whereIn($alias . '.' . $column, $convertedValues);
+
+            $q->whereIn($alias.'.'.$column, $convertedValues);
         } else {
             $convertedValue = $this->convertValueForType($term, $attributeType);
-            
-            $q->where($alias . '.' . $column, $convertedValue);
+
+            $q->where($alias.'.'.$column, $convertedValue);
         }
     }
 
@@ -400,7 +421,7 @@ class ProductGraphQLProvider implements ProviderInterface
             'decimal', 'price' => (float) $value,
             'integer' => (int) $value,
             'boolean' => $value ? '1' : '0',
-            default => $value,
+            default   => $value,
         };
     }
 
@@ -412,8 +433,8 @@ class ProductGraphQLProvider implements ProviderInterface
         return match ($attributeType) {
             'decimal', 'price' => array_map('floatval', $values),
             'integer' => array_map('intval', $values),
-            'boolean' => array_map(fn($v) => (int) filter_var($v, FILTER_VALIDATE_BOOLEAN), $values),
-            default => $values,
+            'boolean' => array_map(fn ($v) => (int) filter_var($v, FILTER_VALIDATE_BOOLEAN), $values),
+            default   => $values,
         };
     }
 }

+ 33 - 6
packages/Webkul/BagistoApi/src/State/ProductReviewProvider.php

@@ -6,6 +6,7 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Webkul\BagistoApi\Models\ProductReview;
 
 /**
@@ -36,23 +37,49 @@ class ProductReviewProvider implements ProviderInterface
         // Eager load relationships
         $query->with(['product', 'customer']);
 
-        // Apply cursor-based pagination
+        // Cursor-based pagination (offset-based cursors from API Platform)
         $first = isset($args['first']) ? (int) $args['first'] : null;
         $last = isset($args['last']) ? (int) $args['last'] : null;
         $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
+        $perPage = $first ?? $last ?? 30;
+        $offset = 0;
+
         if ($after) {
-            $query->where('id', '>', (int) base64_decode($after));
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
         }
+
         if ($before) {
-            $query->where('id', '<', (int) base64_decode($before));
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
         $query->orderBy('id', 'asc');
-        $perPage = $first ?? $last ?? 30;
-        $paginator = $query->paginate($perPage);
 
-        return new Paginator($paginator);
+        $total = (clone $query)->count();
+
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
+        }
+
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
+
+        return new Paginator(
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
+        );
     }
 }

+ 45 - 0
packages/Webkul/BagistoApi/src/State/ReorderProcessor.php

@@ -5,6 +5,7 @@ namespace Webkul\BagistoApi\State;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Request;
 use Webkul\BagistoApi\Dto\ReorderInput;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\InvalidInputException;
@@ -34,6 +35,8 @@ class ReorderProcessor implements ProcessorInterface
     public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
     {
         if ($data instanceof ReorderInput) {
+            $this->hydrateInputFromContext($data, $context);
+
             return $this->handleReorder($data);
         }
 
@@ -145,4 +148,46 @@ class ReorderProcessor implements ProcessorInterface
             itemsAddedCount: 0,
         );
     }
+
+    private function hydrateInputFromContext(ReorderInput $data, array $context): void
+    {
+        if (! empty($data->orderId)) {
+            return;
+        }
+
+        $input = $context['args']['input'] ?? $context['args'] ?? null;
+
+        $orderId = $this->extractOrderId($input);
+
+        if ($orderId === null) {
+            $request = Request::instance();
+
+            if ($request) {
+                $orderId = $this->extractOrderId($request->input('variables.input'))
+                    ?? $this->extractOrderId($request->input('input'))
+                    ?? $this->extractOrderId($request->input('extensions.variables.input'));
+            }
+        }
+
+        if ($orderId !== null) {
+            $data->orderId = $orderId;
+        }
+    }
+
+    private function extractOrderId(mixed $input): ?int
+    {
+        if (is_array($input)) {
+            $value = $input['orderId'] ?? $input['order_id'] ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        if (is_object($input)) {
+            $value = $input->orderId ?? $input->order_id ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        return null;
+    }
 }

+ 6 - 6
packages/Webkul/BagistoApi/src/State/ShippingRatesProvider.php

@@ -64,14 +64,14 @@ class ShippingRatesProvider implements ProviderInterface
                     $output->label = (string) ($group['carrier_title'] ?? $carrier);
                     $output->method = (string) ($rate->method ?? $carrier);
                     $output->price = (float) ($rate->price ?? 0);
-                    $output->formattedPrice = (string) core()->formatPrice($rate->price ?? 0);
+                    $output->formatted_price = (string) core()->formatPrice($rate->price ?? 0);
                     $output->description = (string) ($rate->method_description ?? '');
-                    $output->methodTitle = (string) ($rate->method_title ?? $group['carrier_title'] ?? $carrier);
-                    $output->methodDescription = (string) ($rate->method_description ?? '');
-                    $output->basePrice = (float) ($rate->base_price ?? 0);
-                    $output->baseFormattedPrice = (string) ($rate->base_formatted_price ?? core()->currency($rate->base_price ?? 0));
+                    $output->method_title = (string) ($rate->method_title ?? $group['carrier_title'] ?? $carrier);
+                    $output->method_description = (string) ($rate->method_description ?? '');
+                    $output->base_price = (float) ($rate->base_price ?? 0);
+                    $output->base_formatted_price = (string) ($rate->base_formatted_price ?? core()->currency($rate->base_price ?? 0));
                     $output->carrier = (string) $carrier;
-                    $output->carrierTitle = (string) ($group['carrier_title'] ?? $carrier);
+                    $output->carrier_title = (string) ($group['carrier_title'] ?? $carrier);
 
                     $outputs[] = $output;
                 }

+ 35 - 0
packages/Webkul/BagistoApi/src/State/SnakeCaseLinksHandler.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Str;
+
+/**
+ * Fixes camelCase/snake_case mismatch between GraphQL field names and Eloquent
+ * relationship names in API Platform's LinksHandler.
+ *
+ * GraphQL passes camelCase linkProperty (e.g. "attributeValues") but Link metadata
+ * stores snake_case fromProperty (e.g. "attribute_values"). This decorator normalizes
+ * linkProperty to snake_case before delegating to the original handler.
+ *
+ * Single-word names like "variants" or "images" are unaffected.
+ *
+ * @implements LinksHandlerInterface<\Illuminate\Database\Eloquent\Model>
+ */
+class SnakeCaseLinksHandler implements LinksHandlerInterface
+{
+    public function __construct(
+        private readonly LinksHandlerInterface $inner,
+    ) {}
+
+    public function handleLinks(Builder $builder, array $uriVariables, array $context): Builder
+    {
+        if (isset($context['linkProperty'])) {
+            $context['linkProperty'] = Str::snake($context['linkProperty']);
+        }
+
+        return $this->inner->handleLinks($builder, $uriVariables, $context);
+    }
+}

+ 112 - 20
packages/Webkul/BagistoApi/src/State/WishlistProcessor.php

@@ -4,16 +4,17 @@ namespace Webkul\BagistoApi\State;
 
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Request as RequestFacade;
 use Webkul\BagistoApi\Dto\CreateWishlistInput;
 use Webkul\BagistoApi\Dto\DeleteWishlistInput;
 use Webkul\BagistoApi\Exception\AuthorizationException;
 use Webkul\BagistoApi\Exception\InvalidInputException;
 use Webkul\BagistoApi\Exception\ResourceNotFoundException;
-use Webkul\BagistoApi\Models\Wishlist;
 use Webkul\BagistoApi\Models\Product;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Event;
-use Illuminate\Http\Request;
+use Webkul\BagistoApi\Models\Wishlist;
 
 /**
  * WishlistProcessor - Handles create/delete operations for wishlist items
@@ -32,24 +33,29 @@ class WishlistProcessor implements ProcessorInterface
     {
         $operationName = $operation->getName();
 
-        
-        if (in_array($operationName, ['toggle'])) {
+        if (in_array($operationName, ['toggle']) && $data instanceof CreateWishlistInput) {
+            $this->hydrateCreateInputFromContext($data, $context);
+
             return $this->handleToggle($data, $uriVariables, $context);
         }
 
         if ($data instanceof CreateWishlistInput) {
+            $this->hydrateCreateInputFromContext($data, $context);
+
             return $this->handleCreate($data, $context);
         }
 
         /** Handle REST POST — model received instead of DTO */
         if ($data instanceof Wishlist && $operation instanceof \ApiPlatform\Metadata\Post) {
-            $input = new CreateWishlistInput();
-            $input->productId = request()->input('product_id') ?? request()->input('productId');
+            $input = new CreateWishlistInput;
+            $input->product_id = request()->input('product_id') ?? request()->input('productId');
 
             return $this->handleCreate($input, $context);
         }
 
         if ($data instanceof DeleteWishlistInput) {
+            $this->hydrateDeleteInputFromContext($data, $context);
+
             return $this->handleDeleteFromInput($data, $context);
         }
 
@@ -65,15 +71,19 @@ class WishlistProcessor implements ProcessorInterface
      */
     private function handleCreate(CreateWishlistInput $input, array $context = []): Wishlist
     {
-        if (empty($input->productId)) {
+        if (empty($input->product_id)) {
             throw new InvalidInputException(__('bagistoapi::app.graphql.wishlist.product-id-required'));
         }
 
-        $product = Product::find($input->productId);
+        $product = Product::find($input->product_id);
         if (! $product) {
             throw new ResourceNotFoundException(__('bagistoapi::app.graphql.wishlist.product-not-found'));
         }
 
+        if (! $product->status) {
+            throw new InvalidInputException(__('bagistoapi::app.graphql.wishlist.product-disabled'));
+        }
+
         $user = Auth::guard('sanctum')->user();
 
         if (! $user) {
@@ -84,7 +94,7 @@ class WishlistProcessor implements ProcessorInterface
         $channelId = core()->getCurrentChannel()->id;
 
         $existingItem = Wishlist::where('customer_id', $customerId)
-            ->where('product_id', $input->productId)
+            ->where('product_id', $input->product_id)
             ->where('channel_id', $channelId)
             ->first();
 
@@ -92,10 +102,10 @@ class WishlistProcessor implements ProcessorInterface
             throw new InvalidInputException(__('bagistoapi::app.graphql.wishlist.already-exists'));
         }
 
-        Event::dispatch('customer.wishlist.create.before', $input->productId);
+        Event::dispatch('customer.wishlist.create.before', $input->product_id);
 
         $wishlistItem = Wishlist::create([
-            'product_id'  => $input->productId,
+            'product_id'  => $input->product_id,
             'customer_id' => $customerId,
             'channel_id'  => $channelId,
         ]);
@@ -107,15 +117,19 @@ class WishlistProcessor implements ProcessorInterface
 
     private function handleToggle(CreateWishlistInput $input, array $context = []): Wishlist
     {
-        if (empty($input->productId)) {
+        if (empty($input->product_id)) {
             throw new InvalidInputException(__('bagistoapi::app.graphql.wishlist.product-id-required'));
         }
 
-        $product = Product::find($input->productId);
+        $product = Product::find($input->product_id);
         if (! $product) {
             throw new ResourceNotFoundException(__('bagistoapi::app.graphql.wishlist.product-not-found'));
         }
 
+        if (! $product->status) {
+            throw new InvalidInputException(__('bagistoapi::app.graphql.wishlist.product-disabled'));
+        }
+
         $user = Auth::guard('sanctum')->user();
 
         if (! $user) {
@@ -126,7 +140,7 @@ class WishlistProcessor implements ProcessorInterface
         $channelId = core()->getCurrentChannel()->id;
 
         $existingItem = Wishlist::where('customer_id', $customerId)
-            ->where('product_id', $input->productId)
+            ->where('product_id', $input->product_id)
             ->where('channel_id', $channelId)
             ->first();
 
@@ -138,10 +152,10 @@ class WishlistProcessor implements ProcessorInterface
             throw new InvalidInputException(__('bagistoapi::app.graphql.wishlist.removed'));
         }
 
-        Event::dispatch('customer.wishlist.create.before', $input->productId);
+        Event::dispatch('customer.wishlist.create.before', $input->product_id);
 
         $wishlistItem = Wishlist::create([
-            'product_id'  => $input->productId,
+            'product_id'  => $input->product_id,
             'customer_id' => $customerId,
             'channel_id'  => $channelId,
         ]);
@@ -151,7 +165,86 @@ class WishlistProcessor implements ProcessorInterface
         return $wishlistItem;
     }
 
-    
+    private function hydrateCreateInputFromContext(CreateWishlistInput $input, array $context): void
+    {
+        if (! empty($input->product_id)) {
+            return;
+        }
+
+        $productId = $this->extractProductId($context['args']['input'] ?? $context['args'] ?? null);
+
+        if ($productId === null) {
+            $request = $this->request ?? RequestFacade::instance();
+
+            if ($request) {
+                $productId = $this->extractProductId($request->input('variables.input'))
+                    ?? $this->extractProductId($request->input('input'))
+                    ?? $this->extractProductId($request->input('extensions.variables.input'));
+            }
+        }
+
+        if ($productId !== null) {
+            $input->product_id = $productId;
+        }
+    }
+
+    private function hydrateDeleteInputFromContext(DeleteWishlistInput $input, array $context): void
+    {
+        if (! empty($input->id)) {
+            return;
+        }
+
+        $id = $this->extractId($context['args']['input'] ?? $context['args'] ?? null);
+
+        if ($id === null) {
+            $request = $this->request ?? RequestFacade::instance();
+
+            if ($request) {
+                $id = $this->extractId($request->input('variables.input'))
+                    ?? $this->extractId($request->input('input'))
+                    ?? $this->extractId($request->input('extensions.variables.input'));
+            }
+        }
+
+        if ($id !== null) {
+            $input->id = $id;
+        }
+    }
+
+    private function extractProductId(mixed $input): ?int
+    {
+        if (is_array($input)) {
+            $value = $input['product_id'] ?? $input['productId'] ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        if (is_object($input)) {
+            $value = $input->product_id ?? $input->productId ?? null;
+
+            return is_numeric($value) ? (int) $value : null;
+        }
+
+        return null;
+    }
+
+    private function extractId(mixed $input): ?string
+    {
+        if (is_array($input)) {
+            $value = $input['id'] ?? null;
+
+            return $value !== null && $value !== '' ? (string) $value : null;
+        }
+
+        if (is_object($input)) {
+            $value = $input->id ?? null;
+
+            return $value !== null && $value !== '' ? (string) $value : null;
+        }
+
+        return null;
+    }
+
     /**
      * Handle delete operation from GraphQL mutation input
      */
@@ -225,4 +318,3 @@ class WishlistProcessor implements ProcessorInterface
         return null;
     }
 }
-

+ 29 - 41
packages/Webkul/BagistoApi/src/State/WishlistProvider.php

@@ -6,9 +6,10 @@ use ApiPlatform\Laravel\Eloquent\Paginator;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
-use Webkul\BagistoApi\Models\Wishlist;
+use Illuminate\Pagination\LengthAwarePaginator;
 use Illuminate\Support\Facades\Auth;
 use Webkul\BagistoApi\Exception\AuthorizationException;
+use Webkul\BagistoApi\Models\Wishlist;
 
 /**
  * WishlistProvider - Handles retrieval of wishlist items for authenticated customers
@@ -21,14 +22,6 @@ class WishlistProvider implements ProviderInterface
         private readonly Pagination $pagination
     ) {}
 
-    /**
-     * Retrieve wishlist items for the authenticated customer
-     *
-     * @param Operation $operation
-     * @param array $uriVariables
-     * @param array $context
-     * @return object|array|null
-     */
     public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
     {
         $customer = Auth::guard('sanctum')->user();
@@ -43,14 +36,18 @@ class WishlistProvider implements ProviderInterface
         $after = $args['after'] ?? null;
         $before = $args['before'] ?? null;
 
-        $defaultPerPage = 30;
+        $perPage = $first ?? $last ?? 30;
+        $offset = 0;
+
+        if ($after) {
+            $decoded = base64_decode($after, true);
+            $offset = ctype_digit((string) $decoded) ? ((int) $decoded + 1) : 0;
+        }
 
-        if ($first !== null) {
-            $perPage = $first;
-        } elseif ($last !== null) {
-            $perPage = $last;
-        } else {
-            $perPage = $defaultPerPage;
+        if ($before) {
+            $decoded = base64_decode($before, true);
+            $cursor = ctype_digit((string) $decoded) ? (int) $decoded : 0;
+            $offset = max(0, $cursor - $perPage);
         }
 
         $query = Wishlist::where('customer_id', $customer->id)
@@ -58,36 +55,27 @@ class WishlistProvider implements ProviderInterface
             ->with(['product', 'customer', 'channel'])
             ->orderBy('id', 'asc');
 
-        if ($after) {
-            $afterId = (int) base64_decode($after);
-            $query->where('id', '>', $afterId);
-        } elseif ($before) {
-            $beforeId = (int) base64_decode($before);
-            $query->where('id', '<', $beforeId);
-            
-            $query->orderBy('id', 'desc');
-            $laravelPaginator = $query->paginate($perPage);
-
-            $items = $laravelPaginator->items();
-            $items = array_reverse($items);
+        $total = (clone $query)->count();
 
-            return new Paginator(
-                $laravelPaginator,
-                (int) $laravelPaginator->currentPage(),
-                $perPage,
-                $laravelPaginator->lastPage(),
-                $laravelPaginator->total(),
-            );
+        if ($offset > $total) {
+            $offset = max(0, $total - $perPage);
         }
 
-        $laravelPaginator = $query->paginate($perPage);
+        $items = $query
+            ->offset($offset)
+            ->limit($perPage)
+            ->get();
+
+        $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
 
         return new Paginator(
-            $laravelPaginator,
-            (int) $laravelPaginator->currentPage(),
-            $perPage,
-            $laravelPaginator->lastPage(),
-            $laravelPaginator->total(),
+            new LengthAwarePaginator(
+                $items,
+                $total,
+                $perPage,
+                $currentPage,
+                ['path' => request()->url()]
+            )
         );
     }
 }

+ 4 - 11
packages/Webkul/BagistoApi/src/Traits/HasRateLimit.php

@@ -18,14 +18,14 @@ trait HasRateLimit
      * Check hourly rate limit for a client.
      *
      * @param  object  $client
-     * @param  int  $defaultLimit
-     * @return array
      */
     protected function checkHourlyRateLimit($client, int $defaultLimit = 1000): array
     {
-        $rateLimit = $client->rate_limit ?? $defaultLimit;
+        $rateLimit = property_exists($client, 'rate_limit') || isset($client->rate_limit)
+            ? $client->rate_limit
+            : $defaultLimit;
 
-        if ($rateLimit === null) {
+        if ($rateLimit === null || $rateLimit === 0) {
             return [
                 'allowed'   => true,
                 'limit'     => self::UNLIMITED_RATE,
@@ -58,9 +58,6 @@ trait HasRateLimit
      * Check per-minute rate limit for a client.
      *
      * @param  object  $client
-     * @param  int  $rateLimit
-     * @param  int  $windowMinutes
-     * @return array
      */
     protected function checkMinuteRateLimit($client, int $rateLimit = 100, int $windowMinutes = 1): array
     {
@@ -99,10 +96,6 @@ trait HasRateLimit
 
     /**
      * Calculate reset time for cache TTL.
-     *
-     * @param  string  $cacheKey
-     * @param  int  $defaultSeconds
-     * @return int
      */
     private function calculateReset(string $cacheKey, int $defaultSeconds = 60): int
     {