chengwl 3 هفته پیش
والد
کامیت
58ff0d3ae5
100فایلهای تغییر یافته به همراه8679 افزوده شده و 1 حذف شده
  1. 0 1
      packages/Webkul/BagistoApi
  2. 103 0
      packages/Webkul/BagistoApi/README.md
  3. 34 0
      packages/Webkul/BagistoApi/composer.json
  4. 228 0
      packages/Webkul/BagistoApi/config/api-platform-vendor.php
  5. 230 0
      packages/Webkul/BagistoApi/config/api-platform.php
  6. 78 0
      packages/Webkul/BagistoApi/config/graphql-auth.php
  7. 54 0
      packages/Webkul/BagistoApi/config/storefront.php
  8. 19 0
      packages/Webkul/BagistoApi/src/Attributes/AllowPublic.php
  9. 19 0
      packages/Webkul/BagistoApi/src/Attributes/RequiresStorefrontKey.php
  10. 113 0
      packages/Webkul/BagistoApi/src/CacheProfiles/ApiAwareResponseCache.php
  11. 141 0
      packages/Webkul/BagistoApi/src/Console/Commands/ApiKeyMaintenanceCommand.php
  12. 316 0
      packages/Webkul/BagistoApi/src/Console/Commands/ApiKeyManagementCommand.php
  13. 45 0
      packages/Webkul/BagistoApi/src/Console/Commands/ClearApiPlatformCacheCommand.php
  14. 81 0
      packages/Webkul/BagistoApi/src/Console/Commands/GenerateStorefrontKey.php
  15. 494 0
      packages/Webkul/BagistoApi/src/Console/Commands/InstallApiPlatformCommand.php
  16. 5 0
      packages/Webkul/BagistoApi/src/Contracts/GuestCartTokens.php
  17. 26 0
      packages/Webkul/BagistoApi/src/Database/Migrations/2025_12_10_185743_create_cart_tokens_table.php
  18. 68 0
      packages/Webkul/BagistoApi/src/Database/Migrations/2026_01_08_000000_create_storefront_keys_table.php
  19. 60 0
      packages/Webkul/BagistoApi/src/Dto/AddToCartInput.php
  20. 21 0
      packages/Webkul/BagistoApi/src/Dto/ApplyCouponInput.php
  21. 26 0
      packages/Webkul/BagistoApi/src/Dto/CancelOrderInput.php
  22. 353 0
      packages/Webkul/BagistoApi/src/Dto/CartData.php
  23. 287 0
      packages/Webkul/BagistoApi/src/Dto/CartInput.php
  24. 167 0
      packages/Webkul/BagistoApi/src/Dto/CartItemData.php
  25. 124 0
      packages/Webkul/BagistoApi/src/Dto/CheckoutAddressInput.php
  26. 116 0
      packages/Webkul/BagistoApi/src/Dto/CheckoutAddressOutput.php
  27. 31 0
      packages/Webkul/BagistoApi/src/Dto/CheckoutAddressPayload.php
  28. 11 0
      packages/Webkul/BagistoApi/src/Dto/CheckoutAddressQueryInput.php
  29. 28 0
      packages/Webkul/BagistoApi/src/Dto/ContactUsInput.php
  30. 20 0
      packages/Webkul/BagistoApi/src/Dto/ContactUsOutput.php
  31. 20 0
      packages/Webkul/BagistoApi/src/Dto/CreateCompareItemInput.php
  32. 58 0
      packages/Webkul/BagistoApi/src/Dto/CreateProductReviewInput.php
  33. 20 0
      packages/Webkul/BagistoApi/src/Dto/CreateWishlistInput.php
  34. 117 0
      packages/Webkul/BagistoApi/src/Dto/CustomerAddressInput.php
  35. 56 0
      packages/Webkul/BagistoApi/src/Dto/CustomerAddressOutput.php
  36. 14 0
      packages/Webkul/BagistoApi/src/Dto/CustomerLoginInput.php
  37. 120 0
      packages/Webkul/BagistoApi/src/Dto/CustomerProfileInput.php
  38. 116 0
      packages/Webkul/BagistoApi/src/Dto/CustomerProfileOutput.php
  39. 52 0
      packages/Webkul/BagistoApi/src/Dto/CustomerVerifyOutput.php
  40. 12 0
      packages/Webkul/BagistoApi/src/Dto/DeleteAllCompareItemsInput.php
  41. 12 0
      packages/Webkul/BagistoApi/src/Dto/DeleteAllWishlistsInput.php
  42. 19 0
      packages/Webkul/BagistoApi/src/Dto/DeleteCompareItemInput.php
  43. 19 0
      packages/Webkul/BagistoApi/src/Dto/DeleteWishlistInput.php
  44. 21 0
      packages/Webkul/BagistoApi/src/Dto/DestroySelectedInput.php
  45. 29 0
      packages/Webkul/BagistoApi/src/Dto/DownloadLinkOutput.php
  46. 60 0
      packages/Webkul/BagistoApi/src/Dto/EstimateShippingMethodsInput.php
  47. 13 0
      packages/Webkul/BagistoApi/src/Dto/ForgotPasswordInput.php
  48. 21 0
      packages/Webkul/BagistoApi/src/Dto/GenerateDownloadLinkInput.php
  49. 32 0
      packages/Webkul/BagistoApi/src/Dto/LoginInput.php
  50. 23 0
      packages/Webkul/BagistoApi/src/Dto/LogoutInput.php
  51. 23 0
      packages/Webkul/BagistoApi/src/Dto/LogoutOutput.php
  52. 34 0
      packages/Webkul/BagistoApi/src/Dto/MoveToWishlistInput.php
  53. 25 0
      packages/Webkul/BagistoApi/src/Dto/MoveWishlistToCartInput.php
  54. 27 0
      packages/Webkul/BagistoApi/src/Dto/MoveWishlistToCartOutput.php
  55. 42 0
      packages/Webkul/BagistoApi/src/Dto/PaymentMethodOutput.php
  56. 21 0
      packages/Webkul/BagistoApi/src/Dto/RemoveFromCartInput.php
  57. 26 0
      packages/Webkul/BagistoApi/src/Dto/ReorderInput.php
  58. 66 0
      packages/Webkul/BagistoApi/src/Dto/ShippingRateOutput.php
  59. 19 0
      packages/Webkul/BagistoApi/src/Dto/SubscribeToNewsletterInput.php
  60. 22 0
      packages/Webkul/BagistoApi/src/Dto/SubscribeToNewsletterOutput.php
  61. 21 0
      packages/Webkul/BagistoApi/src/Dto/UpdateCartItemInput.php
  62. 58 0
      packages/Webkul/BagistoApi/src/Dto/UpdateProductReviewInput.php
  63. 13 0
      packages/Webkul/BagistoApi/src/Dto/VerifyTokenInput.php
  64. 16 0
      packages/Webkul/BagistoApi/src/Exception/AuthenticationException.php
  65. 29 0
      packages/Webkul/BagistoApi/src/Exception/AuthorizationException.php
  66. 93 0
      packages/Webkul/BagistoApi/src/Exception/InvalidInputException.php
  67. 32 0
      packages/Webkul/BagistoApi/src/Exception/OperationFailedException.php
  68. 29 0
      packages/Webkul/BagistoApi/src/Exception/ResourceNotFoundException.php
  69. 19 0
      packages/Webkul/BagistoApi/src/Exception/ValidationException.php
  70. 21 0
      packages/Webkul/BagistoApi/src/Facades/CartTokenFacade.php
  71. 22 0
      packages/Webkul/BagistoApi/src/Facades/TokenHeaderFacade.php
  72. 521 0
      packages/Webkul/BagistoApi/src/GraphQl/Serializer/FixedSerializerContextBuilder.php
  73. 67 0
      packages/Webkul/BagistoApi/src/GraphQl/StorefrontKeyGraphqlAuthenticator.php
  74. 16 0
      packages/Webkul/BagistoApi/src/GraphQl/TokenHeaderExtension.php
  75. 105 0
      packages/Webkul/BagistoApi/src/Helper/CustomerProfileHelper.php
  76. 166 0
      packages/Webkul/BagistoApi/src/Http/Controllers/AdminGraphQLPlaygroundController.php
  77. 40 0
      packages/Webkul/BagistoApi/src/Http/Controllers/ApiEntrypointController.php
  78. 151 0
      packages/Webkul/BagistoApi/src/Http/Controllers/AuthenticationController.php
  79. 53 0
      packages/Webkul/BagistoApi/src/Http/Controllers/DownloadableProductController.php
  80. 511 0
      packages/Webkul/BagistoApi/src/Http/Controllers/GraphQLPlaygroundController.php
  81. 59 0
      packages/Webkul/BagistoApi/src/Http/Controllers/InvoicePdfController.php
  82. 149 0
      packages/Webkul/BagistoApi/src/Http/Controllers/SwaggerUIController.php
  83. 41 0
      packages/Webkul/BagistoApi/src/Http/Middleware/BagistoApiDocumentationMiddleware.php
  84. 70 0
      packages/Webkul/BagistoApi/src/Http/Middleware/ForceApiJson.php
  85. 42 0
      packages/Webkul/BagistoApi/src/Http/Middleware/HandleInvalidInputException.php
  86. 139 0
      packages/Webkul/BagistoApi/src/Http/Middleware/LogApiRequests.php
  87. 192 0
      packages/Webkul/BagistoApi/src/Http/Middleware/RateLimitApi.php
  88. 70 0
      packages/Webkul/BagistoApi/src/Http/Middleware/SecurityHeaders.php
  89. 41 0
      packages/Webkul/BagistoApi/src/Http/Middleware/SetLocaleChannel.php
  90. 123 0
      packages/Webkul/BagistoApi/src/Http/Middleware/VerifyBearerToken.php
  91. 441 0
      packages/Webkul/BagistoApi/src/Http/Middleware/VerifyGraphQLStorefrontKey.php
  92. 219 0
      packages/Webkul/BagistoApi/src/Http/Middleware/VerifyStorefrontKey.php
  93. 109 0
      packages/Webkul/BagistoApi/src/Http/Requests/Admin/ProductFormRequest.php
  94. 68 0
      packages/Webkul/BagistoApi/src/Input/CreateProductInput.php
  95. 14 0
      packages/Webkul/BagistoApi/src/Input/LoginInput.php
  96. 73 0
      packages/Webkul/BagistoApi/src/Jobs/LogApiRequestJob.php
  97. 42 0
      packages/Webkul/BagistoApi/src/Metadata/CustomIdentifiersExtractor.php
  98. 266 0
      packages/Webkul/BagistoApi/src/Models/AddProductInCart.php
  99. 101 0
      packages/Webkul/BagistoApi/src/Models/ApplyCoupon.php
  100. 0 0
      packages/Webkul/BagistoApi/src/Models/Attribute.php

+ 0 - 1
packages/Webkul/BagistoApi

@@ -1 +0,0 @@
-Subproject commit d9adbe5a0b6a0766887a5165eaeea6c87640a2c8

+ 103 - 0
packages/Webkul/BagistoApi/README.md

@@ -0,0 +1,103 @@
+# Bagisto API Platform
+
+Comprehensive REST and GraphQL APIs for seamless e-commerce integration and extensibility.
+
+## Installation
+
+### Method 1: Quick Start (Composer Installation – Recommended)
+
+The fastest way to get started:
+
+```bash
+composer require bagisto/bagisto-api
+php artisan bagisto-api-platform:install
+```
+
+Your APIs are now ready! Access them at:
+- **REST API Docs**: `https://your-domain.com/api/docs`
+- **GraphQL Playground**: `https://your-domain.com/graphql`
+ 
+### Method 2: Manual Installation
+
+Use this method if you need more control over the setup.
+
+#### Step 1: Download and Extract
+
+1. Download the BagistoApi package from [GitHub](https://github.com/bagisto/bagisto-api)
+2. Extract it to: `packages/Webkul/BagistoApi/`
+
+#### Step 2: Register Service Provider
+
+Edit `bootstrap/providers.php`:
+
+```php
+<?php
+
+return [
+    // ...existing providers...
+    Webkul\BagistoApi\Providers\BagistoApiServiceProvider::class,
+    // ...rest of providers...
+];
+```
+
+#### Step 3: Update Autoloading
+
+Edit `composer.json` and update the `autoload` section:
+
+```json
+{
+  "autoload": {
+    "psr-4": {
+      "Webkul\\BagistoApi\\": "packages/Webkul/BagistoApi/src"
+    }
+  }
+}
+```
+
+#### Step 4: Install Dependencies
+
+```bash
+# Install required packages
+composer require api-platform/laravel:v4.1.25
+composer require api-platform/graphql:v4.2.3
+```
+
+#### Step 5: Run the installation
+```bash
+php artisan bagisto-api-platform:install
+```
+
+#### Step 9: Environment Setup (Update in the .env)
+```bash
+STOREFRONT_DEFAULT_RATE_LIMIT=100
+STOREFRONT_CACHE_TTL=60
+STOREFRONT_KEY_PREFIX=storefront_key_
+STOREFRONT_PLAYGROUND_KEY=pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxx 
+API_PLAYGROUND_AUTO_INJECT_STOREFRONT_KEY=true
+```
+### Access Points
+
+Once verified, access the APIs at:
+
+- **REST API (Shop)**: [https://your-domain.com/api/shop/](https://api-demo.bagisto.com/api/shop)
+- **REST API (Admin)**: [https://your-domain.com/api/admin/](https://api-demo.bagisto.com/api/admin)
+- **GraphQL Endpoint**: https://your-domain.com/graphql`
+- **GraphQL Playground**: [https://your-domain.com/graphqli](https://api-demo.bagisto.com/api/graphiql?)
+
+## Documentation
+- Bagisto API: [Demo Page](https://api-demo.bagisto.com/api) 
+- API Documentation: [Bagisto API Docs](https://api-docs.bagisto.com/)
+- GraphQL Playground: [Interactive Playground](https://api-demo.bagisto.com/graphiql)
+ 
+## Support
+
+For issues and questions, please visit:
+- [GitHub Issues](https://github.com/bagisto/bagisto-api-platform/issues)
+- [Bagisto Documentation](https://bagisto.com/docs)
+- [Community Forum](https://forum.bagisto.com)
+
+## 📝 License
+
+The Bagisto API Platform is open-source software licensed under the [MIT license](LICENSE).
+
+ 

+ 34 - 0
packages/Webkul/BagistoApi/composer.json

@@ -0,0 +1,34 @@
+{
+  "name": "bagisto/bagisto-api",
+  "description": "Bagisto API Platform package with GraphQL and REST API support",
+  "type": "laravel-package",
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "Bagisto",
+      "email": "support@bagisto.com"
+    }
+  ],
+  "require": {
+    "api-platform/laravel": "v4.1.25",
+    "api-platform/graphql": "v4.2.3"
+  },
+  "autoload": {
+    "psr-4": {
+      "Webkul\\BagistoApi\\": "src/"
+    }
+  },
+  "extra": {
+    "laravel": {
+      "providers": [
+        "Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider"
+      ]
+    }
+  },
+  "scripts": {
+    "post-autoload-dump": [
+      "@php artisan package:discover --ansi"
+    ]
+  }
+}
+

+ 228 - 0
packages/Webkul/BagistoApi/config/api-platform-vendor.php

@@ -0,0 +1,228 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+use ApiPlatform\Metadata\Operation\DashPathSegmentNameGenerator;
+use ApiPlatform\Metadata\UrlGeneratorInterface;
+use Illuminate\Auth\Access\AuthorizationException;
+use Illuminate\Auth\AuthenticationException;
+use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
+use Webkul\BagistoApi\Exception\InvalidInputException;
+use Webkul\BagistoApi\Exception\ValidationException;
+
+return [
+    'title'       => '',
+    'description' => '',
+    'version'     => '1.0.0',
+    'show_webby'  => true,
+
+    'routes' => [
+        'domain' => null,
+        // Global middleware applied to every API Platform routes
+        // HandleInvalidInputException: Catches validation errors and returns RFC 7807 format
+        // VerifyStorefrontKey: Validates X-STOREFRONT-KEY header and rate limiting for shop APIs
+        // BagistoApiDocumentationMiddleware: Handles custom /api index and documentation pages
+        // ForceApiJson: Ensures API responses have JSON content-type
+        // CacheResponse: Using custom ApiAwareResponseCache profile that:
+        // - Excludes API routes from caching (APIs need fresh data)
+        // - Caches shop pages for performance
+        // - Only caches HTML, not JSON responses
+        'middleware' => [
+            'Webkul\BagistoApi\Http\Middleware\HandleInvalidInputException',
+            'Webkul\BagistoApi\Http\Middleware\SecurityHeaders',
+            'Webkul\BagistoApi\Http\Middleware\LogApiRequests',
+            'Webkul\BagistoApi\Http\Middleware\VerifyStorefrontKey',
+            'Webkul\BagistoApi\Http\Middleware\BagistoApiDocumentationMiddleware',
+            'Webkul\BagistoApi\Http\Middleware\ForceApiJson',
+            'Spatie\ResponseCache\Middlewares\CacheResponse',
+        ],
+    ],
+
+    'resources' => [
+        base_path('vendor/bagisto/bagisto-api/src/Models/')
+    ],
+
+    'formats' => [
+        'json'=> ['application/json'],
+    ],
+
+    'patch_formats' => [
+        'json' => ['application/merge-patch+json'],
+    ],
+
+    'docs_formats' => [
+        'jsonopenapi' => ['application/vnd.openapi+json'],
+        'html'        => ['text/html'],
+    ],
+
+    'error_formats' => [
+        'jsonproblem' => ['application/problem+json'],
+    ],
+
+    'defaults' => [
+        'pagination_enabled'                => true,
+        'pagination_partial'                => false,
+        'pagination_client_enabled'         => false,
+        'pagination_client_items_per_page'  => false,
+        'pagination_client_partial'         => false,
+        'pagination_items_per_page'         => 10,
+        'pagination_maximum_items_per_page' => 50,
+        'route_prefix'                      => '/api',
+        'middleware'                        => [],
+    ],
+
+    'pagination' => [
+        'page_parameter_name'           => 'page',
+        'enabled_parameter_name'        => 'pagination',
+        'items_per_page_parameter_name' => 'itemsPerPage',
+        'partial_parameter_name'        => 'partial',
+    ],
+
+    'graphql' => [
+        'enabled'              => true,
+        'nesting_separator'    => '__',
+        'introspection'        => ['enabled' => true],
+        'max_query_complexity' => 400,
+        'max_query_depth'      => 20,
+        'graphiql'             => [
+            'enabled'           => true,
+            'default_query'     => null,
+            'default_variables' => null,
+        ],
+        'graphql_playground' => [
+            'enabled'           => true,
+            'default_query'     => null,
+            'default_variables' => null,
+        ],
+        // GraphQL middleware for authentication and rate limiting
+        'middleware' => [
+            'Webkul\BagistoApi\Http\Middleware\VerifyGraphQLStorefrontKey',
+        ],
+    ],
+
+    'graphiql' => [
+        'enabled' => true,
+    ],
+
+    'name_converter' => SnakeCaseToCamelCaseNameConverter::class,
+
+    'path_segment_name_generator' => DashPathSegmentNameGenerator::class,
+
+    'exception_to_status' => [
+        AuthenticationException::class => 401,
+        AuthorizationException::class  => 403,
+        ValidationException::class     => 400,
+        InvalidInputException::class   => 400,
+    ],
+
+    'swagger_ui' => [
+        'enabled' => true,
+        'apiKeys' => [
+            'api' => [
+                'name'   => 'Authorization',
+                'type'   => 'header',
+                'scheme' => 'bearer',
+            ],
+        ],
+    ],
+
+    'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH,
+
+    'serializer' => [
+        'hydra_prefix'    => false,
+        'datetime_format' => 'Y-m-d\TH:i:sP',
+    ],
+
+    'cache' => 'redis',
+
+    'schema_cache' => [
+        'enabled' => true,
+        'store'   => 'redis',
+    ],
+
+    'security' => [
+        'sanctum' => true,
+    ],
+
+    'rate_limit' => [
+        'skip_localhost' => env('RATE_LIMIT_SKIP_LOCALHOST', true),
+        'auth'           => env('RATE_LIMIT_AUTH', 5),
+        'admin'          => env('RATE_LIMIT_ADMIN', 60),
+        'shop'           => env('RATE_LIMIT_SHOP', 100),
+        'graphql'        => env('RATE_LIMIT_GRAPHQL', 100),
+        'cache_driver'   => env('RATE_LIMIT_CACHE', 'redis'),
+        'cache_prefix'   => 'api:rate-limit:',
+    ],
+
+    'security_headers' => [
+        'enabled'     => true,
+        'force_https' => env('APP_FORCE_HTTPS', false),
+        'csp_header'  => "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
+    ],
+
+    'api_logging' => [
+        'enabled'            => env('API_LOG_ENABLED', true),
+        'log_sensitive_data' => env('API_LOG_SENSITIVE_DATA', false),
+        'exclude_paths'      => ['docs', 'graphiql', 'swagger-ui', 'docs.json'],
+        'channel'            => 'api',
+        'async'              => env('API_LOG_ASYNC', true),
+        'queue'              => env('API_LOG_QUEUE', 'api-logs'),
+    ],
+
+    'graphql_validation' => [
+        'max_depth'      => env('GRAPHQL_MAX_DEPTH', 10),
+        'max_complexity' => env('GRAPHQL_MAX_COMPLEXITY', 300),
+    ],
+
+    'request_limits' => [
+        'max_size_mb'          => env('MAX_REQUEST_SIZE', 10),
+        'max_pagination_limit' => env('MAX_PAGINATION_LIMIT', 100),
+    ],
+
+    'database' => [
+        'log_queries'          => env('DB_QUERY_LOG_ENABLED', false),
+        'slow_query_threshold' => env('DB_SLOW_QUERY_THRESHOLD', 1000),
+    ],
+
+    'caching' => [
+        'enable_security_cache' => env('API_SECURITY_CACHE', true),
+        'security_cache_ttl'    => env('API_SECURITY_CACHE_TTL', 3600),
+        'enable_response_cache' => env('API_RESPONSE_CACHE', true),
+        'response_cache_ttl'    => env('API_RESPONSE_CACHE_TTL', 3600),
+    ],
+
+    'http_cache' => [
+        'etag'                   => true,
+        'max_age'                => 3600,
+        'shared_max_age'         => null,
+        'vary'                   => null,
+        'public'                 => true,
+        'stale_while_revalidate' => 30,
+        'stale_if_error'         => null,
+        'invalidation'           => [
+            'urls'              => [],
+            'scoped_clients'    => [],
+            'max_header_length' => 7500,
+            'request_options'   => [],
+            'purger'            => ApiPlatform\HttpCache\SouinPurger::class,
+        ],
+    ],
+
+    'key_rotation_policy' => [
+        'enabled'               => true,
+        'expiration_months'     => env('API_KEY_EXPIRATION_MONTHS', 12),
+        'transition_days'       => env('API_KEY_TRANSITION_DAYS', 7),
+        'cleanup_days'          => env('API_KEY_CLEANUP_DAYS', 90),
+        'cache_ttl'             => env('API_KEY_CACHE_TTL', 3600),
+        'storefront_key_prefix' => env('STOREFRONT_KEY_PREFIX', 'pk_storefront_'),
+    ],
+];

+ 230 - 0
packages/Webkul/BagistoApi/config/api-platform.php

@@ -0,0 +1,230 @@
+<?php
+
+/*
+ * This file is part of the API Platform project.
+ *
+ * (c) Kévin Dunglas <dunglas@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+use ApiPlatform\Metadata\Operation\DashPathSegmentNameGenerator;
+use ApiPlatform\Metadata\UrlGeneratorInterface;
+use Illuminate\Auth\Access\AuthorizationException;
+use Illuminate\Auth\AuthenticationException;
+use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
+use Webkul\BagistoApi\Exception\InvalidInputException;
+use Webkul\BagistoApi\Exception\ValidationException;
+
+return [
+    'title'       => '',
+    'description' => '',
+    'version'     => '1.0.0',
+    'show_webby'  => true,
+
+    'routes' => [
+        'domain' => null,
+        // Global middleware applied to every API Platform routes
+        // HandleInvalidInputException: Catches validation errors and returns RFC 7807 format
+        // VerifyStorefrontKey: Validates X-STOREFRONT-KEY header and rate limiting for shop APIs
+        // BagistoApiDocumentationMiddleware: Handles custom /api index and documentation pages
+        // ForceApiJson: Ensures API responses have JSON content-type
+        // CacheResponse: Using custom ApiAwareResponseCache profile that:
+        // - Excludes API routes from caching (APIs need fresh data)
+        // - Caches shop pages for performance
+        // - Only caches HTML, not JSON responses
+        'middleware' => [
+            '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\BagistoApiDocumentationMiddleware',
+            'Webkul\BagistoApi\Http\Middleware\ForceApiJson',
+            'Spatie\ResponseCache\Middlewares\CacheResponse',
+        ],
+    ],
+
+    'resources' => [
+        base_path('packages/Webkul/BagistoApi/src/Models/'),
+    ],
+
+    'formats' => [
+        'json'=> ['application/json'],
+    ],
+
+    'patch_formats' => [
+        'json' => ['application/merge-patch+json'],
+    ],
+
+    'docs_formats' => [
+        'jsonopenapi' => ['application/vnd.openapi+json'],
+        'html'        => ['text/html'],
+    ],
+
+    'error_formats' => [
+        'jsonproblem' => ['application/problem+json'],
+    ],
+
+    'defaults' => [
+        'pagination_enabled'                => true,
+        'pagination_partial'                => false,
+        'pagination_client_enabled'         => false,
+        'pagination_client_items_per_page'  => false,
+        'pagination_client_partial'         => false,
+        'pagination_items_per_page'         => 10,
+        'pagination_maximum_items_per_page' => 50,
+        'route_prefix'                      => '/api',
+        'middleware'                        => [],
+    ],
+
+    'pagination' => [
+        'page_parameter_name'           => 'page',
+        'enabled_parameter_name'        => 'pagination',
+        'items_per_page_parameter_name' => 'itemsPerPage',
+        'partial_parameter_name'        => 'partial',
+    ],
+
+    'graphql' => [
+        'enabled'              => true,
+        'nesting_separator'    => '__',
+        'introspection'        => ['enabled' => true],
+        'max_query_complexity' => 400,
+        'max_query_depth'      => 20,
+        'graphiql'             => [
+            'enabled'           => true,
+            'default_query'     => null,
+            'default_variables' => null,
+        ],
+        'graphql_playground' => [
+            'enabled'           => true,
+            'default_query'     => null,
+            'default_variables' => null,
+        ],
+        // GraphQL middleware for authentication and rate limiting
+        'middleware' => [
+            'Webkul\BagistoApi\Http\Middleware\SetLocaleChannel',
+            'Webkul\BagistoApi\Http\Middleware\VerifyGraphQLStorefrontKey',
+        ],
+    ],
+
+    'graphiql' => [
+        'enabled' => true,
+    ],
+
+    'name_converter' => SnakeCaseToCamelCaseNameConverter::class,
+
+    'path_segment_name_generator' => DashPathSegmentNameGenerator::class,
+
+    'exception_to_status' => [
+        AuthenticationException::class => 401,
+        AuthorizationException::class  => 403,
+        ValidationException::class     => 400,
+        InvalidInputException::class   => 400,
+    ],
+
+    'swagger_ui' => [
+        'enabled' => true,
+        'apiKeys' => [
+            'api' => [
+                'name'   => 'Authorization',
+                'type'   => 'header',
+                'scheme' => 'bearer',
+            ],
+        ],
+    ],
+
+    'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH,
+
+    'serializer' => [
+        'hydra_prefix'    => false,
+        'datetime_format' => 'Y-m-d\TH:i:sP',
+    ],
+
+    'cache' => 'redis',
+
+    'schema_cache' => [
+        'enabled' => true,
+        'store'   => 'redis',
+    ],
+
+    'security' => [
+        'sanctum' => true,
+    ],
+
+    'rate_limit' => [
+        'skip_localhost' => env('RATE_LIMIT_SKIP_LOCALHOST', true),
+        'auth'           => env('RATE_LIMIT_AUTH', 5),
+        'admin'          => env('RATE_LIMIT_ADMIN', 60),
+        'shop'           => env('RATE_LIMIT_SHOP', 100),
+        'graphql'        => env('RATE_LIMIT_GRAPHQL', 100),
+        'cache_driver'   => env('RATE_LIMIT_CACHE', 'redis'),
+        'cache_prefix'   => 'api:rate-limit:',
+    ],
+
+    'security_headers' => [
+        'enabled'     => true,
+        'force_https' => env('APP_FORCE_HTTPS', false),
+        'csp_header'  => "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
+    ],
+
+    'api_logging' => [
+        'enabled'            => env('API_LOG_ENABLED', true),
+        'log_sensitive_data' => env('API_LOG_SENSITIVE_DATA', false),
+        'exclude_paths'      => ['docs', 'graphiql', 'swagger-ui', 'docs.json'],
+        'channel'            => 'api',
+        'async'              => env('API_LOG_ASYNC', true),
+        'queue'              => env('API_LOG_QUEUE', 'api-logs'),
+    ],
+
+    'graphql_validation' => [
+        'max_depth'      => env('GRAPHQL_MAX_DEPTH', 10),
+        'max_complexity' => env('GRAPHQL_MAX_COMPLEXITY', 300),
+    ],
+
+    'request_limits' => [
+        'max_size_mb'          => env('MAX_REQUEST_SIZE', 10),
+        'max_pagination_limit' => env('MAX_PAGINATION_LIMIT', 100),
+    ],
+
+    'database' => [
+        'log_queries'          => env('DB_QUERY_LOG_ENABLED', false),
+        'slow_query_threshold' => env('DB_SLOW_QUERY_THRESHOLD', 1000),
+    ],
+
+    'caching' => [
+        'enable_security_cache' => env('API_SECURITY_CACHE', true),
+        'security_cache_ttl'    => env('API_SECURITY_CACHE_TTL', 3600),
+        'enable_response_cache' => env('API_RESPONSE_CACHE', true),
+        'response_cache_ttl'    => env('API_RESPONSE_CACHE_TTL', 3600),
+    ],
+
+    'http_cache' => [
+        'etag'                   => true,
+        'max_age'                => 3600,
+        'shared_max_age'         => null,
+        'vary'                   => null,
+        'public'                 => true,
+        'stale_while_revalidate' => 30,
+        'stale_if_error'         => null,
+        'invalidation'           => [
+            'urls'              => [],
+            'scoped_clients'    => [],
+            'max_header_length' => 7500,
+            'request_options'   => [],
+            'purger'            => ApiPlatform\HttpCache\SouinPurger::class,
+        ],
+    ],
+
+    'key_rotation_policy' => [
+        'enabled'               => true,
+        'expiration_months'     => env('API_KEY_EXPIRATION_MONTHS', 12),
+        'transition_days'       => env('API_KEY_TRANSITION_DAYS', 7),
+        'cleanup_days'          => env('API_KEY_CLEANUP_DAYS', 90),
+        'cache_ttl'             => env('API_KEY_CACHE_TTL', 3600),
+        'storefront_key_prefix' => env('STOREFRONT_KEY_PREFIX', 'pk_storefront_'),
+    ],
+];

+ 78 - 0
packages/Webkul/BagistoApi/config/graphql-auth.php

@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * GraphQL Authentication Configuration
+ *
+ * Defines which GraphQL operations require X-STOREFRONT-KEY authentication.
+ * X-STOREFRONT-KEY is the generic, header for all client types:
+ * - Mobile apps
+ * - Web storefronts
+ * - Headless commerce
+ * - Admin dashboards
+ * - Third-party integrations *
+ */
+
+return [
+    /**
+     * Public GraphQL operations that don't require X-STOREFRONT-KEY header
+     *
+     * AUTHENTICATION STRATEGY:
+     * - X-STOREFRONT-KEY: ALWAYS required (identifies client/app)
+     * - Bearer Token: Required only for user-specific operations
+     *
+     * All operations require X-STOREFRONT-KEY. Then:
+     * - Public operations: X-STOREFRONT-KEY only
+     * - User operations: X-STOREFRONT-KEY + Bearer token (Sanctum)
+     * - Admin operations: X-STOREFRONT-KEY + Admin Bearer token
+     *
+     * Empty list = All operations require X-STOREFRONT-KEY
+     */
+    'public_operations' => [
+        '__schema',
+        '__type',
+    ],
+
+    /**
+     * Protected operations that require X-STOREFRONT-KEY header
+     *
+     * Leave this as an empty array to use blacklist approach (recommended).
+     * If you list operations here, they WILL require authentication.
+     * Unlisted operations with this non-empty array will NOT require auth.
+     *
+     * BEST PRACTICE: Keep this empty and use public_operations instead
+     * This way: protected_operations = everything NOT in public_operations
+     */
+    'protected_operations' => [
+    ],
+
+    /**
+     * Enable selective authentication
+     *
+     * true:  Use whitelist approach (public_operations)
+     * false: Use blacklist approach (protected_operations)
+     *
+     * RECOMMENDED: true (whitelist is more secure)
+     */
+    'use_whitelist' => true,
+
+    /**
+     * Skip authentication for introspection queries
+     * Allow GraphQL tools and playground to inspect schema without key
+     */
+    'allow_introspection' => true,
+
+    /**
+     * Detailed logging for authentication
+     * Set to 'true' to log all authentication checks
+     */
+    'log_auth_checks' => env('GRAPHQL_AUTH_LOG', false),
+
+    /**
+     * Custom error messages
+     */
+    'messages' => [
+        'missing_key' => 'X-STOREFRONT-KEY header is required for this operation',
+        'invalid_key' => 'Invalid or expired API key',
+        'rate_limit'  => 'Rate limit exceeded. Please try again later.',
+    ],
+];

+ 54 - 0
packages/Webkul/BagistoApi/config/storefront.php

@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Storefront API Key Configuration
+ *
+ * Settings for X-STOREFRONT-KEY authentication for shop/storefront APIs
+ */
+
+return [
+    /*
+    |--------------------------------------------------------------------------
+    | Default Rate Limit
+    |--------------------------------------------------------------------------
+    |
+    | Default number of requests allowed per minute for each storefront key.
+    | Can be overridden per key in the database.
+    |
+    */
+    'default_rate_limit' => env('STOREFRONT_DEFAULT_RATE_LIMIT', 100),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Cache TTL
+    |--------------------------------------------------------------------------
+    |
+    | Time-to-live for cached key validation results in minutes.
+    | Reduces database queries for repeated requests using the same key.
+    |
+    */
+    'cache_ttl' => env('STOREFRONT_CACHE_TTL', 60),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Cache Key Prefix
+    |--------------------------------------------------------------------------
+    |
+    | Prefix used for cache keys to avoid collisions with other cache entries.
+    |
+    */
+    'key_prefix' => env('STOREFRONT_KEY_PREFIX', 'storefront_key_'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Playground API Key
+    |--------------------------------------------------------------------------
+    |
+    | API key used for API documentation and GraphQL playground.
+    | Generate a dedicated key and set it in your .env file.
+    |
+    | Example: STOREFRONT_PLAYGROUND_KEY=pk_storefront_xxx
+    |
+    */
+    'playground_key' => env('STOREFRONT_PLAYGROUND_KEY'),
+];

+ 19 - 0
packages/Webkul/BagistoApi/src/Attributes/AllowPublic.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Webkul\BagistoApi\Attributes;
+
+use Attribute;
+
+/**
+ * Mark a GraphQL operation as public (no X-STOREFRONT-KEY required)
+ *
+ * @see RequiresStorefrontKey
+ */
+#[Attribute(Attribute::TARGET_METHOD)]
+class AllowPublic
+{
+    public function __construct(
+        public ?string $description = null,
+        public bool $rateLimitByIp = false,
+    ) {}
+}

+ 19 - 0
packages/Webkul/BagistoApi/src/Attributes/RequiresStorefrontKey.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Webkul\BagistoApi\Attributes;
+
+use Attribute;
+
+/**
+ * Mark a GraphQL operation as requiring X-STOREFRONT-KEY authentication
+ *
+ * @see AllowPublic
+ */
+#[Attribute(Attribute::TARGET_METHOD)]
+class RequiresStorefrontKey
+{
+    public function __construct(
+        public ?string $message = null,
+        public ?string $description = null,
+    ) {}
+}

+ 113 - 0
packages/Webkul/BagistoApi/src/CacheProfiles/ApiAwareResponseCache.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace Webkul\BagistoApi\CacheProfiles;
+
+use Illuminate\Http\Request;
+use Spatie\ResponseCache\CacheProfiles\CacheProfile;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * ApiAwareResponseCache Profile
+ *
+ * This cache profile:
+ * 1. Excludes ALL API routes from caching (API should return fresh data)
+ * 2. Caches shop/storefront pages for performance
+ * 3. Only caches successful (200) responses
+ * 4. Respects cache bypass headers
+ *
+ * Benefits:
+ * - APIs always return fresh data with correct content-type
+ * - Shop pages are cached for speed
+ * - No HTML cached for API responses
+ */
+class ApiAwareResponseCache implements CacheProfile
+{
+    /**
+     * Determine if the response cache middleware is enabled
+     */
+    public function enabled(Request $request): bool
+    {
+        return config('responsecache.enabled', false);
+    }
+
+    /**
+     * Determine if the request should be cached.
+     */
+    public function shouldCacheRequest(Request $request): bool
+    {
+        // Don't cache API routes - they need fresh data
+        if ($request->is('api/*') || $request->is('graphql*')) {
+            return false;
+        }
+
+        // Don't cache non-GET requests
+        if (! $request->isMethod('GET')) {
+            return false;
+        }
+
+        // Don't cache requests with query parameters (search, filters, pagination)
+        if ($request->getQueryString()) {
+            return false;
+        }
+
+        // Don't cache if user is authenticated (personalized content)
+        if ($request->user()) {
+            return false;
+        }
+
+        // Cache only shop pages (storefront)
+        if ($request->is('shop/*') ||
+            $request->is('categories/*') ||
+            $request->is('products/*') ||
+            $request->is('*') && ! $request->is('admin/*')) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Determine if the response should be cached.
+     *
+     * Only cache successful (200) HTML responses
+     */
+    public function shouldCacheResponse(Response $response): bool
+    {
+        // Only cache successful responses
+        if ($response->getStatusCode() !== 200) {
+            return false;
+        }
+
+        // Only cache HTML responses (not JSON or other formats)
+        $contentType = $response->headers->get('Content-Type', '');
+        if (strpos($contentType, 'text/html') === false) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the tags to use for this cached response.
+     */
+    public function cacheNameSuffix(Request $request): string
+    {
+        return '';
+    }
+
+    /**
+     * Return until when the response must be cached.
+     */
+    public function cacheRequestUntil(Request $request): \DateTime
+    {
+        return now()->addDay();
+    }
+
+    /**
+     * Determine if cache name suffix should be used
+     */
+    public function useCacheNameSuffix(Request $request): string
+    {
+        return '';
+    }
+}

+ 141 - 0
packages/Webkul/BagistoApi/src/Console/Commands/ApiKeyMaintenanceCommand.php

@@ -0,0 +1,141 @@
+<?php
+
+namespace Webkul\BagistoApi\Console\Commands;
+
+use Illuminate\Console\Command;
+use Webkul\BagistoApi\Services\KeyRotationService;
+
+/**
+ * Perform automatic API key maintenance tasks
+ */
+class ApiKeyMaintenanceCommand extends Command
+{
+    protected $signature = 'bagisto-api:key:maintain 
+                            {--cleanup : Clean up expired keys}
+                            {--invalidate : Invalidate deprecated keys}
+                            {--notify : Send expiration notifications}
+                            {--all : Perform all maintenance tasks}';
+
+    protected $description = 'Automatic API key maintenance (cleanup, deprecation, notifications)';
+
+    protected KeyRotationService $rotationService;
+
+    public function __construct()
+    {
+        parent::__construct();
+        $this->rotationService = new KeyRotationService;
+    }
+
+    /**
+     * Execute the maintenance command.
+     */
+    public function handle(): int
+    {
+        $cleanup = $this->option('cleanup') || $this->option('all');
+        $invalidate = $this->option('invalidate') || $this->option('all');
+        $notify = $this->option('notify') || $this->option('all');
+
+        if (! $cleanup && ! $invalidate && ! $notify) {
+            $cleanup = $invalidate = $notify = true;
+        }
+
+        $this->info(__('bagistoapi::app.graphql.install.maintenance-starting'));
+        $this->newLine();
+
+        if ($cleanup) {
+            $this->cleanup();
+        }
+
+        if ($invalidate) {
+            $this->invalidateDeprecatedKeys();
+        }
+
+        if ($notify) {
+            $this->notifyExpiringKeys();
+        }
+
+        $this->newLine();
+        $this->info(__('bagistoapi::app.graphql.install.maintenance-complete'));
+
+        return 0;
+    }
+
+    /**
+     * Clean up expired keys.
+     */
+    private function cleanup(): void
+    {
+        $this->line(__('bagistoapi::app.graphql.install.cleanup-expired-keys'));
+
+        $count = $this->rotationService->cleanupExpiredKeys();
+
+        if ($count > 0) {
+            $this->info(__('bagistoapi::app.graphql.install.cleanup-success-message', ['count' => $count]));
+        } else {
+            $this->line(__('bagistoapi::app.graphql.install.cleanup-expired-none'));
+        }
+    }
+
+    /**
+     * Invalidate deprecated keys.
+     */
+    private function invalidateDeprecatedKeys(): void
+    {
+        $this->line(__('bagistoapi::app.graphql.install.invalidate-deprecated'));
+
+        $count = $this->rotationService->invalidateDeprecatedKeys();
+
+        if ($count > 0) {
+            $this->info(__('bagistoapi::app.graphql.install.invalidate-success-message', ['count' => $count]));
+        } else {
+            $this->line(__('bagistoapi::app.graphql.install.invalidate-deprecated-none'));
+        }
+    }
+
+    /**
+     * Send expiration notifications.
+     */
+    private function notifyExpiringKeys(): void
+    {
+        $this->line(__('bagistoapi::app.graphql.install.notify-expiring'));
+
+        $keysExpiring7Days = $this->rotationService->getKeysExpiringSoon(7);
+        $keysExpiring30Days = $this->rotationService->getKeysExpiringSoon(30);
+
+        $notified = 0;
+
+        foreach ($keysExpiring7Days as $key) {
+            if ($this->sendExpirationNotification($key, '7 days')) {
+                $notified++;
+            }
+        }
+
+        foreach ($keysExpiring30Days as $key) {
+            if (! $keysExpiring7Days->contains($key)) {
+                if ($this->sendExpirationNotification($key, '30 days')) {
+                    $notified++;
+                }
+            }
+        }
+
+        if ($notified > 0) {
+            $this->info(__('bagistoapi::app.graphql.install.notify-success-message', ['count' => $notified]));
+        } else {
+            $this->line(__('bagistoapi::app.graphql.install.notify-expiring-none'));
+        }
+    }
+
+    /**
+     * Send expiration notification for a key.
+     */
+    private function sendExpirationNotification($key, string $timeframe): bool
+    {
+        try {
+            return true;
+        } catch (\Exception $e) {
+            $this->warn(__('bagistoapi::app.graphql.install.notify-failed-message', ['key' => $key->name, 'error' => $e->getMessage()]));
+
+            return false;
+        }
+    }
+}

+ 316 - 0
packages/Webkul/BagistoApi/src/Console/Commands/ApiKeyManagementCommand.php

@@ -0,0 +1,316 @@
+<?php
+
+namespace Webkul\BagistoApi\Console\Commands;
+
+use Illuminate\Console\Command;
+use Webkul\BagistoApi\Models\StorefrontKey;
+use Webkul\BagistoApi\Services\KeyRotationService;
+
+/**
+ * Manage API key rotation, expiration, and lifecycle.
+ */
+class ApiKeyManagementCommand extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'bagisto-api:key:manage
+                            {action : Action to perform (rotate, deactivate, cleanup, status, expiring, unused, summary)}
+                            {--key= : API Key ID or name}
+                            {--reason= : Reason for deactivation}
+                            {--days=7 : Number of days for "expiring" action}
+                            {--unused=90 : Number of days for "unused" action}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Manage API key rotation, expiration, and lifecycle';
+
+    /**
+     * Service instance.
+     */
+    protected KeyRotationService $rotationService;
+
+    /**
+     * Create a new command instance.
+     */
+    public function __construct()
+    {
+        parent::__construct();
+        $this->rotationService = new KeyRotationService;
+    }
+
+    /**
+     * Execute the console command.
+     */
+    public function handle(): int
+    {
+        $action = $this->argument('action');
+
+        return match ($action) {
+            'rotate'     => $this->rotateKey(),
+            'deactivate' => $this->deactivateKey(),
+            'cleanup'    => $this->cleanupExpiredKeys(),
+            'status'     => $this->showKeyStatus(),
+            'expiring'   => $this->showExpiringKeys(),
+            'unused'     => $this->showUnusedKeys(),
+            'summary'    => $this->showPolicySummary(),
+            default      => $this->handleInvalidAction($action),
+        };
+    }
+
+    /**
+     * Rotate an API key.
+     */
+    private function rotateKey(): int
+    {
+        $keyId = $this->option('key');
+        if (! $keyId) {
+            $this->error(__('bagistoapi::app.graphql.install.key-management-required', ['action' => 'rotate']));
+            return 1;
+        }
+
+        try {
+            $key = $this->findKey($keyId);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+            return 1;
+        }
+
+        if (! $key->isValid()) {
+            $this->error(__('bagistoapi::app.graphql.install.key-rotation-error', ['error' => 'invalid key']));
+            return 1;
+        }
+
+        try {
+            $newKey = $this->rotationService->rotateKey($key);
+
+            $this->info(__('bagistoapi::app.graphql.install.key-rotated-success'));
+            $this->line(__('bagistoapi::app.graphql.install.old-key', ['name' => $key->name]));
+            $this->line(__('bagistoapi::app.graphql.install.old-key-id', ['id' => $key->id]));
+            $this->line(__('bagistoapi::app.graphql.install.deprecation-date', ['date' => $key->deprecation_date]));
+            $this->newLine();
+            $this->line(__('bagistoapi::app.graphql.install.new-key', ['name' => $newKey->name]));
+            $this->line(__('bagistoapi::app.graphql.install.new-key-id', ['id' => $newKey->id]));
+            $this->line(__('bagistoapi::app.graphql.install.new-key-value', ['key' => $newKey->key]));
+            $this->line(__('bagistoapi::app.graphql.install.expires-at', ['date' => $newKey->expires_at]));
+
+            return 0;
+        } catch (\Exception $e) {
+            $this->error(__('bagistoapi::app.graphql.install.key-rotation-error', ['error' => $e->getMessage()]));
+            return 1;
+        }
+    }
+
+    /**
+     * Deactivate an API key.
+     */
+    private function deactivateKey(): int
+    {
+        $keyId = $this->option('key');
+        if (! $keyId) {
+            $this->error(__('bagistoapi::app.graphql.install.key-management-required', ['action' => 'deactivate']));
+            return 1;
+        }
+
+        try {
+            $key = $this->findKey($keyId);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+            return 1;
+        }
+
+        $reason = $this->option('reason') ?? 'Manual deactivation';
+
+        if ($this->confirm(__('bagistoapi::app.graphql.install.confirm-deactivate', ['name' => $key->name]))) {
+            try {
+                $this->rotationService->deactivateKey($key, $reason);
+                $this->info(__('bagistoapi::app.graphql.install.key-deactivated-success'));
+                return 0;
+            } catch (\Exception $e) {
+                $this->error(__('bagistoapi::app.graphql.install.key-deactivation-error', ['error' => $e->getMessage()]));
+                return 1;
+            }
+        }
+
+        $this->info(__('bagistoapi::app.graphql.install.deactivation-cancelled'));
+        return 0;
+    }
+
+    /**
+     * Clean up expired keys.
+     */
+    private function cleanupExpiredKeys(): int
+    {
+        if ($this->confirm(__('bagistoapi::app.graphql.install.confirm-cleanup'))) {
+            $count = $this->rotationService->cleanupExpiredKeys();
+            $this->info(__('bagistoapi::app.graphql.install.cleanup-success', ['count' => $count]));
+            return 0;
+        }
+
+        $this->info(__('bagistoapi::app.graphql.install.cleanup-cancelled'));
+        return 0;
+    }
+
+    /**
+     * Show status of a specific key.
+     */
+    private function showKeyStatus(): int
+    {
+        $keyId = $this->option('key');
+        if (! $keyId) {
+            $this->error(__('bagistoapi::app.graphql.install.key-management-required', ['action' => 'status']));
+            return 1;
+        }
+
+        try {
+            $key = $this->findKey($keyId);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+            return 1;
+        }
+
+        $status = $this->rotationService->getRotationStatus($key);
+
+        $this->info(__('bagistoapi::app.graphql.install.key-status-title', ['name' => $key->name]));
+        $this->newLine();
+
+        $this->line(__('bagistoapi::app.graphql.install.key-status-active', ['status' => $status['is_valid'] ? '✅ Yes' : '❌ No']));
+        $this->line(__('bagistoapi::app.graphql.install.key-status-usable', ['status' => $status['is_usable'] ? '✅ Yes' : '❌ No']));
+        $this->line(__('bagistoapi::app.graphql.install.key-status-expired', ['status' => $status['is_expired'] ? '❌ Yes' : '✅ No']));
+        $this->line(__('bagistoapi::app.graphql.install.key-status-deprecated', ['status' => $status['is_deprecated'] ? '⚠️ Yes' : '✅ No']));
+        $this->newLine();
+
+        $expiresAtText = $status['expires_at'] ? $status['expires_at']->format('Y-m-d H:i:s') : __('bagistoapi::app.graphql.install.key-never');
+        $this->line(__('bagistoapi::app.graphql.install.key-status-expires-at', ['expires' => $expiresAtText]));
+
+        $daysText = $status['days_until_expiry'] ? $status['days_until_expiry'].__('bagistoapi::app.graphql.install.key-days') : 'N/A';
+        $this->line(__('bagistoapi::app.graphql.install.key-status-days-until-expiry', ['days' => $daysText]));
+
+        $lastUsedText = $status['last_used_at'] ? $status['last_used_at']->format('Y-m-d H:i:s') : __('bagistoapi::app.graphql.install.key-never');
+        $this->line(__('bagistoapi::app.graphql.install.key-status-last-used', ['date' => $lastUsedText]));
+        $this->newLine();
+
+        if ($status['rotated_from']) {
+            $this->line(__('bagistoapi::app.graphql.install.key-status-rotated-from', ['key' => $status['rotated_from']]));
+        }
+        if ($status['rotated_keys']) {
+            $this->line(__('bagistoapi::app.graphql.install.key-status-rotated-keys', ['count' => $status['rotated_keys']]));
+        }
+
+        return 0;
+    }
+
+    /**
+     * Show keys expiring soon.
+     */
+    private function showExpiringKeys(): int
+    {
+        $days = (int) $this->option('days');
+        $keys = $this->rotationService->getKeysExpiringSoon($days);
+
+        if ($keys->isEmpty()) {
+            $this->info(__('bagistoapi::app.graphql.install.no-keys-expiring', ['days' => $days]));
+            return 0;
+        }
+
+        $this->info(__('bagistoapi::app.graphql.install.keys-expiring-title', ['days' => $days]));
+        $this->newLine();
+
+        foreach ($keys as $key) {
+            $daysLeft = $key->expires_at->diffInDays(now());
+            $this->line(__('bagistoapi::app.graphql.install.key-display-format', ['name' => $key->name, 'id' => $key->id]));
+            $this->line(__('bagistoapi::app.graphql.install.key-expires-display', ['date' => $key->expires_at->format('Y-m-d'), 'days' => $daysLeft]));
+        }
+
+        return 0;
+    }
+
+    /**
+     * Show unused keys.
+     */
+    private function showUnusedKeys(): int
+    {
+        $days = (int) $this->option('unused');
+        $keys = $this->rotationService->getUnusedKeys($days);
+
+        if ($keys->isEmpty()) {
+            $this->info(__('bagistoapi::app.graphql.install.no-unused-keys', ['days' => $days]));
+            return 0;
+        }
+
+        $this->info(__('bagistoapi::app.graphql.install.unused-keys-title', ['days' => $days]));
+        $this->newLine();
+
+        foreach ($keys as $key) {
+            $lastUsed = $key->last_used_at
+                ? $key->last_used_at->format('Y-m-d')
+                : __('bagistoapi::app.graphql.install.key-never');
+            $this->line(__('bagistoapi::app.graphql.install.key-display-format', ['name' => $key->name, 'id' => $key->id]));
+            $this->line(__('bagistoapi::app.graphql.install.key-last-used-display', ['date' => $lastUsed]));
+        }
+
+        return 0;
+    }
+
+    /**
+     * Show policy compliance summary.
+     */
+    private function showPolicySummary(): int
+    {
+        $summary = $this->rotationService->getPolicyComplianceSummary();
+
+        $this->info(__('bagistoapi::app.graphql.install.policy-compliance-summary'));
+        $this->newLine();
+
+        $this->line(__('bagistoapi::app.graphql.install.total-keys', ['count' => $summary['total_active_keys']]));
+        $this->line(__('bagistoapi::app.graphql.install.valid-keys', ['count' => $summary['total_valid_keys']]));
+        $this->line(__('bagistoapi::app.graphql.install.expired-keys', ['count' => $summary['expired_keys']]));
+        $this->line(__('bagistoapi::app.graphql.install.deprecated-keys', ['count' => $summary['deprecated_keys']]));
+        $this->line(__('bagistoapi::app.graphql.install.keys-expiring-soon', ['count' => $summary['keys_expiring_soon']]));
+        $this->line(__('bagistoapi::app.graphql.install.unused-keys-summary', ['count' => $summary['unused_keys']]));
+        $this->line(__('bagistoapi::app.graphql.install.recently-rotated', ['count' => $summary['recently_rotated']]));
+
+        return 0;
+    }
+
+    /**
+     * Handle invalid action.
+     */
+    private function handleInvalidAction(string $action): int
+    {
+        $this->error(__('bagistoapi::app.graphql.install.invalid-action', ['action' => $action]));
+        $this->info(__('bagistoapi::app.graphql.install.available-actions'));
+
+        return 1;
+    }
+
+    /**
+     * Find a key by ID or name.
+     *
+     * @param  string  $keyIdentifier
+     * @return StorefrontKey
+     *
+     * @throws \Exception
+     */
+    private function findKey(string $keyIdentifier): StorefrontKey
+    {
+        if (is_numeric($keyIdentifier)) {
+            $key = StorefrontKey::find($keyIdentifier);
+            if ($key) {
+                return $key;
+            }
+        }
+
+        $key = StorefrontKey::where('name', $keyIdentifier)->first();
+        if ($key) {
+            return $key;
+        }
+
+        throw new \Exception(__('bagistoapi::app.graphql.key-not-found', ['identifier' => $keyIdentifier]));
+    }
+}

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

@@ -0,0 +1,45 @@
+<?php
+
+namespace Webkul\BagistoApi\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
+
+class ClearApiPlatformCacheCommand extends Command
+{
+    protected $signature = '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.';
+
+    public function handle(): int
+    {
+        $configuredStore = (string) config('api-platform.cache', 'file');
+        $schemaCacheEnabled = (bool) config('api-platform.schema_cache.enabled', false);
+        $schemaStore = $schemaCacheEnabled
+            ? (string) config('api-platform.schema_cache.store', $configuredStore)
+            : $configuredStore;
+
+        $overrideStore = $this->option('store');
+        if (\is_string($overrideStore) && $overrideStore !== '') {
+            $configuredStore = $overrideStore;
+            $schemaStore = $overrideStore;
+        }
+
+        $stores = array_values(array_unique(array_filter([$configuredStore, $schemaStore])));
+
+        foreach ($stores as $store) {
+            try {
+                Cache::store($store)->flush();
+                $this->info(sprintf('Flushed cache store "%s".', $store));
+            } catch (\Throwable $e) {
+                $this->error(sprintf('Failed to flush cache store "%s": %s', $store, $e->getMessage()));
+
+                return self::FAILURE;
+            }
+        }
+
+        $this->line('If you are running PHP-FPM with OPcache in production, you may also need to restart PHP-FPM.');
+
+        return self::SUCCESS;
+    }
+}

+ 81 - 0
packages/Webkul/BagistoApi/src/Console/Commands/GenerateStorefrontKey.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace Webkul\BagistoApi\Console\Commands;
+
+use Illuminate\Console\Command;
+use Webkul\BagistoApi\Models\StorefrontKey;
+
+/**
+ * Generate a new storefront API key for shop/storefront API authentication
+ */
+class GenerateStorefrontKey extends Command
+{
+    /**
+     * Maximum allowed rate limit (requests per minute).
+     */
+    protected const MAX_RATE_LIMIT = 5000;
+    protected $signature = 'bagisto-api:generate-key
+                            {--name= : Name of the storefront key}
+                            {--rate-limit=100 : Rate limit (requests per minute, leave empty for unlimited)}
+                            {--no-activation : Create the key in inactive state}';
+
+    protected $description = 'Generate a new storefront API key for shop/storefront APIs';
+
+    /**
+     * Execute the command.
+     */
+    public function handle(): int
+    {
+        $name = $this->option('name') ?? $this->ask('Enter the name for this storefront key');
+
+        if (empty($name)) {
+            $this->error(__('bagistoapi::app.graphql.install.key-name-required'));
+
+            return self::FAILURE;
+        }
+
+        if (StorefrontKey::where('name', $name)->exists()) {
+            $this->error(__('bagistoapi::app.graphql.install.key-already-exists', ['name' => $name]));
+
+            return self::FAILURE;
+        }
+
+        $rateLimitOption = $this->option('rate-limit');
+        
+        // Handle rate limit: null/empty = unlimited (up to MAX), number = capped at MAX
+        if ($rateLimitOption === '' || $rateLimitOption === null) {
+            $rateLimit = null; // null means unlimited in database
+        } else {
+            $requestedLimit = (int) $rateLimitOption;
+            if ($requestedLimit > self::MAX_RATE_LIMIT) {
+                $this->warn(__('bagistoapi::app.graphql.install.rate-limit-exceeded', ['max' => self::MAX_RATE_LIMIT]));
+                $rateLimit = self::MAX_RATE_LIMIT;
+            } else {
+                $rateLimit = $requestedLimit;
+            }
+        }
+        
+        $key = StorefrontKey::generateKey();
+        $storefront = StorefrontKey::create([
+            'name'       => $name,
+            'key'        => $key,
+            'is_active'  => ! $this->option('no-activation'),
+            'rate_limit' => $rateLimit,
+        ]);
+
+        $this->info(__('bagistoapi::app.graphql.install.key-generated-success'));
+        $this->newLine();
+        $this->line('<info>'.__('bagistoapi::app.graphql.install.key-details').'</info>');
+        $this->line("  <fg=cyan>".__('bagistoapi::app.graphql.install.key-field-id')."</> : {$storefront->id}");
+        $this->line("  <fg=cyan>".__('bagistoapi::app.graphql.install.key-field-name')."</> : {$storefront->name}");
+        $this->line("  <fg=cyan>".__('bagistoapi::app.graphql.install.key-field-key')."</> : <fg=yellow>{$key}</>");
+        $rateLimitDisplay = $rateLimit ? $rateLimit.__('bagistoapi::app.graphql.install.key-requests-minute') : __('bagistoapi::app.graphql.install.key-unlimited');
+        $this->line("  <fg=cyan>".__('bagistoapi::app.graphql.install.key-field-rate-limit')."</> : {$rateLimitDisplay}");
+        $this->line('  <fg=cyan>'.__('bagistoapi::app.graphql.install.key-field-status').'</> : '.($storefront->is_active ? '<fg=green>Active</>' : '<fg=red>Inactive</>'));
+        $this->newLine();
+        $this->warn(__('bagistoapi::app.graphql.install.key-secure-warning'));
+        $this->warn(__('bagistoapi::app.graphql.install.key-share-warning'));
+
+        return self::SUCCESS;
+    }
+}

+ 494 - 0
packages/Webkul/BagistoApi/src/Console/Commands/InstallApiPlatformCommand.php

@@ -0,0 +1,494 @@
+<?php
+
+namespace Webkul\BagistoApi\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Filesystem\Filesystem;
+use Symfony\Component\Process\Process;
+
+class InstallApiPlatformCommand extends Command
+{
+    protected $signature = 'bagisto-api-platform:install';
+
+    protected $description = 'Install and configure API Platform for Bagisto';
+
+    public function __construct(protected Filesystem $files)
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the installation process.
+     */
+    public function handle(): int
+    {
+        $this->info(__('bagistoapi::app.graphql.install.starting'));
+
+        try {
+            $this->publishPackageAssets();
+            
+            $this->registerServiceProvider();
+                     
+            $this->linkApiPlatformAssets();
+
+            $this->updateComposerAutoload();
+
+            $this->makeTranslatableModelAbstract();
+
+            $this->registerApiPlatformProviders();
+
+            $this->runDatabaseMigrations();           
+
+            $this->clearAndOptimizeCaches();
+            
+            $this->generateApiKey();
+
+            $this->publishConfiguration();
+
+            $this->clearAndOptimizeCaches();
+
+            $this->info(__('bagistoapi::app.graphql.install.completed-success'));
+            $this->newLine();
+            
+            $appUrl = config('app.url');
+
+            $this->newLine();
+            $this->info(__('bagistoapi::app.graphql.install.api-endpoints'));
+            $this->line(__('bagistoapi::app.graphql.install.api-documentation', ['url' => 'https://api-docs.bagisto.com/']));
+            $this->line(__('bagistoapi::app.graphql.install.api-landing-page', ['url' => "{$appUrl}/api"]));
+            $this->line(__('bagistoapi::app.graphql.install.graphql-playground', ['url' => "{$appUrl}/api/graphiql"]));
+            $this->line(__('bagistoapi::app.graphql.install.rest-api-storefront', ['url' => "{$appUrl}/api/shop"]));
+            $this->line(__('bagistoapi::app.graphql.install.rest-api-admin', ['url' => "{$appUrl}/api/admin"]));
+
+            $this->newLine();
+            $this->info(__('bagistoapi::app.graphql.install.completed-info'));
+
+            return self::SUCCESS;
+        } catch (\Exception $e) {
+            $this->error(__('bagistoapi::app.graphql.install.failed').$e->getMessage());
+
+            return self::FAILURE;
+        }
+    }
+
+    /**
+     * Register the API Platform service provider.
+     */
+    protected function registerServiceProvider(): void
+    {
+        $providersPath = base_path('bootstrap/providers.php');
+
+        if (! $this->files->exists($providersPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.provider-file-not-found', ['file' => $providersPath]));
+        }
+
+        if (! is_writable($providersPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.provider-permission-denied', ['file' => $providersPath]));
+        }
+
+        $content = $this->files->get($providersPath);
+
+        $providerClass = 'Webkul\\BagistoApi\\Providers\\BagistoApiServiceProvider::class';
+
+        if (strpos($content, $providerClass) !== false) {
+            $this->comment(__('bagistoapi::app.graphql.install.provider-already-registered'));
+
+            return;
+        }
+
+        $content = preg_replace(
+            '/(\],\s*\);)/',
+            "    $providerClass,\n$1",
+            $content
+        );
+
+        $this->files->put($providersPath, $content);
+
+        $this->line(__('bagistoapi::app.graphql.install.provider-registered'));
+    }
+
+    /**
+     * Publish the API Platform configuration file.
+     */
+    protected function publishConfiguration(): void
+    {
+        $source = __DIR__.'/../../../config/api-platform.php';
+        $vendorSource = __DIR__.'/../../../config/api-platform-vendor.php';
+
+        $destination = config_path('api-platform.php');
+
+        if ($this->files->exists(base_path('vendor/bagisto/bagisto-api/config/api-platform-vendor.php'))) { 
+            $source = $vendorSource;
+        }
+
+        if (! $this->files->exists($source)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.config-source-not-found', ['file' => $source]));
+        }
+
+        if ($this->files->exists($destination)) {
+            $this->comment(__('bagistoapi::app.graphql.install.config-already-exists'));
+
+            return;
+        }
+
+        $configDir = dirname($destination);
+        if (! is_writable($configDir)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.config-permission-denied', ['directory' => $configDir]));
+        }
+
+        $this->files->copy($source, $destination);
+        $this->line(__('bagistoapi::app.graphql.install.config-published'));
+    }
+
+    /**
+     * Publish package assets via vendor:publish command.
+     */
+    protected function publishPackageAssets(): void
+    {
+        try {
+            $process = new Process([
+                'php',
+                'artisan',
+                'vendor:publish',
+                '--provider=Webkul\BagistoApi\Providers\BagistoApiServiceProvider',
+                '--no-interaction',
+            ]);
+
+            $process->run();
+
+            if (! $process->isSuccessful()) {
+                $this->warn(__('bagistoapi::app.graphql.install.publish-assets-warning', ['error' => $process->getErrorOutput()]));
+
+                return;
+            }
+
+            $this->line(__('bagistoapi::app.graphql.install.assets-published'));
+        } catch (\Exception $e) {
+            $this->warn(__('bagistoapi::app.graphql.install.publish-assets-warning', ['error' => $e->getMessage()]));
+        }
+    }
+
+    /**
+     * Update composer.json with required configurations.
+     */
+    protected function updateComposerAutoload(): void
+    {
+        $composerPath = base_path('composer.json');
+
+        if (! $this->files->exists($composerPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.composer-file-not-found', ['file' => $composerPath]));
+        }
+
+        if (! is_writable($composerPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.composer-permission-denied', ['file' => $composerPath]));
+        }
+
+        $composer = json_decode($this->files->get($composerPath), true);
+
+        if (! isset($composer['autoload']['psr-4'])) {
+            $composer['autoload']['psr-4'] = [];
+        }
+
+        $composer['autoload']['psr-4']['Webkul\\GraphQL\\'] = 'packages/Webkul/GraphQL/src';
+
+        if (! isset($composer['extra']['laravel']['dont-discover'])) {
+            $composer['extra']['laravel']['dont-discover'] = [];
+        }
+
+        if (! in_array('api-platform/laravel', $composer['extra']['laravel']['dont-discover'])) {
+            $composer['extra']['laravel']['dont-discover'][] = 'api-platform/laravel';
+        }
+
+        $this->files->put($composerPath, json_encode($composer, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT).PHP_EOL);
+        $this->line(__('bagistoapi::app.graphql.install.composer-updated'));
+    }
+
+    /**
+     * Make TranslatableModel abstract for API Platform compatibility.
+     */
+    protected function makeTranslatableModelAbstract(): void
+    {
+        $modelPath = base_path('packages/Webkul/Core/src/Eloquent/TranslatableModel.php');
+
+        if (! $this->files->exists($modelPath)) {
+            $this->comment(__('bagistoapi::app.graphql.install.translatable-not-found'));
+
+            return;
+        }
+
+        if (! is_writable($modelPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.translatable-permission-denied', ['file' => $modelPath]));
+        }
+
+        $content = $this->files->get($modelPath);
+
+        if (preg_match('/abstract\s+class\s+TranslatableModel/', $content)) {
+            $this->comment(__('bagistoapi::app.graphql.install.translatable-already-abstract'));
+
+            return;
+        }
+
+        $content = preg_replace(
+            '/class\s+TranslatableModel/',
+            'abstract class TranslatableModel',
+            $content
+        );
+
+        $this->files->put($modelPath, $content);
+        $this->line(__('bagistoapi::app.graphql.install.translatable-made-abstract'));
+    }
+
+    /**
+     * Register API Platform providers in bootstrap/app.php.
+     */
+    protected function registerApiPlatformProviders(): void
+    {
+        $appPath = base_path('bootstrap/app.php');
+
+        if (! $this->files->exists($appPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.providers-file-not-found', ['file' => $appPath]));
+        }
+
+        if (! is_writable($appPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.providers-permission-denied', ['file' => $appPath]));
+        }
+
+        $content = $this->files->get($appPath);
+
+        if (strpos($content, 'ApiPlatformProvider::class') !== false) {
+            $this->comment(__('bagistoapi::app.graphql.install.providers-already-registered'));
+
+            return;
+        }
+
+        $providers = "\n->withProviders([\n"
+            ."     \\ApiPlatform\\Laravel\\ApiPlatformProvider::class,\n"
+            ."     \\ApiPlatform\\Laravel\\ApiPlatformDeferredProvider::class,\n"
+            ."     \\ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider::class,\n"
+            ."])\n";
+
+        if (strpos($content, '->create()') !== false) {
+            $content = str_replace('->create()', $providers.'->create()', $content);
+        } else {
+            throw new \Exception(__('bagistoapi::app.graphql.install.providers-not-found'));
+        }
+
+        $this->files->put($appPath, $content);
+        $this->line(__('bagistoapi::app.graphql.install.providers-registered'));
+    }
+
+    /**
+     * Link or copy API Platform assets to public directory.
+     */
+    protected function linkApiPlatformAssets(): void
+    {
+        $vendorPath = base_path('vendor/api-platform/laravel/public');
+        $publicPath = public_path('vendor/api-platform');
+
+        if (! $this->files->exists($vendorPath)) {
+            $this->line(__('bagistoapi::app.graphql.install.vendor-path-not-found', ['path' => $vendorPath]));
+
+            return;
+        }
+
+        if ($this->files->exists($publicPath)) {
+            $this->line(__('bagistoapi::app.graphql.install.assets-already-linked', ['path' => $publicPath]));
+
+            return;
+        }
+
+        $publicVendorDir = dirname($publicPath);
+        if (! $this->files->exists($publicVendorDir)) {
+            $this->files->makeDirectory($publicVendorDir, 0755, true);
+        }
+
+        try {
+            symlink($vendorPath, $publicPath);
+            $this->line(__('bagistoapi::app.graphql.install.asset-linked-success'));
+        } catch (\Exception $e) {
+            $this->comment(__('bagistoapi::app.graphql.install.symlink-create-failed'));
+            if (! $this->files->copyDirectory($vendorPath, $publicPath)) {
+                $this->warn(__('bagistoapi::app.graphql.install.asset-copy-warning'));
+
+                return;
+            }
+            $this->line(__('bagistoapi::app.graphql.install.asset-copied-success'));
+        }
+    }
+
+    /**
+     * Run database migrations.
+     */
+    protected function runDatabaseMigrations(): void
+    {
+        try {
+            $this->info(__('bagistoapi::app.graphql.install.running-migrations'));
+
+            $process = new Process([
+                'php',
+                'artisan',
+                'migrate',
+            ]);
+
+            $process->run();
+
+            if (! $process->isSuccessful()) {
+                throw new \Exception(__('bagistoapi::app.graphql.install.migrations-error').' '.$process->getErrorOutput());
+            }
+
+            $this->line(__('bagistoapi::app.graphql.install.migrations-completed'));
+        } catch (\Exception $e) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.migrations-error-running', ['error' => $e->getMessage()]));
+        }
+    }
+
+    /**
+     * Clear and optimize application caches.
+     */
+    protected function clearAndOptimizeCaches(): void
+    {
+        try {
+            $this->info(__('bagistoapi::app.graphql.install.clearing-caches'));
+
+            $clearProcess = new Process([
+                'php',
+                'artisan',
+                'config:clear',
+            ]);
+
+            $clearProcess->run();
+
+            if (! $clearProcess->isSuccessful()) {
+                throw new \Exception(__('bagistoapi::app.graphql.install.cache-clear-error').' '.$clearProcess->getErrorOutput());
+            }
+
+            $cacheProcess = new Process([
+                'php',
+                'artisan',
+                'cache:clear',
+            ]);
+
+            $cacheProcess->run();
+
+            if (! $cacheProcess->isSuccessful()) {
+                throw new \Exception(__('bagistoapi::app.graphql.install.cache-clear-error').' '.$cacheProcess->getErrorOutput());
+            }
+
+            $clearProcess = new Process([
+                'php',
+                'artisan',
+                'optimize:clear',
+            ]);
+
+            $clearProcess->run();
+
+            if (! $clearProcess->isSuccessful()) {
+                throw new \Exception(__('bagistoapi::app.graphql.install.cache-clear-error').' '.$clearProcess->getErrorOutput());
+            }
+
+            $optimizeProcess = new Process([
+                'php',
+                'artisan',
+                'optimize',
+            ]);
+
+            $optimizeProcess->run();
+
+            if (! $optimizeProcess->isSuccessful()) {
+                throw new \Exception(__('bagistoapi::app.graphql.install.cache-optimize-error').' '.$optimizeProcess->getErrorOutput());
+            }
+
+            $this->line(__('bagistoapi::app.graphql.install.caches-optimized'));
+        } catch (\Exception $e) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.cache-error', ['error' => $e->getMessage()]));
+        }
+    }
+
+    /**
+     * Generate a default storefront API key.
+     */
+    protected function generateApiKey(): void
+    {
+        try {
+            $this->info(__('bagistoapi::app.graphql.install.generating-api-key'));
+
+            $process = new Process([
+                'php',
+                'artisan',
+                'bagisto-api:generate-key',
+                '--name=Default Storefront Key1',
+            ]);
+
+            $process->run();
+
+            $output = $process->getOutput().$process->getErrorOutput();
+
+            if (stripos($output, 'already exists') !== false) {
+                $this->comment(__('bagistoapi::app.graphql.install.api-key-already-exists'));
+
+                return;
+            }
+
+            if (! $process->isSuccessful()) {
+                throw new \Exception(__('bagistoapi::app.graphql.install.api-key-generation-error').' '.$process->getErrorOutput());
+            }
+
+            $this->line(__('bagistoapi::app.graphql.install.api-key-generated'));
+
+            $generatedKey = $this->extractKeyFromOutput($output);
+            $this->saveStorefrontConfigToEnv($generatedKey);
+        } catch (\Exception $e) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.api-key-error', ['error' => $e->getMessage()]));
+        }
+    }
+
+    /**
+     * Extract API key from command output.
+     */
+    protected function extractKeyFromOutput(string $output): string
+    {
+        if (preg_match('/\b(pk_[a-zA-Z0-9_]+)\b/', $output, $matches)) {
+            return $matches[1];
+        }
+
+        return '';
+    }
+
+    /**
+     * Save storefront configuration to .env file.
+     */
+    protected function saveStorefrontConfigToEnv(string $generatedKey = ''): void
+    {
+        $envPath = base_path('.env');
+
+        if (! $this->files->exists($envPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.env-file-not-found'));
+        }
+
+        if (! is_writable($envPath)) {
+            throw new \Exception(__('bagistoapi::app.graphql.install.env-permission-denied'));
+        }
+
+        $envContent = $this->files->get($envPath);
+
+        $envVariables = [
+            'STOREFRONT_DEFAULT_RATE_LIMIT' => '100',
+            'STOREFRONT_CACHE_TTL' => '60',
+            'STOREFRONT_KEY_PREFIX' => 'storefront_key_',
+            'STOREFRONT_PLAYGROUND_KEY' => $generatedKey,
+            'API_PLAYGROUND_AUTO_INJECT_STOREFRONT_KEY' => 'false',
+        ];
+
+        foreach ($envVariables as $key => $value) {
+            if (preg_match("/^{$key}=/m", $envContent)) {
+                $envContent = preg_replace("/^{$key}=.*/m", "{$key}={$value}", $envContent);
+            } else {
+                $envContent .= "\n{$key}={$value}";
+            }
+        }
+
+        $this->files->put($envPath, $envContent);
+
+        $this->line(__('bagistoapi::app.graphql.install.env-config-saved'));
+    }
+}

+ 5 - 0
packages/Webkul/BagistoApi/src/Contracts/GuestCartTokens.php

@@ -0,0 +1,5 @@
+<?php
+
+namespace Webkul\BagistoApi\Contracts;
+
+interface GuestCartTokens {}

+ 26 - 0
packages/Webkul/BagistoApi/src/Database/Migrations/2025_12_10_185743_create_cart_tokens_table.php

@@ -0,0 +1,26 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('guest_cart_tokens', function (Blueprint $table) {
+            $table->id();
+            $table->unsignedInteger('cart_id')->unique()->index();
+            $table->string('token')->unique()->index();
+            $table->timestamp('created_at')->useCurrent();
+            $table->timestamp('updated_at')->useCurrentOnUpdate();
+
+            $table->foreign('cart_id')->references('id')->on('cart')->onDelete('cascade');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('guest_cart_tokens');
+    }
+};

+ 68 - 0
packages/Webkul/BagistoApi/src/Database/Migrations/2026_01_08_000000_create_storefront_keys_table.php

@@ -0,0 +1,68 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * Create storefront_keys table with complete API key management features:
+     * - Basic key storage and configuration
+     * - Key type (shop vs admin) for multi-tenant support
+     * - Expiration and rotation tracking
+     * - Usage monitoring for compliance
+     * - Soft deletes for audit trail
+     */
+    public function up(): void
+    {
+        if (Schema::hasTable('storefront_keys')) {
+            return;
+        }
+
+        Schema::create('storefront_keys', function (Blueprint $table) {
+            $table->id();
+
+            $table->string('name')->unique()->comment('Human-readable key name');
+            $table->enum('key_type', ['shop', 'admin'])
+                ->default('shop')
+                ->comment('API key type: shop (X-STOREFRONT-KEY) or admin (X-Admin-Key)');
+            $table->string('key')->unique()->index()
+                ->comment('The actual API key (prefixed with pk_storefront_)');
+            $table->boolean('is_active')->default(true)
+                ->comment('Whether this key is active and usable');
+
+            $table->integer('rate_limit')->default(100)
+                ->comment('Requests per minute allowed for this key');
+            $table->json('allowed_ips')->nullable()
+                ->comment('IP whitelist (JSON array) for additional security');
+
+            $table->timestamp('expires_at')->nullable()
+                ->comment('Key expiration date - after this, key is invalid');
+            $table->timestamp('last_used_at')->nullable()
+                ->comment('Last time this key was used - for usage tracking');
+            $table->timestamp('deprecation_date')->nullable()
+                ->comment('Date after which key is deprecated - used during rotation transition');
+            $table->foreignId('rotated_from_id')->nullable()
+                ->constrained('storefront_keys')
+                ->comment('Reference to the key this was rotated from - for audit trail');
+
+            $table->timestamps();
+            $table->softDeletes()->comment('Soft delete timestamp - for maintaining audit history');
+
+            $table->index('expires_at');
+            $table->index('last_used_at');
+            $table->index('deprecation_date');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('storefront_keys');
+    }
+};

+ 60 - 0
packages/Webkul/BagistoApi/src/Dto/AddToCartInput.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class AddToCartInput
+{
+    #[Groups(['mutation'])]
+    public ?int $product_id = null;
+
+    #[Groups(['mutation'])]
+    public ?int $quantity = null;
+
+    #[Groups(['mutation'])]
+    public ?int $is_buy_now = null;
+
+    #[Groups(['mutation'])]
+    public ?array $options = null;
+
+    public function getProduct_id(): ?int
+    {
+        return $this->product_id;
+    }
+
+    public function setProduct_id(?int $product_id): void
+    {
+        $this->product_id = $product_id;
+    }
+
+    public function getQuantity(): ?int
+    {
+        return $this->quantity;
+    }
+
+    public function setQuantity(?int $quantity): void
+    {
+        $this->quantity = $quantity;
+    }
+
+    public function getIs_buy_now(): ?int
+    {
+        return $this->is_buy_now;
+    }
+
+    public function setIs_buy_now(?int $is_buy_now): void
+    {
+        $this->is_buy_now = $is_buy_now;
+    }
+
+    public function getOptions(): ?array
+    {
+        return $this->options;
+    }
+
+    public function setOptions(?array $options): void
+    {
+        $this->options = $options;
+    }
+}

+ 21 - 0
packages/Webkul/BagistoApi/src/Dto/ApplyCouponInput.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class ApplyCouponInput
+{
+    #[Groups(['mutation'])]
+    public ?string $code = null;
+
+    public function getCode(): ?string
+    {
+        return $this->code;
+    }
+
+    public function setCode(?string $code): void
+    {
+        $this->code = $code;
+    }
+}

+ 26 - 0
packages/Webkul/BagistoApi/src/Dto/CancelOrderInput.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DTO for canceling a customer order
+ *
+ * Defines input structure for the cancelOrder mutation
+ */
+#[ApiResource]
+class CancelOrderInput
+{
+    /**
+     * The ID of the order to cancel
+     */
+    #[ApiProperty(
+        description: 'The ID of the order to cancel',
+        required: true
+    )]
+    #[Groups(['mutation'])]
+    public ?int $orderId = null;
+}

+ 353 - 0
packages/Webkul/BagistoApi/src/Dto/CartData.php

@@ -0,0 +1,353 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * Shopping cart data transfer object
+ */
+class CartData
+{
+    #[Groups(['query', 'mutation'])]
+    public ?int $id = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Cart token for guest users')]
+    public ?string $cartToken = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?int $customerId = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?int $channelId = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?int $itemsCount = null;
+
+    /**
+     * Individual cart items - array of CartItemData DTO objects
+     *
+     * @var array<CartItemData>|null
+     */
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Individual cart items')]
+    public ?array $items = [];
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Subtotal before discounts and taxes')]
+    public ?float $subtotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseSubtotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Total discount amount')]
+    public ?float $discountAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseDiscountAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Total tax amount')]
+    public ?float $taxAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseTaxAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Shipping cost')]
+    public ?float $shippingAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseShippingAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Grand total')]
+    public ?float $grandTotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseGrandTotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted subtotal price')]
+    public ?string $formattedSubtotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted discount amount')]
+    public ?string $formattedDiscountAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted tax amount')]
+    public ?string $formattedTaxAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted shipping amount')]
+    public ?string $formattedShippingAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted grand total price')]
+    public ?string $formattedGrandTotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Applied coupon code')]
+    public ?string $couponCode = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?bool $success = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $message = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?array $carts = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Unique session token for guest cart')]
+    public ?string $sessionToken = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Is this a guest cart')]
+    public bool $isGuest = false;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Total quantity of all items')]
+    public ?int $itemsQty = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Subtotal including tax')]
+    public ?float $subTotalInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Base subtotal including tax')]
+    public ?float $baseSubTotalInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted subtotal including tax')]
+    public ?string $formattedSubTotalInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Tax total')]
+    public ?float $taxTotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted tax total')]
+    public ?string $formattedTaxTotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Shipping amount including tax')]
+    public ?float $shippingAmountInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Base shipping amount including tax')]
+    public ?float $baseShippingAmountInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Formatted shipping amount including tax')]
+    public ?string $formattedShippingAmountInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Billing address')]
+    public ?array $billingAddress = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Shipping address')]
+    public ?array $shippingAddress = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Applied taxes')]
+    public ?array $appliedTaxes = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Has stockable items')]
+    public ?bool $haveStockableItems = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Payment method code')]
+    public ?string $paymentMethod = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Payment method title')]
+    public ?string $paymentMethodTitle = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false, description: 'Selected shipping rate')]
+    public ?string $selectedShippingRate = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false, description: 'Selected shipping rate title')]
+    public ?string $selectedShippingRateTitle = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Payment redirect URL (if payment gateway redirect needed)')]
+    public ?string $paymentRedirectUrl = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Order ID after order creation')]
+    public ?string $orderId = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Redirect URL for buy now checkout')]
+    public ?string $redirectUri = null;
+
+    public function getSelectedShippingRate(): ?string
+    {
+        return $this->selectedShippingRate;
+    }
+
+    public function setSelectedShippingRate(?string $selectedShippingRate): void
+    {
+        $this->selectedShippingRate = $selectedShippingRate;
+    }
+
+    public function getSelectedShippingRateTitle(): ?string
+    {
+        return $this->selectedShippingRateTitle;
+    }
+
+    public function setSelectedShippingRateTitle(?string $selectedShippingRateTitle): void
+    {
+        $this->selectedShippingRateTitle = $selectedShippingRateTitle;
+    }
+
+    public static function fromModel(\Webkul\Checkout\Models\Cart $cart): self
+    {
+        $data = new self;
+
+        $data->id = $cart->id;
+        $data->cartToken = (string) $cart->id;
+        $data->customerId = $cart->customer_id;
+        $data->isGuest = ! $cart->customer_id;
+        $data->channelId = $cart->channel_id;
+        $data->itemsCount = $cart->items()->count();
+        $data->itemsQty = (int) ($cart->items_qty ?? 0);
+
+        $items = $cart->items()
+            ->with(['product'])
+            ->get()
+            ->map(fn ($item) => CartItemData::fromModel($item));
+
+        $data->items = $items->toArray();
+
+        $data->subtotal = (float) ($cart->sub_total ?? 0);
+        $data->baseSubtotal = (float) ($cart->base_sub_total ?? 0);
+        $data->formattedSubtotal = core()->formatPrice($cart->sub_total ?? 0);
+
+        $data->subTotalInclTax = (float) ($cart->sub_total_incl_tax ?? $cart->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->taxAmount = (float) ($cart->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->discountAmount = (float) ($cart->discount_amount ?? 0);
+        $data->baseDiscountAmount = (float) ($cart->base_discount_amount ?? 0);
+        $data->formattedDiscountAmount = core()->formatPrice($cart->discount_amount ?? 0);
+
+        $data->shippingAmount = (float) ($cart->shipping_amount ?? 0);
+        $data->baseShippingAmount = (float) ($cart->base_shipping_amount ?? 0);
+        $data->formattedShippingAmount = core()->formatPrice($cart->shipping_amount ?? 0);
+
+        $data->shippingAmountInclTax = (float) ($cart->shipping_amount_incl_tax ?? $cart->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->grandTotal = (float) ($cart->grand_total ?? 0);
+        $data->baseGrandTotal = (float) ($cart->base_grand_total ?? 0);
+        $data->formattedGrandTotal = core()->formatPrice($cart->grand_total ?? 0);
+
+        $additional = $cart->additional ?
+            (is_string($cart->additional) ? json_decode($cart->additional, true) : $cart->additional) : [];
+        $data->couponCode = $additional['coupon_code'] ?? null;
+
+        if ($cart->billing_address) {
+            $data->billingAddress = [
+                'id'        => $cart->billing_address->id,
+                'firstName' => $cart->billing_address->first_name,
+                'lastName'  => $cart->billing_address->last_name,
+                'email'     => $cart->billing_address->email,
+                'address'   => $cart->billing_address->address,
+                'city'      => $cart->billing_address->city,
+                'state'     => $cart->billing_address->state,
+                'country'   => $cart->billing_address->country,
+                'postcode'  => $cart->billing_address->postcode,
+                'phone'     => $cart->billing_address->phone,
+            ];
+        }
+
+        if ($cart->shipping_address) {
+            $data->shippingAddress = [
+                'id'        => $cart->shipping_address->id,
+                'firstName' => $cart->shipping_address->first_name,
+                'lastName'  => $cart->shipping_address->last_name,
+                'email'     => $cart->shipping_address->email,
+                'address'   => $cart->shipping_address->address,
+                'city'      => $cart->shipping_address->city,
+                'state'     => $cart->shipping_address->state,
+                'country'   => $cart->shipping_address->country,
+                'postcode'  => $cart->shipping_address->postcode,
+                'phone'     => $cart->shipping_address->phone,
+            ];
+        }
+
+        if ($cart->payment) {
+            $data->paymentMethod = $cart->payment->method;
+            $data->paymentMethodTitle = core()->getConfigData('sales.payment_methods.'.$cart->payment->method.'.title');
+        }
+
+        try {
+            $taxes = collect(\Webkul\Tax\Facades\Tax::getTaxRatesWithAmount($cart, true))->map(function ($rate) {
+                return core()->currency($rate ?? 0);
+            })->toArray();
+            $data->appliedTaxes = $taxes;
+        } catch (\Exception $e) {
+            $data->appliedTaxes = [];
+        }
+
+        $data->haveStockableItems = $cart->items()->whereHas('product', function ($q) {
+            $q->where('type', 'simple');
+        })->count() > 0;
+
+        if ($cart->selected_shipping_rate) {
+            $data->selectedShippingRate = $cart->selected_shipping_rate->method ?? null;
+            $data->selectedShippingRateTitle = $cart->selected_shipping_rate->method_title ?? null;
+        } else {
+            $data->selectedShippingRate = null;
+            $data->selectedShippingRateTitle = null;
+        }
+
+        return $data;
+    }
+
+    public static function collection(iterable $carts): array
+    {
+        $cartDataCollection = [];
+        foreach ($carts as $cart) {
+            $cartDataCollection[] = self::fromModel($cart);
+        }
+
+        return $cartDataCollection;
+    }
+
+    public function getItems(): ?array
+    {
+        return $this->items;
+    }
+
+    public function setItems(?array $items): void
+    {
+        $this->items = $items;
+    }
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Redirect URL for payment gateway')]
+    public ?string $redirectUrl = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Order ID after successful order creation')]
+    public ?string $orderIncrementId = null;
+}

+ 287 - 0
packages/Webkul/BagistoApi/src/Dto/CartInput.php

@@ -0,0 +1,287 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * Input DTO for cart operations with token-based authentication
+ *
+ * Supports both authenticated users (via bearer token) and guest users (via cart token).
+ * Operations: add product, update item quantity, remove item, get cart, get all carts
+ *
+ * Authentication token is passed via Authorization: Bearer header, not as input parameter.
+ */
+class CartInput
+{
+    /**
+     * ID field (optional, for GraphQL API Platform compatibility)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation', 'query'])]
+    public ?string $id = null;
+
+    /**
+     * Cart ID (optional, for specific cart operations)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation', 'query'])]
+    public ?int $cartId = null;
+
+    /**
+     * Product ID (required for addProduct operation)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?int $productId = null;
+
+    /**
+     * Cart item ID (required for update/remove operations)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?int $cartItemId = null;
+
+    /**
+     * Quantity of items to add/update
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?int $quantity = null;
+
+    /**
+     * Product options/attributes (JSON)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?array $options = null;
+
+    /**
+     * Coupon code for discount
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?string $couponCode = null;
+
+    /**
+     * Shipping address ID
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?int $shippingAddressId = null;
+
+    /**
+     * Billing address ID
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?int $billingAddressId = null;
+
+    /**
+     * Shipping method code
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?string $shippingMethod = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Selected shipping rate object')]
+    public $selectedShippingRate = null;
+
+    /**
+     * Payment method code
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?string $paymentMethod = null;
+
+    /**
+     * Session ID for creating new guest cart (createOrGetCart operation)
+     * Used to identify guest session and generate unique token
+     */
+    #[ApiProperty(required: false, description: 'Session ID for cart creation')]
+    #[Groups(['mutation', 'query'])]
+    public ?string $sessionId = null;
+
+    /**
+     * Flag to create new cart instead of using existing one
+     * Used in createOrGetCart mutation
+     */
+    #[ApiProperty(required: false, description: 'Generate new cart with unique token')]
+    #[Groups(['mutation'])]
+    public ?bool $createNew = false;
+
+    /**
+     * Array of cart item IDs for bulk operations (remove multiple, move to wishlist)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?array $itemIds = null;
+
+    /**
+     * Array of quantities for bulk operations
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?array $quantities = null;
+
+    /**
+     * Country code for shipping estimation
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?string $country = null;
+
+    /**
+     * State/Province code for shipping estimation
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?string $state = null;
+
+    /**
+     * Postal code for shipping estimation
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?string $postcode = null;
+
+    /**
+     * Device token for push notifications (optional)
+     */
+    #[ApiProperty(required: false, description: 'Device token for push notifications')]
+    #[Groups(['mutation'])]
+    public ?string $deviceToken = null;
+
+    /**
+     * Is buy now flag (0 = add to cart, 1 = buy now)
+     * Used for direct checkout
+     */
+    #[ApiProperty(required: false, description: 'Is buy now (0 = cart, 1 = buy now)')]
+    #[Groups(['mutation'])]
+    public ?int $isBuyNow = 0;
+
+    /**
+     * Bundle product options
+     * Format: [option_id => [product_id1, product_id2, ...]]
+     * Example: [1 => [2, 3], 2 => [5]]
+     * Used for Bundle products
+     */
+    #[ApiProperty(required: false, description: 'Bundle options JSON string. Example: {"1":[1],"2":[2]}')]
+    #[Groups(['mutation'])]
+    public ?string $bundleOptions = null;
+
+    /**
+     * Bundle product option quantities
+     * Format: [option_id => quantity]
+     * Example: [1 => 2, 2 => 1]
+     * Used for Bundle products
+     */
+    #[ApiProperty(required: false, description: 'Bundle option quantities JSON string. Example: {"1":1,"2":2}')]
+    #[Groups(['mutation'])]
+    public ?string $bundleOptionQty = null;
+
+    /**
+     * Selected configurable product option (child product ID)
+     * Used for Configurable products
+     */
+    #[ApiProperty(required: false, description: 'Selected configurable option (child product ID)')]
+    #[Groups(['mutation'])]
+    public ?int $selectedConfigurableOption = null;
+
+    /**
+     * Super attribute values for configurable products
+     * Format: [attribute_id => option_value]
+     * Example: [123 => 56, 124 => 57]
+     * Used for Configurable products
+     */
+    #[ApiProperty(required: false, description: 'Super attributes for configurable products')]
+    #[Groups(['mutation'])]
+    public ?array $superAttribute = null;
+
+    /**
+     * Quantities for grouped product associated products
+     * Format: [associated_product_id => quantity]
+     * Example: [101 => 2, 102 => 1]
+     * Used for Grouped products
+     */
+    #[ApiProperty(required: false, description: 'Quantities for grouped product associated products')]
+    #[Groups(['mutation'])]
+    public ?array $qty = null;
+
+    /**
+     * Quantities for grouped product associated products (GraphQL-friendly).
+     *
+     * GraphQL input objects cannot have numeric keys (e.g. {101: 2}), so this accepts a JSON string
+     * that can represent a map: {"101":2,"102":1}.
+     *
+     * Used for Grouped products.
+     */
+    #[ApiProperty(required: false, description: 'Grouped product quantities as JSON string. Example: {"101":2,"102":1}')]
+    #[Groups(['mutation'])]
+    public ?string $groupedQty = null;
+
+    /**
+     * Downloadable product links to purchase
+     * Format: [link_id1, link_id2, ...]
+     * Used for Downloadable products
+     */
+    #[ApiProperty(
+        required: false,
+        description: 'Downloadable product link IDs'
+    )]
+    #[Groups(['mutation'])]
+    public ?array $links = null;
+
+    /**
+     * Customizable options for products
+     * Format: [option_id => value]
+     * Used for Virtual products with customizable options
+     */
+    #[ApiProperty(required: false, description: 'Customizable options')]
+    #[Groups(['mutation'])]
+    public ?array $customizableOptions = null;
+
+    /**
+     * Additional data for products
+     * Used for any extra product-specific data
+     */
+    #[ApiProperty(required: false, description: 'Additional product data')]
+    #[Groups(['mutation'])]
+    public ?array $additional = null;
+
+    /**
+     * Booking product options (GraphQL-friendly).
+     *
+     * Bagisto expects booking options under the `booking` key when adding a booking product to cart.
+     * GraphQL input objects cannot represent arbitrary/nested maps reliably in all clients, so this
+     * accepts a JSON string (decoded server-side) that will be forwarded as `booking`.
+     *
+     * Examples:
+     * - Appointment/Default/Table: {"type":"appointment","date":"2026-03-12","slot":"10:00 AM - 11:00 AM"}
+     * - Rental (hourly): {"type":"rental","date":"2026-03-12","slot":{"from":1710208800,"to":1710212400}}
+     * - Rental (daily): {"type":"rental","date_from":"2026-03-12","date_to":"2026-03-14","renting_type":"daily"}
+     * - Event: {"type":"event","qty":{"12":1,"13":2}} (at least one ticket qty > 0 required)
+     */
+    #[ApiProperty(required: false, description: 'Booking options as JSON string')]
+    #[Groups(['mutation'])]
+    public ?string $booking = null;
+
+    /**
+     * Optional booking note (mainly for table booking).
+     *
+     * If provided, it will be merged into the decoded `booking` payload as `booking.note`.
+     * This avoids clients having to embed/escape the note inside the booking JSON string.
+     */
+    #[ApiProperty(required: false, description: 'Booking note / special request')]
+    #[Groups(['mutation'])]
+    public ?string $specialNote = null;
+
+    /**
+     * Backward compatibility: previous name for `specialNote`.
+     */
+    #[ApiProperty(required: false, description: 'Deprecated: use specialNote')]
+    #[Groups(['mutation'])]
+    public ?string $bookingNote = null;
+}

+ 167 - 0
packages/Webkul/BagistoApi/src/Dto/CartItemData.php

@@ -0,0 +1,167 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GraphQl\Query;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * Data Transfer Object for Cart Items
+ *
+ * Represents individual items in a shopping cart with pricing and product information.
+ * Used in API responses for cart operations.
+ *
+ * This class is registered as an ApiResource to enable GraphQL type generation,
+ * but the Query operation with output: false means it won't be exposed as a standalone query.
+ */
+#[ApiResource(
+    shortName: 'CartItem',
+    graphQlOperations: [
+        new Query(name: 'item_query', output: false),
+    ]
+)]
+class CartItemData
+{
+    #[Groups(['query', 'mutation'])]
+    public ?int $id = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?int $cartId = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?int $productId = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $name = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $sku = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?int $quantity = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $price = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $basePrice = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $total = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseTotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $discountAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseDiscountAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $taxAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseTaxAmount = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?array $options = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $type = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $formattedPrice = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $formattedTotal = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $priceInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $basePriceInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $formattedPriceInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $totalInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?float $baseTotalInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $formattedTotalInclTax = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $baseImage = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?string $productUrlKey = null;
+
+    #[Groups(['query', 'mutation'])]
+    public ?bool $canChangeQty = null;
+
+    /**
+     * Create CartItemData from CartItem model
+     */
+    public static function fromModel(\Webkul\Checkout\Models\CartItem $item): self
+    {
+        $data = new self;
+
+        $data->id = $item->id;
+        $data->cartId = $item->cart_id;
+        $data->productId = $item->product_id;
+        $data->name = $item->name ?? ($item->product?->name ?? '');
+        $data->sku = $item->sku ?? ($item->product?->sku ?? '');
+        $data->quantity = (int) $item->quantity;
+        $data->type = $item->type;
+
+        // Base prices
+        $data->price = (float) ($item->price ?? 0);
+        $data->basePrice = (float) ($item->base_price ?? 0);
+        $data->formattedPrice = core()->formatPrice($item->price ?? 0);
+
+        // Prices including tax
+        $data->priceInclTax = (float) ($item->price_incl_tax ?? $item->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);
+
+        // Line totals
+        $data->total = (float) ($item->total ?? 0);
+        $data->baseTotal = (float) ($item->base_total ?? 0);
+        $data->formattedTotal = core()->formatPrice($item->total ?? 0);
+
+        // Line totals including tax
+        $data->totalInclTax = (float) ($item->total_incl_tax ?? $item->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);
+
+        // Discounts
+        $data->discountAmount = (float) ($item->discount_amount ?? 0);
+        $data->baseDiscountAmount = (float) ($item->base_discount_amount ?? 0);
+
+        // Tax
+        $data->taxAmount = (float) ($item->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;
+
+        // Base image
+        if ($item->product) {
+            try {
+                $data->baseImage = json_encode($item->product->getTypeInstance()->getBaseImage($item));
+            } catch (\Exception $e) {
+                $data->baseImage = null;
+            }
+            $data->productUrlKey = $item->product->url_key;
+            $data->canChangeQty = $item->product->getTypeInstance()->showQuantityBox();
+        }
+
+        return $data;
+    }
+}

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

@@ -0,0 +1,124 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * CheckoutAddressInput - GraphQL Input DTO for Checkout Address
+ *
+ * Input for storing billing and shipping addresses during checkout
+ * Authentication token is passed via Authorization: Bearer header, NOT as input parameter
+ */
+class CheckoutAddressInput
+{
+    // Billing Address
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing first name')]
+    public ?string $billingFirstName = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing last name')]
+    public ?string $billingLastName = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing email')]
+    public ?string $billingEmail = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing company name')]
+    public ?string $billingCompanyName = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing address')]
+    public ?string $billingAddress = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing country')]
+    public ?string $billingCountry = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing state')]
+    public ?string $billingState = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing city')]
+    public ?string $billingCity = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing postcode')]
+    public ?string $billingPostcode = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Billing phone number')]
+    public ?string $billingPhoneNumber = null;
+
+    // Shipping Address
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping first name')]
+    public ?string $shippingFirstName = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping last name')]
+    public ?string $shippingLastName = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping email')]
+    public ?string $shippingEmail = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping company name')]
+    public ?string $shippingCompanyName = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping address')]
+    public ?string $shippingAddress = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping country')]
+    public ?string $shippingCountry = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping state')]
+    public ?string $shippingState = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping city')]
+    public ?string $shippingCity = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping postcode')]
+    public ?string $shippingPostcode = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping phone number')]
+    public ?string $shippingPhoneNumber = null;
+
+    // Flags
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Use address for shipping')]
+    public ?bool $useForShipping = null;
+
+    // Additional fields for shipping and payment methods
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Shipping method code')]
+    public ?string $shippingMethod = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Payment method code')]
+    public ?string $paymentMethod = null;
+
+    // Payment callback URLs (for headless frontends)
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Payment success callback URL')]
+    public ?string $paymentSuccessUrl = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Payment failure callback URL')]
+    public ?string $paymentFailureUrl = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Payment cancel callback URL')]
+    public ?string $paymentCancelUrl = null;
+}

+ 116 - 0
packages/Webkul/BagistoApi/src/Dto/CheckoutAddressOutput.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * CheckoutAddressOutput - GraphQL Output DTO for Checkout Address
+ *
+ * Output for retrieving billing and shipping addresses during checkout
+ */
+class CheckoutAddressOutput
+{
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?int $id = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $cartToken = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?int $customerId = null;
+
+    // Billing Address
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingFirstName = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingLastName = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingEmail = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingCompanyName = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingAddress = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingCountry = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingState = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingCity = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingPostcode = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $billingPhoneNumber = null;
+
+    // Shipping Address
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingFirstName = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingLastName = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingEmail = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingCompanyName = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingAddress = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingCountry = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingState = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingCity = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingPostcode = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $shippingPhoneNumber = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?bool $success = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $message = null;
+}

+ 31 - 0
packages/Webkul/BagistoApi/src/Dto/CheckoutAddressPayload.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * CheckoutAddressPayload - Response DTO for CreateCheckoutAddress mutation
+ *
+ * Wraps the created address and cart information in a payload structure
+ * that matches the expected GraphQL response format
+ */
+class CheckoutAddressPayload
+{
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'The created or updated cart address')]
+    public ?CheckoutAddressOutput $checkoutAddress = null;
+
+    #[Groups(['mutation'])]
+    #[ApiProperty(description: 'Current cart state')]
+    public ?CartData $cart = null;
+
+    public function __construct(
+        ?CheckoutAddressOutput $checkoutAddress = null,
+        ?CartData $cart = null
+    ) {
+        $this->checkoutAddress = $checkoutAddress;
+        $this->cart = $cart;
+    }
+}

+ 11 - 0
packages/Webkul/BagistoApi/src/Dto/CheckoutAddressQueryInput.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+/**
+ * CheckoutAddressQueryInput - GraphQL Input DTO for Checkout Address Query
+ *
+ * Input for querying billing and shipping addresses during checkout
+ * Authentication token is passed via Authorization: Bearer header, NOT as input parameter
+ */
+class CheckoutAddressQueryInput {}

+ 28 - 0
packages/Webkul/BagistoApi/src/Dto/ContactUsInput.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DTO for Contact Us form submission via GraphQL mutation and REST API
+ */
+class ContactUsInput
+{
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation', 'query'])]
+    public ?string $id = null;
+
+    #[Groups(['mutation'])]
+    public string $name;
+
+    #[Groups(['mutation'])]
+    public string $email;
+
+    #[Groups(['mutation'])]
+    public ?string $contact = null;
+
+    #[Groups(['mutation'])]
+    public string $message;
+}

+ 20 - 0
packages/Webkul/BagistoApi/src/Dto/ContactUsOutput.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DTO for Contact Us form submission response
+ */
+class ContactUsOutput
+{
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public bool $success;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public string $message;
+}

+ 20 - 0
packages/Webkul/BagistoApi/src/Dto/CreateCompareItemInput.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+
+/**
+ * DTO for creating a compare item
+ *
+ * Defines the input structure for the createCompareItem mutation
+ * Customer ID is automatically determined from the authenticated user
+ */
+class CreateCompareItemInput
+{
+    /**
+     * Product ID to add to comparison
+     */
+    #[ApiProperty(description: 'The ID of the product to add to comparison')]
+    public ?int $productId = null;
+}

+ 58 - 0
packages/Webkul/BagistoApi/src/Dto/CreateProductReviewInput.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiResource;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DTO for creating product reviews via GraphQL mutation
+ * This explicitly defines all input fields for the GraphQL schema
+ */
+#[ApiResource]
+class CreateProductReviewInput
+{
+    #[Groups(['mutation'])]
+    public int $productId;
+
+    #[Groups(['mutation'])]
+    public string $title;
+
+    #[Groups(['mutation'])]
+    public string $comment;
+
+    #[Groups(['mutation'])]
+    public int $rating;
+
+    #[Groups(['mutation'])]
+    public string $name;
+
+    #[Groups(['mutation'])]
+    public ?string $email = null;
+
+    #[Groups(['mutation'])]
+    public ?int $status = null;
+
+    #[Groups(['mutation'])]
+    public ?string $attachments;
+
+    public function __construct(
+        int $productId,
+        string $title,
+        string $comment,
+        int $rating,
+        string $name,
+        ?string $email = null,
+        ?int $status = null,
+        ?string $attachments = '',
+    ) {
+        $this->productId = $productId;
+        $this->title = $title;
+        $this->comment = $comment;
+        $this->rating = $rating;
+        $this->name = $name;
+        $this->email = $email;
+        $this->status = $status;
+        $this->attachments = $attachments;
+    }
+}

+ 20 - 0
packages/Webkul/BagistoApi/src/Dto/CreateWishlistInput.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+
+/**
+ * DTO for creating a wishlist item
+ *
+ * Defines the input structure for the createWishlist mutation
+ * Customer ID and channel ID are automatically determined from the authenticated user and current channel
+ */
+class CreateWishlistInput
+{
+    /**
+     * Product ID to add to wishlist
+     */
+    #[ApiProperty(description: 'The ID of the product to add to wishlist')]
+    public ?int $productId = null;
+}

+ 117 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerAddressInput.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\SerializedName;
+
+/**
+ * Input DTO for customer address operations with token-based authentication
+ * Token is passed via Authorization: Bearer header, NOT as input parameter
+ *
+ * NOTE: Token is NOT a DTO property. It is extracted from the Authorization header
+ * via TokenHeaderFacade::getAuthorizationBearerToken() in the processor.
+ */
+class CustomerAddressInput
+{
+    /**
+     * Identifier for API Platform GraphQL serialization
+     */
+    #[SerializedName('id')]
+    #[ApiProperty(identifier: true)]
+    #[Groups(['mutation'])]
+    public ?int $id = null;
+
+    /**
+     * Address ID (required for update/delete, not used for create)
+     */
+    #[SerializedName('addressId')]
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?int $addressId = null;
+
+    /**
+     * First name
+     */
+    #[SerializedName('firstName')]
+    #[Groups(['mutation'])]
+    public ?string $firstName = null;
+
+    /**
+     * Last name
+     */
+    #[SerializedName('lastName')]
+    #[Groups(['mutation'])]
+    public ?string $lastName = null;
+
+    /**
+     * Email address
+     */
+    #[SerializedName('email')]
+    #[Groups(['mutation'])]
+    public ?string $email = null;
+
+    /**
+     * Phone number
+     */
+    #[SerializedName('phone')]
+    #[Groups(['mutation'])]
+    public ?string $phone = null;
+
+    /**
+     * Street address line 1
+     */
+    #[SerializedName('address1')]
+    #[Groups(['mutation'])]
+    public ?string $address1 = null;
+
+    /**
+     * Street address line 2
+     */
+    #[SerializedName('address2')]
+    #[Groups(['mutation'])]
+    public ?string $address2 = null;
+
+    /**
+     * Country
+     */
+    #[SerializedName('country')]
+    #[Groups(['mutation'])]
+    public ?string $country = null;
+
+    /**
+     * State/Province
+     */
+    #[SerializedName('state')]
+    #[Groups(['mutation'])]
+    public ?string $state = null;
+
+    /**
+     * City
+     */
+    #[SerializedName('city')]
+    #[Groups(['mutation'])]
+    public ?string $city = null;
+
+    /**
+     * Postal code
+     */
+    #[SerializedName('postcode')]
+    #[Groups(['mutation'])]
+    public ?string $postcode = null;
+
+    /**
+     * Use for shipping
+     */
+    #[SerializedName('useForShipping')]
+    #[Groups(['mutation'])]
+    public ?bool $useForShipping = null;
+
+    /**
+     * Set as default address
+     */
+    #[SerializedName('defaultAddress')]
+    #[Groups(['mutation'])]
+    public ?bool $defaultAddress = null;
+}

+ 56 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerAddressOutput.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class CustomerAddressOutput
+{
+    #[Groups(['read'])]
+    public ?int $id = null;
+
+    #[Groups(['read'])]
+    public ?string $firstName = null;
+
+    #[Groups(['read'])]
+    public ?string $lastName = null;
+
+    #[Groups(['read'])]
+    public ?string $email = null;
+
+    #[Groups(['read'])]
+    public ?string $phone = null;
+
+    #[Groups(['read'])]
+    public ?string $company = null;
+
+    #[Groups(['read'])]
+    public ?string $address1 = null;
+
+    #[Groups(['read'])]
+    public ?string $address2 = null;
+
+    #[Groups(['read'])]
+    public ?string $country = null;
+
+    #[Groups(['read'])]
+    public ?string $state = null;
+
+    #[Groups(['read'])]
+    public ?string $city = null;
+
+    #[Groups(['read'])]
+    public ?string $postcode = null;
+
+    #[Groups(['read'])]
+    public ?bool $useForShipping = null;
+
+    #[Groups(['read'])]
+    public ?bool $defaultAddress = null;
+
+    #[Groups(['read'])]
+    public ?bool $success = null;
+
+    #[Groups(['read'])]
+    public ?string $message = null;
+}

+ 14 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerLoginInput.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+/**
+ * DTO for customer login input
+ */
+class CustomerLoginInput
+{
+    public function __construct(
+        public ?string $email = null,
+        public ?string $password = null,
+    ) {}
+}

+ 120 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerProfileInput.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * Input DTO for customer profile operations with token-based authentication
+ * Token is passed via Authorization: Bearer header, NOT as input parameter
+ *
+ * NOTE: Token is NOT a DTO property. It is extracted from the Authorization header
+ * via TokenHeaderFacade::getAuthorizationBearerToken() in the processor.
+ */
+class CustomerProfileInput
+{
+    /**
+     * Customer ID (optional for get, required for update/delete when multiple profiles exist)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation'])]
+    public ?string $id = null;
+
+    /**
+     * First name
+     */
+    #[Groups(['mutation'])]
+    public ?string $firstName = null;
+
+    /**
+     * Last name
+     */
+    #[Groups(['mutation'])]
+    public ?string $lastName = null;
+
+    /**
+     * Email address
+     */
+    #[Groups(['mutation'])]
+    public ?string $email = null;
+
+    /**
+     * Phone number
+     */
+    #[Groups(['mutation'])]
+    public ?string $phone = null;
+
+    /**
+     * Gender
+     */
+    #[Groups(['mutation'])]
+    public ?string $gender = null;
+
+    /**
+     * Date of birth
+     */
+    #[Groups(['mutation'])]
+    public ?string $dateOfBirth = null;
+
+    /**
+     * Current password (for password change verification)
+     */
+    #[Groups(['mutation'])]
+    public ?string $password = null;
+
+    /**
+     * New password confirmation
+     */
+    #[Groups(['mutation'])]
+    public ?string $confirmPassword = null;
+
+    /**
+     * Customer status
+     */
+    #[Groups(['mutation'])]
+    public ?string $status = null;
+
+    /**
+     * Newsletter subscription
+     */
+    #[Groups(['mutation'])]
+    public ?bool $subscribedToNewsLetter = null;
+
+    /**
+     * Verification status
+     */
+    #[Groups(['mutation'])]
+    public ?string $isVerified = null;
+
+    /**
+     * Suspension status
+     */
+    #[Groups(['mutation'])]
+    public ?string $isSuspended = null;
+
+    /**
+     * Customer profile image (base64 encoded)
+     * Format: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA...
+     */
+    #[Groups(['mutation'])]
+    public ?string $image = null;
+
+    /**
+     * Flag to delete existing image
+     */
+    #[Groups(['mutation'])]
+    public ?bool $deleteImage = null;
+
+    /**
+     * Success status of the operation
+     */
+    #[Groups(['mutation'])]
+    public ?bool $success = null;
+
+    /**
+     * Response message
+     */
+    #[Groups(['mutation'])]
+    public ?string $message = null;
+}

+ 116 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerProfileOutput.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\SerializedName;
+
+/**
+ * Output DTO for customer profile mutation responses
+ * Uses camelCase for GraphQL field names
+ */
+class CustomerProfileOutput
+{
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('id')]
+    #[Groups(['mutation'])]
+    public ?string $id = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('_id')]
+    #[Groups(['mutation'])]
+    public ?string $_id = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('firstName')]
+    #[Groups(['mutation'])]
+    public ?string $firstName = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('lastName')]
+    #[Groups(['mutation'])]
+    public ?string $lastName = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['mutation'])]
+    public ?string $email = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['mutation'])]
+    public ?string $phone = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['mutation'])]
+    public ?string $gender = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('dateOfBirth')]
+    #[Groups(['mutation'])]
+    public ?string $dateOfBirth = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['mutation'])]
+    public ?string $status = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('subscribedToNewsLetter')]
+    #[Groups(['mutation'])]
+    public ?bool $subscribedToNewsLetter = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('isVerified')]
+    #[Groups(['mutation'])]
+    public ?string $isVerified = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[SerializedName('isSuspended')]
+    #[Groups(['mutation'])]
+    public ?string $isSuspended = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['mutation'])]
+    public ?string $image = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['mutation'])]
+    public ?bool $success = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['mutation'])]
+    public ?string $message = null;
+
+    public function __construct(
+        ?string $id = null,
+        ?string $_id = null,
+        ?string $firstName = null,
+        ?string $lastName = null,
+        ?string $email = null,
+        ?string $phone = null,
+        ?string $gender = null,
+        ?string $dateOfBirth = null,
+        ?string $status = null,
+        ?bool $subscribedToNewsLetter = null,
+        ?string $isVerified = null,
+        ?string $isSuspended = null,
+        ?string $image = null,
+        ?bool $success = null,
+        ?string $message = null,
+    ) {
+        $this->id = $id;
+        $this->_id = $_id;
+        $this->firstName = $firstName;
+        $this->lastName = $lastName;
+        $this->email = $email;
+        $this->phone = $phone;
+        $this->gender = $gender;
+        $this->dateOfBirth = $dateOfBirth;
+        $this->status = $status;
+        $this->subscribedToNewsLetter = $subscribedToNewsLetter;
+        $this->isVerified = $isVerified;
+        $this->isSuspended = $isSuspended;
+        $this->image = $image;
+        $this->success = $success;
+        $this->message = $message;
+    }
+}

+ 52 - 0
packages/Webkul/BagistoApi/src/Dto/CustomerVerifyOutput.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\SerializedName;
+
+/**
+ * DTO for customer info returned from token verification
+ */
+class CustomerVerifyOutput
+{
+    #[ApiProperty(writable: false, readable: true)]
+    #[SerializedName('id')]
+    public ?int $id = null;
+
+    #[ApiProperty(writable: false, readable: true)]
+    #[SerializedName('firstName')]
+    public ?string $firstName = null;
+
+    #[ApiProperty(writable: false, readable: true)]
+    #[SerializedName('lastName')]
+    public ?string $lastName = null;
+
+    #[ApiProperty(writable: false, readable: true)]
+    #[SerializedName('email')]
+    public ?string $email = null;
+
+    #[ApiProperty(writable: false, readable: true)]
+    #[SerializedName('isValid')]
+    public ?bool $isValid = null;
+
+    #[ApiProperty(writable: false, readable: true)]
+    #[SerializedName('message')]
+    public ?string $message = null;
+
+    public function __construct(
+        ?int $id = null,
+        ?string $firstName = null,
+        ?string $lastName = null,
+        ?string $email = null,
+        ?bool $isValid = null,
+        ?string $message = null,
+    ) {
+        $this->id = $id;
+        $this->firstName = $firstName;
+        $this->lastName = $lastName;
+        $this->email = $email;
+        $this->isValid = $isValid;
+        $this->message = $message;
+    }
+}

+ 12 - 0
packages/Webkul/BagistoApi/src/Dto/DeleteAllCompareItemsInput.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+/**
+ * DTO for deleting all compare items
+ *
+ * Empty input — customer is determined from the auth token
+ */
+class DeleteAllCompareItemsInput
+{
+}

+ 12 - 0
packages/Webkul/BagistoApi/src/Dto/DeleteAllWishlistsInput.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+/**
+ * DTO for deleting all wishlist items
+ *
+ * Empty input — customer is determined from the auth token
+ */
+class DeleteAllWishlistsInput
+{
+}

+ 19 - 0
packages/Webkul/BagistoApi/src/Dto/DeleteCompareItemInput.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+
+/**
+ * DTO for deleting compare items
+ *
+ * Defines the input structure for deleting items from compare list
+ */
+class DeleteCompareItemInput
+{
+    /**
+     * Compare item ID to delete
+     */
+    #[ApiProperty(description: 'The ID of the compare item to delete')]
+    public ?string $id = null;
+}

+ 19 - 0
packages/Webkul/BagistoApi/src/Dto/DeleteWishlistInput.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+
+/**
+ * DTO for deleting wishlist items
+ *
+ * Defines the input structure for deleting items from wishlist
+ */
+class DeleteWishlistInput
+{
+    /**
+     * Wishlist item ID to delete
+     */
+    #[ApiProperty(description: 'The ID of the wishlist item to delete')]
+    public ?string $id = null;
+}

+ 21 - 0
packages/Webkul/BagistoApi/src/Dto/DestroySelectedInput.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class DestroySelectedInput
+{
+    #[Groups(['mutation'])]
+    public ?array $ids = null;
+
+    public function getIds(): ?array
+    {
+        return $this->ids;
+    }
+
+    public function setIds(?array $ids): void
+    {
+        $this->ids = $ids;
+    }
+}

+ 29 - 0
packages/Webkul/BagistoApi/src/Dto/DownloadLinkOutput.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+
+/**
+ * DownloadLinkOutput
+ *
+ * Output DTO for download link generation mutation
+ */
+#[ApiResource(operations: [])]
+class DownloadLinkOutput
+{
+    public function __construct(
+        #[ApiProperty(identifier: true, writable: false)]
+        public string $id = '1',
+
+        #[ApiProperty(description: 'Temporary download token')]
+        public ?string $token = null,
+
+        #[ApiProperty(description: 'Download URL')]
+        public ?string $url = null,
+
+        #[ApiProperty(description: 'Token expiration timestamp')]
+        public ?string $expiresAt = null,
+    ) {}
+}

+ 60 - 0
packages/Webkul/BagistoApi/src/Dto/EstimateShippingMethodsInput.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class EstimateShippingMethodsInput
+{
+    #[Groups(['mutation'])]
+    public ?string $country = null;
+
+    #[Groups(['mutation'])]
+    public ?string $state = null;
+
+    #[Groups(['mutation'])]
+    public ?string $postcode = null;
+
+    #[Groups(['mutation'])]
+    public ?string $shipping_method = null;
+
+    public function getCountry(): ?string
+    {
+        return $this->country;
+    }
+
+    public function setCountry(?string $country): void
+    {
+        $this->country = $country;
+    }
+
+    public function getState(): ?string
+    {
+        return $this->state;
+    }
+
+    public function setState(?string $state): void
+    {
+        $this->state = $state;
+    }
+
+    public function getPostcode(): ?string
+    {
+        return $this->postcode;
+    }
+
+    public function setPostcode(?string $postcode): void
+    {
+        $this->postcode = $postcode;
+    }
+
+    public function getShipping_method(): ?string
+    {
+        return $this->shipping_method;
+    }
+
+    public function setShipping_method(?string $shipping_method): void
+    {
+        $this->shipping_method = $shipping_method;
+    }
+}

+ 13 - 0
packages/Webkul/BagistoApi/src/Dto/ForgotPasswordInput.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class ForgotPasswordInput
+{
+    #[ApiProperty(writable: true, readable: false)]
+    #[Groups(['mutation'])]
+    public string $email;
+}

+ 21 - 0
packages/Webkul/BagistoApi/src/Dto/GenerateDownloadLinkInput.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+
+/**
+ * GenerateDownloadLinkInput
+ *
+ * DTO for generating download links for purchased downloadable products
+ */
+class GenerateDownloadLinkInput
+{
+    #[ApiProperty(description: 'ID of the purchased downloadable link')]
+    public int $downloadableLinkPurchasedId;
+
+    public function __construct(int $downloadableLinkPurchasedId)
+    {
+        $this->downloadableLinkPurchasedId = $downloadableLinkPurchasedId;
+    }
+}

+ 32 - 0
packages/Webkul/BagistoApi/src/Dto/LoginInput.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DTO for customer login via GraphQL mutation input
+ * This explicitly defines all input fields for the GraphQL schema
+ * Note: No 'id' field here - only email and password are input
+ */
+class LoginInput
+{
+    #[Groups(['mutation'])]
+    public string $email;
+
+    #[Groups(['mutation'])]
+    public string $password;
+
+    #[Groups(['mutation'])]
+    public ?string $deviceToken = null;
+
+    public function __construct(
+        string $email = '',
+        string $password = '',
+        ?string $deviceToken = null,
+    ) {
+        $this->email = $email;
+        $this->password = $password;
+        $this->deviceToken = $deviceToken;
+    }
+}

+ 23 - 0
packages/Webkul/BagistoApi/src/Dto/LogoutInput.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * LogoutInput DTO
+ *
+ * Contains input fields for logout mutation.
+ * The token is extracted from the Authorization header, but deviceToken can be passed to remove it from the device tokens table.
+ */
+class LogoutInput
+{
+    #[Groups(['mutation'])]
+    public ?string $deviceToken = null;
+
+    public function __construct(
+        ?string $deviceToken = null,
+    ) {
+        $this->deviceToken = $deviceToken;
+    }
+}

+ 23 - 0
packages/Webkul/BagistoApi/src/Dto/LogoutOutput.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+
+/**
+ * DTO for logout response
+ */
+class LogoutOutput
+{
+    #[ApiProperty(writable: false, readable: true)]
+    public ?bool $success = null;
+
+    #[ApiProperty(writable: false, readable: true)]
+    public ?string $message = null;
+
+    public function __construct(?bool $success = null, ?string $message = null)
+    {
+        $this->success = $success;
+        $this->message = $message;
+    }
+}

+ 34 - 0
packages/Webkul/BagistoApi/src/Dto/MoveToWishlistInput.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class MoveToWishlistInput
+{
+    #[Groups(['mutation'])]
+    public ?array $ids = null;
+
+    #[Groups(['mutation'])]
+    public ?array $qty = null;
+
+    public function getIds(): ?array
+    {
+        return $this->ids;
+    }
+
+    public function setIds(?array $ids): void
+    {
+        $this->ids = $ids;
+    }
+
+    public function getQty(): ?array
+    {
+        return $this->qty;
+    }
+
+    public function setQty(?array $qty): void
+    {
+        $this->qty = $qty;
+    }
+}

+ 25 - 0
packages/Webkul/BagistoApi/src/Dto/MoveWishlistToCartInput.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+
+/**
+ * DTO for moving wishlist items to cart
+ *
+ * Defines the input structure for moving items from wishlist to cart
+ */
+class MoveWishlistToCartInput
+{
+    /**
+     * Wishlist item ID to move to cart
+     */
+    #[ApiProperty(description: 'The numeric ID of the wishlist item to move to cart')]
+    public ?int $wishlistItemId = null;
+
+    /**
+     * Quantity of the item to add to cart
+     */
+    #[ApiProperty(description: 'Quantity of the item to add to cart')]
+    public int $quantity = 1;
+}

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

@@ -0,0 +1,27 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * Move Wishlist to Cart Output DTO
+ */
+class MoveWishlistToCartOutput
+{
+    #[ApiProperty(identifier: true)]
+    public ?int $id = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Whether the operation was successful')]
+    public ?bool $success = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'Message describing the result')]
+    public ?string $message = null;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(description: 'ID of the wishlist item that was moved')]
+    public ?int $wishlistItemId = null;
+}

+ 42 - 0
packages/Webkul/BagistoApi/src/Dto/PaymentMethodOutput.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * PaymentMethodOutput - GraphQL Output DTO for Payment Methods
+ *
+ * Output for retrieving available payment methods during checkout
+ */
+class PaymentMethodOutput
+{
+    #[Groups(['query'])]
+    #[ApiProperty(identifier: true, readable: true, writable: false)]
+    public ?string $id = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $method = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $title = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $description = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $icon = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?array $additionalData = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?bool $isAllowed = null;
+}

+ 21 - 0
packages/Webkul/BagistoApi/src/Dto/RemoveFromCartInput.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class RemoveFromCartInput
+{
+    #[Groups(['mutation'])]
+    public ?int $cart_item_id = null;
+
+    public function getCart_item_id(): ?int
+    {
+        return $this->cart_item_id;
+    }
+
+    public function setCart_item_id(?int $cart_item_id): void
+    {
+        $this->cart_item_id = $cart_item_id;
+    }
+}

+ 26 - 0
packages/Webkul/BagistoApi/src/Dto/ReorderInput.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * DTO for reordering from a previous customer order
+ *
+ * Defines input structure for the reorder mutation
+ */
+#[ApiResource]
+class ReorderInput
+{
+    /**
+     * The ID of the order to reorder
+     */
+    #[ApiProperty(
+        description: 'The ID of the order to reorder from',
+        required: true
+    )]
+    #[Groups(['mutation'])]
+    public ?int $orderId = null;
+}

+ 66 - 0
packages/Webkul/BagistoApi/src/Dto/ShippingRateOutput.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * ShippingRateOutput - GraphQL Output DTO for Shipping Rates
+ *
+ * Output for retrieving available shipping rates during checkout
+ */
+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;
+
+    #[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;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $methodDescription = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $basePrice = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $baseFormattedPrice = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $carrier = null;
+
+    #[Groups(['query'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $carrierTitle = null;
+}

+ 19 - 0
packages/Webkul/BagistoApi/src/Dto/SubscribeToNewsletterInput.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class SubscribeToNewsletterInput
+{
+    /**
+     * ID field (optional, for GraphQL API Platform compatibility)
+     */
+    #[ApiProperty(required: false)]
+    #[Groups(['mutation', 'query'])]
+    public ?string $id = null;
+
+    #[Groups(['mutation'])]
+    public string $customerEmail;
+}

+ 22 - 0
packages/Webkul/BagistoApi/src/Dto/SubscribeToNewsletterOutput.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use ApiPlatform\Metadata\ApiProperty;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+/**
+ * ShippingRateOutput - GraphQL Output DTO for Shipping Rates
+ *
+ * Output for retrieving available shipping rates during checkout
+ */
+class SubscribeToNewsletterOutput
+{
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public bool $success;
+
+    #[Groups(['query', 'mutation'])]
+    #[ApiProperty(readable: true, writable: false)]
+    public string $message;
+}

+ 21 - 0
packages/Webkul/BagistoApi/src/Dto/UpdateCartItemInput.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class UpdateCartItemInput
+{
+    #[Groups(['mutation'])]
+    public ?array $items = null;
+
+    public function getItems(): ?array
+    {
+        return $this->items;
+    }
+
+    public function setItems(?array $items): void
+    {
+        $this->items = $items;
+    }
+}

+ 58 - 0
packages/Webkul/BagistoApi/src/Dto/UpdateProductReviewInput.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\SerializedName;
+
+/**
+ * DTO for updating product reviews via GraphQL mutation
+ * The 'id' field accepts IRI format like "/api/shop/reviews/16"
+ */
+class UpdateProductReviewInput
+{
+    #[Groups(['mutation'])]
+    public string $id;
+
+    #[Groups(['mutation'])]
+    #[SerializedName('productId')]
+    public ?int $product_id = null;
+
+    #[Groups(['mutation'])]
+    public ?string $title = null;
+
+    #[Groups(['mutation'])]
+    public ?string $comment = null;
+
+    #[Groups(['mutation'])]
+    public ?int $rating = null;
+
+    #[Groups(['mutation'])]
+    public ?string $name = null;
+
+    #[Groups(['mutation'])]
+    public ?string $email = null;
+
+    #[Groups(['mutation'])]
+    public ?int $status = null;
+
+    public function __construct(
+        string $id = '',
+        ?int $product_id = null,
+        ?string $title = null,
+        ?string $comment = null,
+        ?int $rating = null,
+        ?string $name = null,
+        ?string $email = null,
+        ?int $status = null,
+    ) {
+        $this->id = $id;
+        $this->product_id = $product_id;
+        $this->title = $title;
+        $this->comment = $comment;
+        $this->rating = $rating;
+        $this->name = $name;
+        $this->email = $email;
+        $this->status = $status;
+    }
+}

+ 13 - 0
packages/Webkul/BagistoApi/src/Dto/VerifyTokenInput.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace Webkul\BagistoApi\Dto;
+
+/**
+ * DTO for verifying customer token
+ * This is used in GraphQL mutations to validate token and get customer details
+ * Token is passed via Authorization: Bearer header, NOT as input parameter.
+ *
+ * NOTE: Token is NOT a DTO property. It is extracted from the Authorization header
+ * via TokenHeaderFacade::getAuthorizationBearerToken() in the processor.
+ */
+class VerifyTokenInput {}

+ 16 - 0
packages/Webkul/BagistoApi/src/Exception/AuthenticationException.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Webkul\BagistoApi\Exception;
+
+class AuthenticationException extends \Exception implements \GraphQL\Error\ClientAware
+{
+    public function isClientSafe(): bool
+    {
+        return true;
+    }
+
+    public function getCategory(): string
+    {
+        return 'authentication';
+    }
+}

+ 29 - 0
packages/Webkul/BagistoApi/src/Exception/AuthorizationException.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Webkul\BagistoApi\Exception;
+
+/**
+ * AuthorizationException
+ *
+ * Thrown when an authenticated user tries to access a resource they don't have permission for.
+ * This is different from AuthenticationException - the user IS authenticated but lacks permissions.
+ *
+ * Examples:
+ * - User trying to access another user's cart
+ * - User trying to modify another user's order
+ * - User trying to access resources outside their scope
+ *
+ * Status Code: 403 Forbidden
+ */
+class AuthorizationException extends \Exception implements \GraphQL\Error\ClientAware
+{
+    public function isClientSafe(): bool
+    {
+        return true;
+    }
+
+    public function getCategory(): string
+    {
+        return 'authorization';
+    }
+}

+ 93 - 0
packages/Webkul/BagistoApi/src/Exception/InvalidInputException.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace Webkul\BagistoApi\Exception;
+
+use ApiPlatform\Metadata\Exception\HttpExceptionInterface;
+use ApiPlatform\Metadata\Exception\ProblemExceptionInterface;
+
+/**
+ * InvalidInputException
+ *
+ * Thrown when user input validation fails.
+ * This covers required field validation, data type validation, and business logic validation.
+ *
+ * Examples:
+ * - Missing required fields (productId, quantity, cartItemId)
+ * - Invalid quantity value
+ * - Missing coupon code
+ * - Invalid address data
+ *
+ * Status Code: 400 Bad Request
+ */
+class InvalidInputException extends \Exception implements \GraphQL\Error\ClientAware, HttpExceptionInterface, ProblemExceptionInterface
+{
+    private int $status = 400;
+
+    private array $headers = [];
+
+    public function isClientSafe(): bool
+    {
+        return true;
+    }
+
+    public function getCategory(): string
+    {
+        return 'invalid_input';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getStatusCode(): int
+    {
+        return $this->status;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getHeaders(): array
+    {
+        return $this->headers;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getType(): string
+    {
+        return '/errors/400';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTitle(): ?string
+    {
+        return 'Bad Request';
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getStatus(): ?int
+    {
+        return $this->status;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDetail(): ?string
+    {
+        return $this->message;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getInstance(): ?string
+    {
+        return null;
+    }
+}

+ 32 - 0
packages/Webkul/BagistoApi/src/Exception/OperationFailedException.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace Webkul\BagistoApi\Exception;
+
+/**
+ * OperationFailedException
+ *
+ * Thrown when an operation fails due to system errors or unexpected conditions.
+ * This is for failures during actual processing, not input validation errors.
+ *
+ * Examples:
+ * - Failed to add product to cart (system error)
+ * - Failed to update cart item (system error)
+ * - Failed to remove item from cart
+ * - Failed to apply coupon
+ * - Failed to estimate shipping
+ * - Failed to merge carts
+ *
+ * Status Code: 500 Internal Server Error (but ClientAware for GraphQL)
+ */
+class OperationFailedException extends \Exception implements \GraphQL\Error\ClientAware
+{
+    public function isClientSafe(): bool
+    {
+        return true;
+    }
+
+    public function getCategory(): string
+    {
+        return 'operation_failed';
+    }
+}

+ 29 - 0
packages/Webkul/BagistoApi/src/Exception/ResourceNotFoundException.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Webkul\BagistoApi\Exception;
+
+/**
+ * ResourceNotFoundException
+ *
+ * Thrown when a requested resource (cart, product, item, etc.) is not found.
+ * This is for legitimate 404-type errors where the resource simply doesn't exist.
+ *
+ * Examples:
+ * - Cart not found
+ * - Product not found
+ * - Cart item not found
+ *
+ * Status Code: 404 Not Found
+ */
+class ResourceNotFoundException extends \Exception implements \GraphQL\Error\ClientAware
+{
+    public function isClientSafe(): bool
+    {
+        return true;
+    }
+
+    public function getCategory(): string
+    {
+        return 'resource_not_found';
+    }
+}

+ 19 - 0
packages/Webkul/BagistoApi/src/Exception/ValidationException.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Webkul\BagistoApi\Exception;
+
+/**
+ * ValidationException
+ */
+class ValidationException extends \Exception implements \GraphQL\Error\ClientAware
+{
+    public function isClientSafe(): bool
+    {
+        return true;
+    }
+
+    public function getCategory(): string
+    {
+        return 'validation';
+    }
+}

+ 21 - 0
packages/Webkul/BagistoApi/src/Facades/CartTokenFacade.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Webkul\BagistoApi\Facades;
+
+use Illuminate\Support\Facades\Facade;
+
+/**
+ * @method static object|null getCartByToken(string $token)
+ * @method static object|null getCartById(int $cartId)
+ * @method static object|null getCustomerByToken(string $token)
+ * @method static object|null getGuestTokenRecord(string $token)
+ * @method static string getTokenType(string $token)
+ * @method static bool isValidToken(string $token)
+ */
+class CartTokenFacade extends Facade
+{
+    protected static function getFacadeAccessor()
+    {
+        return 'cart-token-service';
+    }
+}

+ 22 - 0
packages/Webkul/BagistoApi/src/Facades/TokenHeaderFacade.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace Webkul\BagistoApi\Facades;
+
+use Illuminate\Support\Facades\Facade;
+
+/**
+ * TokenHeaderFacade - Facade for extracting Bearer tokens from Authorization headers
+ *
+ * @method static string|null getAuthorizationBearerToken(\Illuminate\Http\Request $request)
+ * @method static bool hasAuthorizationToken(\Illuminate\Http\Request $request)
+ * @method static string|null extractToken(\Illuminate\Http\Request $request)
+ *
+ * @see \Webkul\BagistoApi\Services\TokenHeaderService
+ */
+class TokenHeaderFacade extends Facade
+{
+    protected static function getFacadeAccessor()
+    {
+        return 'token-header-service';
+    }
+}

+ 521 - 0
packages/Webkul/BagistoApi/src/GraphQl/Serializer/FixedSerializerContextBuilder.php

@@ -0,0 +1,521 @@
+<?php
+
+namespace Webkul\BagistoApi\GraphQl\Serializer;
+
+use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface;
+use ApiPlatform\Metadata\GraphQl\Mutation;
+use ApiPlatform\Metadata\GraphQl\Operation;
+use GraphQL\Type\Definition\ResolveInfo;
+use Illuminate\Database\Eloquent\Model;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
+use Webkul\BagistoApi\Models\CustomerOrderItem;
+use Webkul\BagistoApi\Models\CustomerOrder;
+use Webkul\BagistoApi\Models\CustomerInvoiceItem;
+use Webkul\BagistoApi\Models\CustomerInvoice;
+use Webkul\BagistoApi\Models\CustomerInvoiceAddress;
+use Webkul\BagistoApi\Models\CustomerOrderShipment;
+use Webkul\BagistoApi\Models\CustomerOrderShipmentItem;
+
+/**
+ * Decorates the GraphQL SerializerContextBuilder to fix attribute resolution
+ * for multi-word resource names (e.g., CompareItem).
+ *
+ * API Platform's default implementation uses the name converter to denormalize
+ * field names in the selection set (e.g., compareItem → compare_item), but then
+ * looks up the wrap field name without denormalization (using lcfirst(shortName)).
+ * This mismatch causes empty attributes for multi-word resource names.
+ * 
+ * Also ensures that nested relationships (like CustomerOrder.items) properly
+ * include all requested fields in the serialization context.
+ */
+class FixedSerializerContextBuilder implements SerializerContextBuilderInterface
+{
+    public function __construct(
+        private readonly SerializerContextBuilderInterface $decorated,
+        private readonly ?NameConverterInterface $nameConverter = null,
+    ) {}
+
+    public function create(?string $resourceClass, Operation $operation, array $resolverContext, bool $normalization): array
+    {
+        $context = $this->decorated->create($resourceClass, $operation, $resolverContext, $normalization);
+
+        // Ensure nested CustomerOrderItem fields are always included
+        // This handles the case where nested items don't trigger their own context builder call
+        if ($normalization && $resourceClass === CustomerOrder::class) {
+            // When processing the parent order, also ensure nested items have proper attributes
+            $attributes = $context['attributes'] ?? [];
+            
+            // Check if items field is being requested
+            if (isset($attributes['items']) && is_array($attributes['items'])) {
+                $this->ensureNestedItemsAttributes($attributes['items']);
+            }
+            
+            // Check if addresses field is being requested
+            if (isset($attributes['addresses']) && is_array($attributes['addresses'])) {
+                $this->ensureNestedAddressesAttributes($attributes['addresses']);
+            }
+            
+            // Check if shipments field is being requested
+            if (isset($attributes['shipments']) && is_array($attributes['shipments'])) {
+                $this->ensureNestedShipmentsAttributes($attributes['shipments']);
+            }
+        }
+        
+        // Ensure nested CustomerInvoiceItem fields are always included
+        if ($normalization && $resourceClass === CustomerInvoice::class) {
+            $attributes = $context['attributes'] ?? [];
+            
+            // Check if items field is being requested
+            if (isset($attributes['items']) && is_array($attributes['items'])) {
+                $this->ensureNestedInvoiceItemsAttributes($attributes['items']);
+            }
+            
+            // Check if addresses field is being requested
+            if (isset($attributes['addresses']) && is_array($attributes['addresses'])) {
+                $this->ensureNestedAddressesAttributes($attributes['addresses']);
+            }
+        }
+
+        // Handle direct nested CustomerOrderItem serialization
+        if ($normalization && $resourceClass === CustomerOrderItem::class) {
+            $context = $this->ensureCustomerOrderItemAttributes($context, $resolverContext);
+        }
+        
+        // Handle direct nested CustomerInvoiceItem serialization
+        if ($normalization && $resourceClass === CustomerInvoiceItem::class) {
+            $context = $this->ensureCustomerInvoiceItemAttributes($context, $resolverContext);
+        }
+        
+        // Handle direct nested CustomerInvoiceAddress serialization
+        if ($normalization && $resourceClass === CustomerInvoiceAddress::class) {
+            $context = $this->ensureAddressAttributes($context, $resolverContext);
+        }
+        
+        // Handle direct nested CustomerOrderShipment serialization
+        if ($normalization && $resourceClass === CustomerOrderShipment::class) {
+            $context = $this->ensureShipmentAttributes($context, $resolverContext);
+        }
+        
+        // Handle direct nested CustomerOrderShipmentItem serialization
+        if ($normalization && $resourceClass === CustomerOrderShipmentItem::class) {
+            $context = $this->ensureShipmentItemAttributes($context, $resolverContext);
+        }
+
+        if (! $normalization || ! ($operation instanceof Mutation)) {
+            return $context;
+        }
+
+        if (! empty($context['attributes'] ?? null)) {
+            return $context;
+        }
+
+        $wrapFieldName = lcfirst($operation->getShortName());
+
+        if ($this->nameConverter) {
+            $denormalizedName = $this->nameConverter->denormalize($wrapFieldName, $resourceClass);
+
+            if ($denormalizedName !== $wrapFieldName) {
+                $context['attributes'] = $this->rebuildMutationAttributes(
+                    $resourceClass,
+                    $operation,
+                    $resolverContext,
+                    $denormalizedName
+                );
+            }
+        }
+
+        return $context;
+    }
+
+    /**
+     * Ensure nested items' attributes include qty fields
+     */
+    private function ensureNestedItemsAttributes(array $itemsAttributes): void
+    {
+        // The itemsAttributes should contain edges > node structure for paginated results
+        // Recursively ensure qty fields are in node attributes
+        if (isset($itemsAttributes['edges']) && is_array($itemsAttributes['edges'])) {
+            if (isset($itemsAttributes['edges']['node']) && is_array($itemsAttributes['edges']['node'])) {
+                $this->addQtyFieldsToAttributes($itemsAttributes['edges']['node']);
+                // Also add database column names in case they're being used
+                $this->addQtyFieldsToAttributes($itemsAttributes['edges']);
+            }
+        }
+    }
+
+    /**
+     * Add qty fields to an attributes array
+     */
+    private function addQtyFieldsToAttributes(array &$attributes): void
+    {
+        $qtyFields = ['qty_ordered', 'qty_shipped', 'qty_invoiced', 'qty_canceled', 'qty_refunded'];
+        
+        foreach ($qtyFields as $field) {
+            $attributes[$field] = true;
+        }
+    }
+
+    /**
+     * Ensure nested invoice items' attributes include qty field
+     */
+    private function ensureNestedInvoiceItemsAttributes(array &$itemsAttributes): void
+    {
+        // The itemsAttributes should contain edges > node structure for paginated results
+        if (isset($itemsAttributes['edges']) && is_array($itemsAttributes['edges'])) {
+            if (isset($itemsAttributes['edges']['node']) && is_array($itemsAttributes['edges']['node'])) {
+                $this->addInvoiceItemFieldsToAttributes($itemsAttributes['edges']['node']);
+                $this->addInvoiceItemFieldsToAttributes($itemsAttributes['edges']);
+            }
+        }
+    }
+    
+    /**
+     * Ensure nested addresses' attributes are properly set
+     */
+    private function ensureNestedAddressesAttributes(array &$addressAttributes): void
+    {
+        // The addressAttributes should contain edges > node structure for paginated results
+        if (isset($addressAttributes['edges']) && is_array($addressAttributes['edges'])) {
+            if (isset($addressAttributes['edges']['node']) && is_array($addressAttributes['edges']['node'])) {
+                $this->addAddressFieldsToAttributes($addressAttributes['edges']['node']);
+                $this->addAddressFieldsToAttributes($addressAttributes['edges']);
+            }
+        }
+    }
+
+    /**
+     * Add invoice item fields to an attributes array
+     */
+    private function addInvoiceItemFieldsToAttributes(array &$attributes): void
+    {
+        $itemFields = ['id', 'qty', 'sku', 'name', 'price', 'base_price', 'total', 'base_total', 'tax_amount', 'discount_amount'];
+        
+        foreach ($itemFields as $field) {
+            $attributes[$field] = true;
+        }
+    }
+    
+    /**
+     * Add address fields to an attributes array
+     */
+    private function addAddressFieldsToAttributes(array &$attributes): void
+    {
+        // Include both snake_case and camelCase versions
+        $addressFields = [
+            'id', 
+            'name', 
+            'address', 
+            'city', 
+            'state', 
+            'postcode', 
+            'country_id',
+            'countryId',
+            'phone', 
+            'address_type',
+            'addressType',
+            'first_name',
+            'firstName',
+            'last_name',
+            'lastName',
+            'country',
+        ];
+        
+        foreach ($addressFields as $field) {
+            $attributes[$field] = true;
+        }
+    }
+
+    /**
+     * Ensure CustomerOrderItem attributes always include qty fields
+     */
+    private function ensureCustomerOrderItemAttributes(array $context, array $resolverContext): array
+    {
+        // Always ensure qty fields are in the attributes for item serialization
+        $attributes = $context['attributes'] ?? [];
+        
+        // Use snake_case field names to match the denormalization used by the serializer
+        $qtyFields = ['id', 'qty_ordered', 'qty_shipped', 'qty_invoiced', 'qty_canceled', 'qty_refunded', 'sku', 'name', 'price', 'base_price', 'total', 'base_total'];
+        
+        // Ensure attributes includes all qty fields
+        foreach ($qtyFields as $field) {
+            if (is_array($attributes)) {
+                if (array_key_exists('qty_ordered', $attributes) || !empty($attributes)) {
+                    $attributes[$field] = true;
+                } else {
+                    $attributes[] = $field;
+                }
+            } else {
+                if (!in_array($field, (array)$attributes)) {
+                    $attributes[] = $field;
+                }
+            }
+        }
+        
+        $context['attributes'] = $attributes;
+        
+        return $context;
+    }
+    
+    /**
+     * Ensure CustomerInvoiceItem attributes include qty field
+     */
+    private function ensureCustomerInvoiceItemAttributes(array $context, array $resolverContext): array
+    {
+        $attributes = $context['attributes'] ?? [];
+        
+        $fields = ['id', 'qty', 'sku', 'name', 'price', 'base_price', 'total', 'base_total', 'tax_amount', 'discount_amount'];
+        
+        foreach ($fields as $field) {
+            if (is_array($attributes)) {
+                $attributes[$field] = true;
+            }
+        }
+        
+        $context['attributes'] = $attributes;
+        
+        return $context;
+    }
+    
+    /**
+     * Ensure address attributes are properly set
+     */
+    private function ensureAddressAttributes(array $context, array $resolverContext): array
+    {
+        $attributes = $context['attributes'] ?? [];
+        
+        $fields = ['id', 'name', 'address', 'city', 'state', 'postcode', 'country_id', 'phone', 'address_type'];
+        
+        foreach ($fields as $field) {
+            if (is_array($attributes)) {
+                $attributes[$field] = true;
+            }
+        }
+        
+        $context['attributes'] = $attributes;
+        
+        return $context;
+    }
+    
+    /**
+     * Ensure nested shipments' attributes include items and addresses
+     */
+    private function ensureNestedShipmentsAttributes(array &$shipmentsAttributes): void
+    {
+        // The shipmentsAttributes should contain edges > node structure for paginated results
+        if (isset($shipmentsAttributes['edges']) && is_array($shipmentsAttributes['edges'])) {
+            if (isset($shipmentsAttributes['edges']['node']) && is_array($shipmentsAttributes['edges']['node'])) {
+                $this->addShipmentFieldsToAttributes($shipmentsAttributes['edges']['node']);
+                
+                // Also ensure nested fields within the node are properly handled
+                if (isset($shipmentsAttributes['edges']['node']['shippingAddress']) && is_array($shipmentsAttributes['edges']['node']['shippingAddress'])) {
+                    $this->addAddressFieldsToAttributes($shipmentsAttributes['edges']['node']['shippingAddress']);
+                }
+                if (isset($shipmentsAttributes['edges']['node']['items']) && is_array($shipmentsAttributes['edges']['node']['items'])) {
+                    if (isset($shipmentsAttributes['edges']['node']['items']['edges']) && is_array($shipmentsAttributes['edges']['node']['items']['edges'])) {
+                        if (isset($shipmentsAttributes['edges']['node']['items']['edges']['node']) && is_array($shipmentsAttributes['edges']['node']['items']['edges']['node'])) {
+                            $this->addShipmentItemFieldsToAttributes($shipmentsAttributes['edges']['node']['items']['edges']['node']);
+                        }
+                    }
+                }
+                
+                $this->addShipmentFieldsToAttributes($shipmentsAttributes['edges']);
+            }
+        }
+    }
+    
+    /**
+     * Add shipment fields to an attributes array
+     */
+    private function addShipmentFieldsToAttributes(array &$attributes): void
+    {
+        // Include both snake_case and camelCase versions to handle serializer name conversion
+        $shipmentFields = [
+            'id', 
+            'status', 
+            'total_qty', 
+            'totalQty',
+            'total_weight', 
+            'totalWeight',
+            'carrier_code', 
+            'carrierCode',
+            'carrier_title', 
+            'carrierTitle',
+            'track_number', 
+            'trackNumber',
+            'email_sent', 
+            'emailSent',
+            'shipping_number',
+            'shippingNumber',
+            'payment_method_title',
+            'paymentMethodTitle',
+            'shipping_method_title',
+            'shippingMethodTitle',
+            'created_at',
+            'createdAt',
+            'items',
+            'shipping_address',
+            'shippingAddress',
+            'billing_address',
+            'billingAddress',
+        ];
+        
+        foreach ($shipmentFields as $field) {
+            $attributes[$field] = true;
+        }
+    }
+    
+    /**
+     * Add shipment item fields to an attributes array
+     */
+    private function addShipmentItemFieldsToAttributes(array &$attributes): void
+    {
+        $itemFields = ['id', 'sku', 'name', 'qty', 'weight', 'description', 'order_item_id'];
+        
+        foreach ($itemFields as $field) {
+            $attributes[$field] = true;
+        }
+    }
+    
+    /**
+     * Ensure CustomerOrderShipment attributes are properly set
+     */
+    private function ensureShipmentAttributes(array $context, array $resolverContext): array
+    {
+        $attributes = $context['attributes'] ?? [];
+        
+        // Include both snake_case and camelCase versions
+        $fields = [
+            'id', 
+            'status', 
+            'total_qty', 
+            'totalQty',
+            'total_weight', 
+            'totalWeight',
+            'carrier_code', 
+            'carrierCode',
+            'carrier_title', 
+            'carrierTitle',
+            'track_number', 
+            'trackNumber',
+            'email_sent', 
+            'emailSent',
+            'shipping_number',
+            'shippingNumber',
+            'payment_method_title',
+            'paymentMethodTitle',
+            'shipping_method_title',
+            'shippingMethodTitle',
+            'created_at',
+            'createdAt',
+            'items', 
+            'shipping_address',
+            'shippingAddress',
+            'billing_address',
+            'billingAddress',
+        ];
+        
+        foreach ($fields as $field) {
+            if (is_array($attributes)) {
+                $attributes[$field] = true;
+            }
+        }
+        
+        $context['attributes'] = $attributes;
+        
+        return $context;
+    }
+    
+    /**
+     * Ensure CustomerOrderShipmentItem attributes are properly set
+     */
+    private function ensureShipmentItemAttributes(array $context, array $resolverContext): array
+    {
+        $attributes = $context['attributes'] ?? [];
+        
+        $fields = ['id', 'sku', 'name', 'qty', 'weight', 'description'];
+        
+        foreach ($fields as $field) {
+            if (is_array($attributes)) {
+                $attributes[$field] = true;
+            }
+        }
+        
+        $context['attributes'] = $attributes;
+        
+        return $context;
+    }
+
+    /**
+     * Rebuild mutation attributes using the denormalized wrap field name
+     *
+     * For Eloquent models, inner field names are denormalized (camelCase → snake_case)
+     * since Eloquent properties use snake_case. For non-Eloquent response models
+     * (DTOs, action responses), inner field names are kept as-is since PHP properties
+     * use camelCase.
+     */
+    private function rebuildMutationAttributes(
+        ?string $resourceClass,
+        Operation $operation,
+        array $resolverContext,
+        string $denormalizedWrapFieldName,
+    ): array {
+        if (isset($resolverContext['fields'])) {
+            $fields = $resolverContext['fields'];
+        } elseif (isset($resolverContext['info']) && $resolverContext['info'] instanceof ResolveInfo) {
+            $fields = $resolverContext['info']->getFieldSelection(\PHP_INT_MAX);
+        } else {
+            return [];
+        }
+
+        $isEloquent = $resourceClass && is_subclass_of($resourceClass, Model::class);
+
+        /** Denormalize top-level keys to locate the wrap field */
+        $topLevel = [];
+
+        foreach ($fields as $key => $value) {
+            $denormalizedKey = $this->nameConverter
+                ? $this->nameConverter->denormalize((string) $key, $resourceClass)
+                : $key;
+
+            $topLevel[$denormalizedKey] = $value;
+        }
+
+        $innerFields = $topLevel[$denormalizedWrapFieldName] ?? [];
+
+        if (! \is_array($innerFields)) {
+            return [];
+        }
+
+        /**
+         * For Eloquent models, denormalize inner field names (e.g., productId → product_id).
+         * For non-Eloquent models, keep camelCase field names as-is (they match PHP properties).
+         */
+        return $this->replaceIdKeys($innerFields, $resourceClass, $isEloquent);
+    }
+
+    /**
+     * Replace _id keys with id and optionally denormalize field names
+     */
+    private function replaceIdKeys(array $fields, ?string $resourceClass, bool $denormalizeKeys = true): array
+    {
+        $denormalizedFields = [];
+
+        foreach ($fields as $key => $value) {
+            if ('_id' === $key) {
+                $denormalizedFields['id'] = $fields['_id'];
+
+                continue;
+            }
+
+            $denormalizedKey = ($denormalizeKeys && $this->nameConverter)
+                ? $this->nameConverter->denormalize((string) $key, $resourceClass)
+                : $key;
+
+            $denormalizedFields[$denormalizedKey] = \is_array($value)
+                ? $this->replaceIdKeys($value, $resourceClass, $denormalizeKeys)
+                : $value;
+        }
+
+        return $denormalizedFields;
+    }
+}

+ 67 - 0
packages/Webkul/BagistoApi/src/GraphQl/StorefrontKeyGraphqlAuthenticator.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Webkul\BagistoApi\GraphQl;
+
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\Exception\BadRequestException;
+use Webkul\BagistoApi\Services\StorefrontKeyService;
+
+/**
+ * Authenticates GraphQL operations using X-STOREFRONT-KEY header
+ */
+class StorefrontKeyGraphqlAuthenticator
+{
+    public function __construct(
+        protected StorefrontKeyService $storefrontKeyService,
+        protected Request $request
+    ) {}
+
+    public function authenticate(): void
+    {
+        if ($this->isIntrospectionQuery($this->request)) {
+            return;
+        }
+
+        $key = $this->request->header('X-STOREFRONT-KEY');
+
+        if (! $key) {
+            throw new BadRequestException('X-STOREFRONT-KEY header is required');
+        }
+
+        $ipAddress = $this->request->ip();
+        $validation = $this->storefrontKeyService->validate($key, $ipAddress);
+
+        if (! $validation['valid']) {
+            throw new BadRequestException('Invalid storefront key');
+        }
+
+        $storefront = $validation['storefront'];
+        $rateLimit = $this->storefrontKeyService->checkRateLimit($storefront);
+
+        if (! $rateLimit['allowed']) {
+            throw new BadRequestException('Rate limit exceeded. Please retry after '.$rateLimit['reset_at'].' seconds');
+        }
+
+        $this->request->attributes->set('storefront_key', $storefront);
+        $this->request->attributes->set('rate_limit', $rateLimit);
+    }
+
+    protected function isIntrospectionQuery(Request $request): bool
+    {
+        $body = $request->getContent();
+
+        if (empty($body)) {
+            return false;
+        }
+
+        try {
+            $data = json_decode($body, true);
+            $query = $data['query'] ?? '';
+
+            return strpos($query, '__schema') !== false ||
+                   strpos($query, '__type') !== false;
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+}

+ 16 - 0
packages/Webkul/BagistoApi/src/GraphQl/TokenHeaderExtension.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Webkul\BagistoApi\GraphQl;
+
+use Illuminate\Http\Request;
+
+/**
+ * @deprecated Token extraction now happens in processors/providers only
+ */
+class TokenHeaderExtension
+{
+    public static function injectHeaderToken($data, ?Request $request = null): mixed
+    {
+        return $data;
+    }
+}

+ 105 - 0
packages/Webkul/BagistoApi/src/Helper/CustomerProfileHelper.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace Webkul\BagistoApi\Helper;
+
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\Facades\Storage;
+use Webkul\BagistoApi\Exception\InvalidInputException;
+use Webkul\BagistoApi\Dto\CustomerProfileOutput;
+use Webkul\BagistoApi\Models\CustomerProfile as CustomerProfileModel;
+use Webkul\Customer\Models\Customer;
+
+/**
+ * Maps Customer model to CustomerProfile API resource
+ */
+class CustomerProfileHelper
+{
+    public static function mapCustomerToProfile(Customer $customer): CustomerProfileModel
+    {
+        $imageUrl = null;
+        if ($customer->image) {
+            $imageUrl = Storage::url($customer->image);
+        }
+
+        $profile = new CustomerProfileModel;
+        $profile->id = (string) $customer->id;
+        $profile->first_name = (string) $customer->first_name;
+        $profile->last_name = (string) $customer->last_name;
+        $profile->email = (string) $customer->email;
+        $profile->phone = (string) $customer->phone;
+        $profile->gender = (string) $customer->gender;
+        $profile->date_of_birth = (string) $customer->date_of_birth;
+        $profile->status = (string) $customer->status;
+        $profile->subscribed_to_news_letter = (bool) $customer->subscribed_to_news_letter;
+        $profile->is_verified = (string) $customer->is_verified;
+        $profile->is_suspended = (string) $customer->is_suspended;
+        $profile->image = $imageUrl;
+
+        return $profile;
+    }
+
+    /**
+     * Map customer model to CustomerProfileOutput DTO
+     */
+    public static function mapCustomerToProfileOutput(Customer $customer): CustomerProfileOutput
+    {
+        $imageUrl = null;
+        if ($customer->image) {
+            $imageUrl = Storage::url($customer->image);
+        }
+
+        return new CustomerProfileOutput(
+            id: (string) $customer->id,
+            _id: (string) $customer->id,
+            firstName: $customer->first_name,
+            lastName: $customer->last_name,
+            email: $customer->email,
+            phone: $customer->phone,
+            gender: $customer->gender,
+            dateOfBirth: $customer->date_of_birth,
+            status: (string) $customer->status,
+            subscribedToNewsLetter: (bool) $customer->subscribed_to_news_letter,
+            isVerified: $customer->is_verified ? 'true' : 'false',
+            isSuspended: $customer->is_suspended ? 'true' : 'false',
+            image: $imageUrl,
+        );
+    }
+
+    public static function handleImageUpload(string $imageData, Customer $customer): void
+    {
+        try {
+            if (preg_match('/^data:image\/(\w+);base64,/', $imageData, $matches)) {
+                $imageFormat = $matches[1];
+                $base64Data = substr($imageData, strpos($imageData, ',') + 1);
+                $decodedData = base64_decode($base64Data, true);
+
+                if ($decodedData === false) {
+                    throw new InvalidInputException(__('bagistoapi::app.graphql.upload.invalid-base64'));
+                }
+
+                if (strlen($decodedData) > 5 * 1024 * 1024) {
+                    throw new InvalidInputException(__('bagistoapi::app.graphql.upload.size-exceeds-limit'));
+                }
+
+                $directory = 'customer/'.$customer->id;
+
+                if ($customer->image) {
+                    Storage::delete($customer->image);
+                }
+
+                $filename = $directory.'/'.uniqid().'.'.$imageFormat;
+
+                Storage::put($filename, $decodedData);
+
+                $customer->image = $filename;
+                $customer->save();
+
+                Event::dispatch('customer.image.upload.after', $customer);
+            } else {
+                throw new InvalidInputException(__('bagistoapi::app.graphql.upload.invalid-format'));
+            }
+        } catch (\Exception $e) {
+            throw new InvalidInputException($e->getMessage(), 0, $e);
+        }
+    }
+}

+ 166 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/AdminGraphQLPlaygroundController.php

@@ -0,0 +1,166 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Http\Response;
+use Illuminate\Routing\Controller;
+
+/**
+ * Admin GraphQL Playground UI with X-Admin-Key header support
+ */
+class AdminGraphQLPlaygroundController extends Controller
+{
+    public function __invoke()
+    {
+        $adminKey = env('ADMIN_PLAYGROUND_KEY') ?? 'ak_admin_live_xxxxx';
+
+        return new Response($this->getGraphQLPlaygroundHTML($adminKey), 200, [
+            'Content-Type' => 'text/html; charset=UTF-8',
+        ]);
+    }
+
+    private function getGraphQLPlaygroundHTML(string $adminKey): string
+    {
+        $serverUrl = config('app.url').'/graphql';
+
+        return <<<HTML
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset=utf-8/>
+    <meta name="viewport" content="width=device-width, initial-scale=1"/>
+    <title>GraphQL Playground - Admin API</title>
+    <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
+    <link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
+    <script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
+    <style>
+        body {
+            height: 100%;
+            margin: 0;
+            width: 100%;
+            overflow: hidden;
+        }
+        #root {
+            height: 100%;
+            width: 100%;
+            display: flex;
+            flex-direction: column;
+        }
+        .graphql-header {
+            background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+            color: white;
+            padding: 15px 20px;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+            flex-wrap: wrap;
+            gap: 10px;
+        }
+        .graphql-header h1 {
+            margin: 0;
+            font-size: 18px;
+            font-weight: 700;
+        }
+        .graphql-header .info {
+            font-size: 13px;
+            opacity: 0.9;
+        }
+        .key-input-container {
+            display: flex;
+            gap: 8px;
+            align-items: center;
+            background: rgba(255, 255, 255, 0.1);
+            padding: 8px 12px;
+            border-radius: 4px;
+        }
+        .key-input-container label {
+            font-size: 12px;
+            font-weight: 600;
+            white-space: nowrap;
+        }
+        .key-input-container input {
+            padding: 6px 8px;
+            border: none;
+            border-radius: 3px;
+            font-size: 12px;
+            min-width: 200px;
+            font-family: 'Monaco', 'Courier New', monospace;
+        }
+        .key-input-container input::placeholder {
+            color: #999;
+        }
+        .graphql-playground {
+            flex: 1;
+            overflow: hidden;
+        }
+        .key-indicator {
+            background: rgba(255, 255, 255, 0.2);
+            padding: 5px 10px;
+            border-radius: 3px;
+            font-size: 12px;
+            font-weight: 600;
+            white-space: nowrap;
+        }
+    </style>
+  </head>
+  <body>
+    <div id="root">
+        <div class="graphql-header">
+            <div>
+                <h1>🔑 Admin GraphQL Playground</h1>
+                <p class="info">Bagisto Admin API - Interactive GraphQL Explorer</p>
+            </div>
+            <div class="key-input-container">
+                <label for="admin-key">X-Admin-Key:</label>
+                <input 
+                    type="text" 
+                    id="admin-key" 
+                    placeholder="ak_admin_live_xxxxx"
+                    value="{$adminKey}"
+                />
+            </div>
+        </div>
+        <div class="graphql-playground" id="playground"></div>
+    </div>
+    <script>
+      window.addEventListener('load', function (event) {
+        GraphQLPlayground.init(document.getElementById('playground'), {
+          endpoint: window.location.href.split('?')[0].replace('/admin/graphiql', '/graphql'),
+          subscriptionEndpoint: window.location.protocol === 'wss:' ? 'wss:' : 'ws:' + '//' + window.location.host + '/graphql',
+          headers: {
+            'X-Admin-Key': document.getElementById('admin-key').value || '{$adminKey}'
+          },
+          settings: {
+            'request.credentials': 'include',
+            'prettier.useTabs': false,
+            'prettier.printWidth': 100,
+            'editor.fontSize': 13,
+            'editor.fontFamily': '"Consolas", "Inconsolata", "Droid Sans Mono", "Source Code Pro", monospace'
+          }
+        })
+      })
+
+      // Update headers when key input changes
+      document.getElementById('admin-key').addEventListener('change', function(e) {
+        const newKey = e.target.value || '{$adminKey}';
+        // Update the headers in GraphQL Playground
+        if (window.playground && window.playground.state) {
+          window.playground.state.headers = { 'X-Admin-Key': newKey };
+        }
+        // Store in localStorage for persistence
+        localStorage.setItem('bagisto-admin-key', newKey);
+      });
+
+      // Load persisted key from localStorage
+      const savedKey = localStorage.getItem('bagisto-admin-key');
+      if (savedKey) {
+        document.getElementById('admin-key').value = savedKey;
+      }
+    </script>
+  </body>
+</html>
+HTML;
+    }
+}

+ 40 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/ApiEntrypointController.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Http\Request;
+
+/**
+ * Custom API entrypoint providing Bagisto-branded documentation index
+ */
+class ApiEntrypointController
+{
+    public function __invoke(Request $request, ?string $index = null, ?string $_format = null)
+    {
+        if ($_format && $_format !== '') {
+            app()->make('api_platform.openapi.factory')->__invoke();
+        }
+
+        return view('webkul::api-platform.docs-index', [
+            'documentation_url' => 'https://api-docs.bagisto.com',
+            'rest_apis'         => [
+                [
+                    'name'        => 'Shop API',
+                    'description' => 'Customer-facing API for shop operations, products, cart management, and orders',
+                    'url'         => '/api/shop',
+                    'icon'        => 'shop-api.svg',
+                    'type'        => 'shop',
+                ],
+                [
+                    'name'        => 'Admin API',
+                    'description' => 'Administrator API for store management, products, orders, and configurations',
+                    'url'         => '/api/admin',
+                    'icon'        => 'admin-api.svg',
+                    'type'        => 'admin',
+                ],
+            ],
+            'graphql_url'            => '/graphql',
+            'graphql_playground_url' => '/graphiql',
+        ]);
+    }
+}

+ 151 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/AuthenticationController.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Tymon\JWTAuth\Exceptions\JWTException;
+use Tymon\JWTAuth\Facades\JWTAuth;
+
+/**
+ * Handles user authentication with JWT Bearer tokens
+ */
+class AuthenticationController extends Controller
+{
+    public function login(Request $request): JsonResponse
+    {
+        $credentials = $request->validate([
+            'email'    => 'required|email',
+            'password' => 'required|string|min:6',
+        ]);
+
+        try {
+
+            if (! $token = JWTAuth::attempt($credentials)) {
+                return response()->json([
+                    'message' => 'Invalid email or password',
+                    'error'   => 'invalid_credentials',
+                ], 401);
+            }
+
+            $user = JWTAuth::user();
+
+            return response()->json([
+                'message'    => 'Login successful',
+                'token'      => $token,
+                'token_type' => 'Bearer',
+                'expires_in' => auth()->factory()->getTTL() * 60, // in seconds
+                'user'       => [
+                    'id'    => $user->id,
+                    'email' => $user->email,
+                    'name'  => $user->name,
+                ],
+            ], 200);
+
+        } catch (JWTException $e) {
+            return response()->json([
+                'message' => 'Token creation failed',
+                'error'   => 'token_creation_failed',
+            ], 500);
+        }
+    }
+
+    public function register(Request $request): JsonResponse
+    {
+        $validated = $request->validate([
+            'name'     => 'required|string|max:255',
+            'email'    => 'required|email|unique:customers,email',
+            'password' => 'required|string|min:6|confirmed',
+        ]);
+
+        try {
+            $user = User::create([
+                'name'     => $validated['name'],
+                'email'    => $validated['email'],
+                'password' => bcrypt($validated['password']),
+            ]);
+
+            $token = JWTAuth::fromUser($user);
+
+            return response()->json([
+                'message'    => 'Registration successful',
+                'token'      => $token,
+                'token_type' => 'Bearer',
+                'expires_in' => auth()->factory()->getTTL() * 60,
+                'user'       => [
+                    'id'    => $user->id,
+                    'email' => $user->email,
+                    'name'  => $user->name,
+                ],
+            ], 201);
+
+        } catch (JWTException $e) {
+            return response()->json([
+                'message' => 'Registration failed',
+                'error'   => 'registration_failed',
+            ], 500);
+        }
+    }
+
+    public function refreshToken(Request $request): JsonResponse
+    {
+        try {
+            $token = JWTAuth::parseToken()->refresh();
+
+            return response()->json([
+                'message'    => 'Token refreshed',
+                'token'      => $token,
+                'token_type' => 'Bearer',
+                'expires_in' => auth()->factory()->getTTL() * 60,
+            ], 200);
+
+        } catch (JWTException $e) {
+            return response()->json([
+                'message' => 'Token refresh failed',
+                'error'   => 'token_refresh_failed',
+            ], 401);
+        }
+    }
+
+    public function logout(Request $request): JsonResponse
+    {
+        try {
+            JWTAuth::parseToken()->invalidate();
+
+            return response()->json([
+                'message' => 'Logout successful',
+            ], 200);
+
+        } catch (JWTException $e) {
+            return response()->json([
+                'message' => 'Logout failed',
+                'error'   => 'logout_failed',
+            ], 500);
+        }
+    }
+
+    /**
+     * Get current authenticated user
+     *
+     * GET /api/shop/me
+     *
+     * Header:
+     * Authorization: Bearer {token}
+     */
+    public function me(Request $request): JsonResponse
+    {
+        try {
+            $user = JWTAuth::parseToken()->authenticate();
+
+            return response()->json([
+                'user' => $user,
+            ], 200);
+
+        } catch (JWTException $e) {
+            return response()->json([
+                'message' => 'User not found',
+                'error'   => 'user_not_found',
+            ], 404);
+        }
+    }
+}

+ 53 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/DownloadableProductController.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Support\Facades\Storage;
+use Webkul\BagistoApi\Models\DownloadableProductDownloadLink;
+use Webkul\Sales\Repositories\DownloadableLinkPurchasedRepository;
+
+/**
+ * Handles secure file downloads for purchased downloadable products
+ */
+class DownloadableProductController
+{
+    public function __construct(
+        protected DownloadableLinkPurchasedRepository $downloadableLinkPurchasedRepository,
+    ) {}
+
+    public function download($token)
+    {
+        $downloadLink = DownloadableProductDownloadLink::where('token', $token)
+            ->where('expires_at', '>', now())
+            ->first();
+
+        if (! $downloadLink) {
+            abort(404, __('bagistoapi::downloadable-product.download-link-not-found'));
+        }
+
+        $downloadableLinkPurchased = $this->downloadableLinkPurchasedRepository->find(
+            $downloadLink->downloadable_link_purchased_id
+        );
+
+        if (! $downloadableLinkPurchased) {
+            abort(404, __('bagistoapi::downloadable-product.purchased-link-not-found'));
+        }
+
+        if ($downloadableLinkPurchased->type == 'file') {
+            $privateDisk = Storage::disk('private');
+
+            if (! $privateDisk->exists($downloadableLinkPurchased->file)) {
+                abort(404, __('bagistoapi::downloadable-product.file-not-found'));
+            }
+
+            $file = $privateDisk->get($downloadableLinkPurchased->file);
+            $fileName = basename($downloadableLinkPurchased->file);
+
+            return response($file, 200)
+                ->header('Content-Type', 'application/octet-stream')
+                ->header('Content-Disposition', 'attachment; filename="'.$fileName.'"');
+        } else {
+            return redirect()->away($downloadableLinkPurchased->url);
+        }
+    }
+}

+ 511 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/GraphQLPlaygroundController.php

@@ -0,0 +1,511 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Http\Response;
+use Illuminate\Routing\Controller;
+
+/**
+ * GraphQL Playground UI with X-STOREFRONT-KEY header support
+ */
+class GraphQLPlaygroundController extends Controller
+{
+    /**
+     * Display GraphQL Playground with Storefront Key input
+     */
+    public function __invoke()
+    {
+        $storefrontKey = env('STOREFRONT_PLAYGROUND_KEY', 'pk_storefront_xxxxx');
+        $autoInjectKey = filter_var(env('API_PLAYGROUND_AUTO_INJECT_STOREFRONT_KEY', 'true'), FILTER_VALIDATE_BOOLEAN);
+
+        return new Response($this->getGraphQLPlaygroundHTML($storefrontKey, $autoInjectKey), 200, [
+            'Content-Type' => 'text/html; charset=UTF-8',
+        ]);
+    }
+
+    /**
+     * Generate GraphQL Playground HTML with custom styling and header injection
+     *
+     * @param  string  $storefrontKey  The storefront API key to use
+     * @param  bool  $autoInjectKey  Whether to auto-inject the key in headers (controlled by API_AUTO_INJECT_STOREFRONT_KEY env)
+     */
+    private function getGraphQLPlaygroundHTML(string $storefrontKey, bool $autoInjectKey = false): string
+    {
+        $graphiqlData = json_encode([
+            'entrypoint'    => '/api/graphql',
+            'apiKey'        => $storefrontKey,
+            'autoInjectKey' => $autoInjectKey,
+        ]);
+
+        $html = <<<'HTML'
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>GraphQL - API Platform</title>
+    <link rel="stylesheet" href="/vendor/api-platform/graphiql/graphiql.css">
+    <link rel="stylesheet" href="/vendor/api-platform/graphiql-style.css">
+    <script id="graphiql-data" type="application/json">GRAPHIQL_DATA_PLACEHOLDER</script>
+    <style>
+        body { margin: 0; padding: 0; }
+        #graphiql { height: calc(100vh - 36px); }
+
+        /* Auth status bar */
+        #auth-top-bar {
+            height: 36px;
+            display: flex;
+            align-items: center;
+            padding: 0 14px;
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            font-size: 13px;
+            font-weight: 600;
+            box-sizing: border-box;
+            gap: 10px;
+            transition: background 0.2s;
+        }
+        #auth-top-bar.bar-none {
+            background: #fff3cd;
+            border-bottom: 1px solid #ffc107;
+            color: #856404;
+        }
+        #auth-top-bar.bar-customer {
+            background: #d4edda;
+            border-bottom: 1px solid #28a745;
+            color: #155724;
+        }
+        #auth-top-bar.bar-guest {
+            background: #d1ecf1;
+            border-bottom: 1px solid #17a2b8;
+            color: #0c5460;
+        }
+        #auth-top-bar .bar-msg {
+            flex: 1;
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            font-size: 13px;
+            line-height: 1;
+        }
+        #auth-top-bar .bar-token {
+            font-family: 'SFMono-Regular', Consolas, monospace;
+            font-size: 11px;
+            font-weight: 400;
+            opacity: 0.85;
+        }
+        #auth-top-bar .bar-actions {
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            flex-shrink: 0;
+        }
+        #auth-top-bar button {
+            padding: 3px 10px;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 11px;
+            font-weight: 600;
+            transition: opacity 0.15s;
+            line-height: 1.4;
+        }
+        #auth-top-bar button:hover { opacity: 0.85; }
+        .bar-btn-clear { background: rgba(0,0,0,0.15); color: inherit; }
+        .bar-btn-manual { background: rgba(0,0,0,0.1); color: inherit; }
+        .bar-btn-apply { background: #0d6efd; color: #fff; }
+        .bar-manual-input {
+            font-family: 'SFMono-Regular', Consolas, monospace;
+            font-size: 11px;
+            padding: 3px 8px;
+            border: 1px solid rgba(0,0,0,0.2);
+            border-radius: 4px;
+            outline: none;
+            background: rgba(255,255,255,0.7);
+            color: #333;
+            width: 280px;
+        }
+        .bar-manual-input:focus {
+            border-color: #80bdff;
+            background: #fff;
+        }
+    </style>
+</head>
+<body>
+<div id="auth-top-bar"></div>
+<div id="graphiql">Loading...</div>
+<script src="/vendor/api-platform/react/react.production.min.js"></script>
+<script src="/vendor/api-platform/react/react-dom.production.min.js"></script>
+<script src="/vendor/api-platform/graphiql/graphiql.min.js"></script>
+<script>
+/* ═══════════════════════════════════════════════════════════
+   Token Encryption — AES-GCM via Web Crypto API
+   ═══════════════════════════════════════════════════════════ */
+var CRYPTO_KEY = null;
+
+/** Derive a stable encryption key from the storefront API key using PBKDF2 → AES-GCM */
+async function initCryptoKey(passphrase) {
+    var enc = new TextEncoder();
+    var keyMaterial = await crypto.subtle.importKey(
+        'raw', enc.encode(passphrase), 'PBKDF2', false, ['deriveKey']
+    );
+    CRYPTO_KEY = await crypto.subtle.deriveKey(
+        { name: 'PBKDF2', salt: enc.encode('bagisto-graphiql-v1'), iterations: 100000, hash: 'SHA-256' },
+        keyMaterial,
+        { name: 'AES-GCM', length: 256 },
+        false,
+        ['encrypt', 'decrypt']
+    );
+}
+
+/** Encrypt a plaintext token → base64 string stored in sessionStorage */
+async function encryptToken(plaintext) {
+    if (!CRYPTO_KEY || !plaintext) return plaintext;
+    try {
+        var enc = new TextEncoder();
+        var iv = crypto.getRandomValues(new Uint8Array(12));
+        var ciphertext = await crypto.subtle.encrypt(
+            { name: 'AES-GCM', iv: iv },
+            CRYPTO_KEY,
+            enc.encode(plaintext)
+        );
+        /* Prepend IV (12 bytes) to ciphertext */
+        var combined = new Uint8Array(iv.length + ciphertext.byteLength);
+        combined.set(iv);
+        combined.set(new Uint8Array(ciphertext), iv.length);
+        return 'enc:' + btoa(String.fromCharCode.apply(null, combined));
+    } catch (e) {
+        return plaintext; /* Fallback to plaintext if encryption fails */
+    }
+}
+
+/** Decrypt a stored value back to plaintext */
+async function decryptToken(stored) {
+    if (!stored) return null;
+    if (!CRYPTO_KEY || !stored.startsWith('enc:')) return stored;
+    try {
+        var raw = atob(stored.substring(4));
+        var bytes = new Uint8Array(raw.length);
+        for (var i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
+        var iv = bytes.slice(0, 12);
+        var ciphertext = bytes.slice(12);
+        var decrypted = await crypto.subtle.decrypt(
+            { name: 'AES-GCM', iv: iv },
+            CRYPTO_KEY,
+            ciphertext
+        );
+        return new TextDecoder().decode(decrypted);
+    } catch (e) {
+        return null; /* Corrupted — return null so it gets cleared */
+    }
+}
+
+/* ═══════════════════════════════════════════════════════════
+   Token Storage (encrypted in localStorage)
+   ═══════════════════════════════════════════════════════════ */
+var AUTH_TOKEN_KEY = 'bagisto-graphiql-auth-token';
+var CART_TOKEN_KEY = 'bagisto-graphiql-cart-token';
+
+/* In-memory plaintext cache to avoid async decrypt on every fetch */
+var _cachedAuthToken = null;
+var _cachedCartToken = null;
+
+function getStoredToken() { return _cachedAuthToken; }
+function getStoredCartToken() { return _cachedCartToken; }
+function getActiveToken() { return _cachedAuthToken || _cachedCartToken || null; }
+
+async function storeToken(token) {
+    _cachedAuthToken = token;
+    _cachedCartToken = null;
+    localStorage.setItem(AUTH_TOKEN_KEY, await encryptToken(token));
+    localStorage.removeItem(CART_TOKEN_KEY);
+    refreshUI();
+}
+
+async function storeCartToken(token) {
+    if (_cachedAuthToken) return; /* Customer auth takes priority */
+    _cachedCartToken = token;
+    localStorage.setItem(CART_TOKEN_KEY, await encryptToken(token));
+    refreshUI();
+}
+
+function clearAuthToken() {
+    _cachedAuthToken = null;
+    localStorage.removeItem(AUTH_TOKEN_KEY);
+    refreshUI();
+}
+
+function clearCartToken() {
+    _cachedCartToken = null;
+    localStorage.removeItem(CART_TOKEN_KEY);
+    refreshUI();
+}
+
+/** Restore cached tokens from encrypted localStorage on page load */
+async function restoreTokens() {
+    _cachedAuthToken = await decryptToken(localStorage.getItem(AUTH_TOKEN_KEY));
+    _cachedCartToken = await decryptToken(localStorage.getItem(CART_TOKEN_KEY));
+    /* Clear corrupted entries */
+    if (localStorage.getItem(AUTH_TOKEN_KEY) && !_cachedAuthToken) localStorage.removeItem(AUTH_TOKEN_KEY);
+    if (localStorage.getItem(CART_TOKEN_KEY) && !_cachedCartToken) localStorage.removeItem(CART_TOKEN_KEY);
+}
+
+function refreshUI() {
+    updateToolbarButton();
+    syncHeadersEditor();
+}
+
+/* ═══════════════════════════════════════════════════════════
+   Helpers
+   ═══════════════════════════════════════════════════════════ */
+function maskToken(token) {
+    if (!token) return '';
+    return token.length > 20
+        ? token.substring(0, 10) + '\u2022\u2022\u2022' + token.substring(token.length - 4)
+        : token;
+}
+
+function syncHeadersEditor() {
+    var headersObj = { 'X-STOREFRONT-KEY': defaultApiKey };
+    var token = getActiveToken();
+    if (token) headersObj['Authorization'] = 'Bearer ' + token;
+    var headersJson = JSON.stringify(headersObj, null, 2);
+    setTimeout(function() {
+        var editors = document.querySelectorAll('.graphiql-editor-tool .CodeMirror');
+        if (editors.length >= 2) {
+            var cm = editors[1].CodeMirror;
+            if (cm) cm.setValue(headersJson);
+        }
+    }, 100);
+}
+
+function deepFind(obj, field, maxDepth) {
+    if (!obj || typeof obj !== 'object' || maxDepth <= 0) return null;
+    if (obj.hasOwnProperty(field) && typeof obj[field] === 'string' && obj[field].length > 0) return obj[field];
+    var keys = Object.keys(obj);
+    for (var i = 0; i < keys.length; i++) {
+        var val = obj[keys[i]];
+        if (val && typeof val === 'object') {
+            var found = deepFind(val, field, maxDepth - 1);
+            if (found) return found;
+        }
+    }
+    return null;
+}
+
+function interceptAuthResponse(result) {
+    if (!result || !result.data) return;
+    var loginData = result.data.createCustomerLogin;
+    if (loginData) {
+        var inner = loginData.customerLogin || loginData;
+        if (inner && inner.success === true && inner.token) { storeToken(inner.token); return; }
+    }
+    var logoutData = result.data.createLogout;
+    if (logoutData) {
+        var logoutInner = logoutData.logout || logoutData;
+        if (logoutInner && logoutInner.success === true) { clearAuthToken(); return; }
+    }
+    var cartToken = deepFind(result.data, 'cartToken', 3);
+    if (cartToken && typeof cartToken === 'string' && cartToken.length > 0) storeCartToken(cartToken);
+}
+
+/* ═══════════════════════════════════════════════════════════
+   GraphQL Fetcher
+   ═══════════════════════════════════════════════════════════ */
+var initParameters = {};
+var entrypoint = null;
+var defaultApiKey = null;
+var autoInjectStorefrontKey = false;
+
+function onEditQuery(q) { initParameters.query = q; updateURL(); }
+function onEditVariables(v) { initParameters.variables = v; updateURL(); }
+function onEditOperationName(n) { initParameters.operationName = n; updateURL(); }
+
+function updateURL() {
+    var s = '?' + Object.keys(initParameters).filter(function(k){ return Boolean(initParameters[k]); })
+        .map(function(k){ return encodeURIComponent(k) + '=' + encodeURIComponent(initParameters[k]); }).join('&');
+    history.replaceState(null, null, s);
+}
+
+function graphQLFetcher(graphQLParams, opts) {
+    var headers = (opts && opts.headers) ? opts.headers : {};
+    var token = getActiveToken();
+    if (token && !headers['Authorization'] && !headers['authorization']) {
+        headers['Authorization'] = 'Bearer ' + token;
+    }
+
+    /**
+     * Fix "Unknown operation named" error when switching tabs.
+     * GraphiQL may pass an operationName from a previous tab that
+     * doesn't exist in the current query. Strip it if not found.
+     */
+    var params = Object.assign({}, graphQLParams);
+    if (params.operationName && params.query) {
+        var escaped = params.operationName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+        var opNamePattern = new RegExp('(query|mutation|subscription)\\s+' + escaped + '\\b');
+        if (!opNamePattern.test(params.query)) {
+            delete params.operationName;
+        }
+    }
+
+    return fetch(entrypoint, {
+        method: 'post',
+        headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...headers },
+        body: JSON.stringify(params),
+        credentials: 'include'
+    }).then(function(r){ return r.text(); })
+    .then(function(body){
+        try { var result = JSON.parse(body); interceptAuthResponse(result); return result; }
+        catch(e) { return body; }
+    });
+}
+
+/* ═══════════════════════════════════════════════════════════
+   Auth Status Bar (React component)
+   ═══════════════════════════════════════════════════════════ */
+var _authBarForceUpdate = null;
+var _showManualInput = false;
+
+function updateToolbarButton() {
+    if (_authBarForceUpdate) _authBarForceUpdate();
+}
+
+function AuthStatusBar() {
+    var stateRef = React.useState(0);
+    var forceUpdate = function() { stateRef[1](function(n){ return n + 1; }); };
+    React.useEffect(function() { _authBarForceUpdate = forceUpdate; return function() { _authBarForceUpdate = null; }; }, []);
+
+    var authToken = getStoredToken();
+    var cartToken = getStoredCartToken();
+    var hasAuth = !!authToken;
+    var hasCart = !hasAuth && !!cartToken;
+
+    /* Set bar class for color */
+    var barClass = hasAuth ? 'bar-customer' : (hasCart ? 'bar-guest' : 'bar-none');
+
+    /* Build message */
+    var msgParts = [];
+    if (hasAuth) {
+        msgParts.push('\uD83D\uDD12 Customer authenticated');
+        msgParts.push(React.createElement('span', { key: 't', className: 'bar-token' }, '\u2014 Bearer ' + maskToken(authToken)));
+    } else if (hasCart) {
+        msgParts.push('\uD83D\uDED2 Guest cart token active');
+        msgParts.push(React.createElement('span', { key: 't', className: 'bar-token' }, '\u2014 Bearer ' + maskToken(cartToken)));
+    } else {
+        msgParts.push('\uD83D\uDD13 No auth token \u2014 run createCustomerLogin or createCartToken mutation');
+    }
+
+    function handleManualApply() {
+        var input = document.getElementById('manual-token-input');
+        if (!input || !input.value.trim()) return;
+        var val = input.value.trim();
+        if (/^\d+\|/.test(val)) { storeToken(val); } else { storeCartToken(val); }
+        _showManualInput = false;
+        forceUpdate();
+    }
+
+    function toggleManual() {
+        _showManualInput = !_showManualInput;
+        forceUpdate();
+        if (_showManualInput) {
+            setTimeout(function() { var el = document.getElementById('manual-token-input'); if (el) el.focus(); }, 50);
+        }
+    }
+
+    /* Action buttons */
+    var actions = [];
+    if (_showManualInput) {
+        actions.push(
+            React.createElement('input', {
+                key: 'inp',
+                id: 'manual-token-input',
+                className: 'bar-manual-input',
+                type: 'text',
+                placeholder: 'Paste token (123|abc... or UUID)...',
+                onKeyDown: function(e) { if (e.key === 'Enter') handleManualApply(); if (e.key === 'Escape') toggleManual(); }
+            }),
+            React.createElement('button', { key: 'ap', className: 'bar-btn-apply', onClick: handleManualApply }, 'Apply'),
+            React.createElement('button', { key: 'cn', className: 'bar-btn-manual', onClick: toggleManual }, 'Cancel')
+        );
+    } else {
+        actions.push(
+            React.createElement('button', { key: 'me', className: 'bar-btn-manual', onClick: toggleManual }, 'Manual Entry')
+        );
+        if (hasAuth) actions.push(
+            React.createElement('button', { key: 'ca', className: 'bar-btn-clear', onClick: clearAuthToken }, 'Clear')
+        );
+        if (hasCart) actions.push(
+            React.createElement('button', { key: 'cc', className: 'bar-btn-clear', onClick: clearCartToken }, 'Clear')
+        );
+    }
+
+    /* Update bar element class directly for color */
+    React.useEffect(function() {
+        var bar = document.getElementById('auth-top-bar');
+        if (bar) { bar.className = barClass; }
+    });
+
+    return React.createElement(React.Fragment, null,
+        React.createElement('div', { className: 'bar-msg' }, msgParts),
+        React.createElement('div', { className: 'bar-actions' }, actions)
+    );
+}
+
+/* ═══════════════════════════════════════════════════════════
+   Init
+   ═══════════════════════════════════════════════════════════ */
+window.onload = async function() {
+    var data = JSON.parse(document.getElementById('graphiql-data').innerText);
+    entrypoint = data.entrypoint;
+    defaultApiKey = data.apiKey;
+    autoInjectStorefrontKey = data.autoInjectKey === true || data.autoInjectKey === 'true';
+
+    /* Initialize encryption key from the storefront API key */
+    await initCryptoKey(defaultApiKey + '-playground-secret');
+    await restoreTokens();
+
+    var search = window.location.search;
+    search.substr(1).split('&').forEach(function(entry) {
+        var eq = entry.indexOf('=');
+        if (eq >= 0) initParameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent(entry.slice(eq + 1));
+    });
+
+    if (initParameters.variables) {
+        try { initParameters.variables = JSON.stringify(JSON.parse(initParameters.variables), null, 2); }
+        catch(e) {}
+    }
+
+    var headersObj = { 'X-STOREFRONT-KEY': defaultApiKey };
+    var existingToken = getActiveToken();
+    if (existingToken) headersObj['Authorization'] = 'Bearer ' + existingToken;
+    var defaultHeaders = JSON.stringify(headersObj, null, 2);
+
+    var renderProps = {
+        fetcher: graphQLFetcher,
+        query: initParameters.query,
+        variables: initParameters.variables,
+        operationName: initParameters.operationName,
+        onEditQuery: onEditQuery,
+        onEditVariables: onEditVariables,
+        onEditOperationName: onEditOperationName
+    };
+
+    if (autoInjectStorefrontKey) renderProps.defaultHeaders = defaultHeaders;
+
+    /* Render auth status bar above GraphiQL */
+    ReactDOM.render(
+        React.createElement(AuthStatusBar, null),
+        document.getElementById('auth-top-bar')
+    );
+
+    ReactDOM.render(
+        React.createElement(GraphiQL, renderProps),
+        document.getElementById('graphiql')
+    );
+}
+</script>
+</body>
+</html>
+HTML;
+
+        return str_replace('GRAPHIQL_DATA_PLACEHOLDER', $graphiqlData, $html);
+    }
+}

+ 59 - 0
packages/Webkul/BagistoApi/src/Http/Controllers/InvoicePdfController.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Http\Response;
+use Illuminate\Routing\Controller;
+use Illuminate\Support\Facades\Auth;
+use Webkul\BagistoApi\Exception\AuthorizationException;
+use Webkul\BagistoApi\Exception\ResourceNotFoundException;
+use Webkul\Core\Traits\PDFHandler;
+use Webkul\Customer\Models\Customer;
+use Webkul\Sales\Models\Invoice;
+
+/**
+ * Invoice PDF Download Controller
+ *
+ * Generates and streams invoice PDF for authenticated customers.
+ * Scopes access through order → customer relationship.
+ */
+class InvoicePdfController extends Controller
+{
+    use PDFHandler;
+
+    /**
+     * Download invoice as PDF
+     *
+     * @param  int  $id  Invoice ID
+     * @return \Illuminate\Http\Response
+     */
+    public function __invoke(int $id)
+    {
+        $customer = Auth::guard('sanctum')->user();
+
+        if (! $customer) {
+            throw new AuthorizationException(__('bagistoapi::app.graphql.logout.unauthenticated'));
+        }
+
+        $invoice = Invoice::where('id', $id)
+            ->whereHas('order', function ($query) use ($customer) {
+                $query->where('customer_id', $customer->id)
+                    ->where('customer_type', Customer::class);
+            })
+            ->with(['order', 'items'])
+            ->first();
+
+        if (! $invoice) {
+            throw new ResourceNotFoundException(
+                __('bagistoapi::app.graphql.customer-invoice.not-found', ['id' => $id])
+            );
+        }
+
+        $orderCurrencyCode = $invoice->order->order_currency_code;
+
+        return $this->downloadPDF(
+            view('shop::customers.account.orders.pdf', compact('invoice', 'orderCurrencyCode'))->render(),
+            'invoice-'.$invoice->created_at->format('d-m-Y')
+        );
+    }
+}

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

@@ -0,0 +1,149 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Controllers;
+
+use Illuminate\Routing\Controller;
+use Illuminate\View\View;
+
+/**
+ * Customizes Swagger UI for Shop and Admin APIs
+ */
+class SwaggerUIController extends Controller
+{
+    /**
+     * Display Shop API Swagger UI
+     * Shows all /api/shop routes and customer-facing endpoints
+     * Embeds the OpenAPI spec directly to avoid self-referential requests
+     */
+    public function shopApi(): View
+    {
+        $specData = $this->getOpenApiSpec('shop');
+
+        return view('webkul::api-platform.swagger-ui-embedded', [
+            'title'         => 'Bagisto Shop API',
+            'description'   => 'Customer-facing API for shop operations',
+            'specData'      => $specData,
+            'endpoint'      => 'shop',
+            'defaultServer' => '/api/shop',
+        ]);
+    }
+
+    /**
+     * Display Admin API Swagger UI
+     * Shows all /api/admin routes and administrative endpoints
+     * Embeds the OpenAPI spec directly to avoid self-referential requests
+     */
+    public function adminApi(): View
+    {
+        $specData = $this->getOpenApiSpec('admin');
+
+        return view('webkul::api-platform.swagger-ui-embedded', [
+            'title'         => 'Bagisto Admin API',
+            'description'   => 'Administrative API for platform management',
+            'specData'      => $specData,
+            'endpoint'      => 'admin',
+            'defaultServer' => '/api/admin',
+        ]);
+    }
+
+    /**
+     * Get Shop API OpenAPI Specification (JSON)
+     * Returns filtered OpenAPI spec for shop endpoints only
+     */
+    public function shopApiDocs()
+    {
+        $specData = $this->getOpenApiSpec('shop');
+
+        return response()->json($specData, 200, [], JSON_UNESCAPED_SLASHES);
+    }
+
+    /**
+     * Get Admin API OpenAPI Specification (JSON)
+     * Returns filtered OpenAPI spec for admin endpoints only
+     */
+    public function adminApiDocs()
+    {
+        $specData = $this->getOpenApiSpec('admin');
+
+        return response()->json($specData, 200, [], JSON_UNESCAPED_SLASHES);
+    }
+
+    /**
+     * Display API Documentation Index
+     * Shows links to Shop and Admin API documentation
+     */
+    public function index(): View
+    {
+        return view('webkul::api-platform.docs-index', [
+            'apis' => [
+                [
+                    'name'        => 'Shop API',
+                    'description' => 'Customer-facing API for shop operations',
+                    'url'         => url('/api/shop'),
+                    'icon'        => '🛍️',
+                ],
+                [
+                    'name'        => 'Admin API',
+                    'description' => 'Administrative API for platform management',
+                    'url'         => url('/api/admin'),
+                    'icon'        => '⚙️',
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * Retrieve OpenAPI specification array
+     * Returns the spec as a PHP array (not JSON) for embedding in Swagger UI
+     */
+    private function getOpenApiSpec(string $endpoint): array
+    {
+        try {
+            // Get the already-registered OpenAPI Factory from the service container
+            // It's already a SplitOpenApiFactory, so we just need to call it with context
+            $factory = app(\ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface::class);
+
+            if (! $factory) {
+                throw new \Exception('OpenAPI Factory could not be instantiated');
+            }
+
+            // Create context with endpoint information
+            // The SplitOpenApiFactory will use this to determine which endpoint to filter for
+            $context = [
+                'endpoint' => $endpoint,
+                'base_url' => request()->getBaseUrl(),
+                'request'  => request(),
+            ];
+
+            // Generate the OpenAPI spec using the factory
+            // The SplitOpenApiFactory will handle filtering based on endpoint
+            $openApi = $factory($context);
+
+            // Use our custom serializer to properly convert the OpenAPI object to an array
+            // This handles all the nested objects and complex types properly
+            $array = \Webkul\BagistoApi\Services\OpenApiSerializer::toArray($openApi);
+
+            return $array ?: [];
+
+        } catch (\Exception $e) {
+            \Log::error('OpenAPI Spec Generation Error: '.$e->getMessage(), [
+                'exception' => $e,
+                'endpoint'  => $endpoint,
+                'trace'     => $e->getTraceAsString(),
+            ]);
+
+            return [
+                'openapi' => '3.0.0',
+                'info'    => [
+                    'title'       => 'Error',
+                    'version'     => '1.0.0',
+                    'description' => 'Failed to generate OpenAPI specification: '.$e->getMessage(),
+                ],
+                'paths'      => [],
+                'components' => [
+                    'schemas' => [],
+                ],
+            ];
+        }
+    }
+}

+ 41 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/BagistoApiDocumentationMiddleware.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Intercepts requests for API documentation and returns custom Swagger UI
+ */
+class BagistoApiDocumentationMiddleware
+{
+    public function handle(Request $request, Closure $next): Response
+    {
+        $path = $request->getPathInfo();
+
+        if ($path === '/api') {
+            return response()->view('webkul::api-platform.docs-index', [
+                'documentation_url'      => 'https://api-docs.bagisto.com',
+                'graphql_playground_url' => config('app.url').'/api/graphql',
+                'rest_apis'              => [
+                    [
+                        'name'        => 'Shop API',
+                        'description' => 'Customer-facing API for shop operations, products, cart management, and orders',
+                        'url'         => '/api/shop',
+                        'icon'        => 'shop-api.svg',
+                    ],
+                    [
+                        'name'        => 'Admin API',
+                        'description' => 'Administrator API for store management, products, orders, and configurations',
+                        'url'         => '/api/admin',
+                        'icon'        => 'admin-api.svg',
+                    ],
+                ],
+            ])->header('Content-Type', 'text/html; charset=UTF-8');
+        }
+
+        return $next($request);
+    }
+}

+ 70 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/ForceApiJson.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * ForceApiJson Middleware
+ *
+ * Ensures API responses return JSON content-type instead of HTML.
+ * Works in conjunction with ApiAwareResponseCache profile:
+ * - Sets Accept header to application/json if not present
+ * - Ensures responses have correct JSON content-type
+ * - Prevents HTML from being cached for API responses
+ * - Shop pages (HTML) are still cached for speed
+ */
+class ForceApiJson
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
+     */
+    public function handle(Request $request, Closure $next): Response
+    {
+        // Skip entirely for documentation/playground pages that return HTML
+        if ($this->isDocumentationRoute($request)) {
+            return $next($request);
+        }
+
+        // If no Accept header is set, default to JSON for API requests
+        if (! $request->header('Accept') && $request->is('api/*', 'graphql*')) {
+            $request->headers->set('Accept', 'application/json');
+        }
+
+        $response = $next($request);
+
+        // Ensure API responses are JSON (for API Platform routes)
+        if ($request->is('api/*', 'graphql*')) {
+            if (! $response->headers->has('Content-Type') ||
+                strpos($response->headers->get('Content-Type'), 'text/html') !== false) {
+                $response->headers->set('Content-Type', 'application/json; charset=utf-8');
+            }
+        }
+
+        return $response;
+    }
+
+    /**
+     * Check if the request is for an API documentation/playground route that serves HTML.
+     */
+    private function isDocumentationRoute(Request $request): bool
+    {
+        if ($request->method() !== 'GET') {
+            return false;
+        }
+
+        $path = $request->path();
+
+        return in_array($path, [
+            'api/graphiql',
+            'api/graphql',
+            'api',
+            'api/shop',
+            'api/admin',
+        ]);
+    }
+}

+ 42 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/HandleInvalidInputException.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+use Symfony\Component\HttpFoundation\Response;
+use Webkul\BagistoApi\Exception\InvalidInputException;
+
+/**
+ * Handle InvalidInputException for REST API
+ * Converts validation errors to proper RFC 7807 format
+ */
+class HandleInvalidInputException
+{
+    public function handle(Request $request, Closure $next): Response
+    {
+        try {
+            Log::info('HandleInvalidInputException middleware invoked', [
+                'path' => $request->path(),
+            ]);
+
+            return $next($request);
+        } catch (InvalidInputException $e) {
+            Log::info('InvalidInputException caught in middleware', [
+                'message' => $e->getMessage(),
+            ]);
+            // Return proper API error response for REST APIs
+            if ($request->wantsJson() || $request->is('api/*')) {
+                return response()->json([
+                    'type'   => $e->getType(),
+                    'title'  => $e->getTitle(),
+                    'status' => $e->getStatus(),
+                    'detail' => $e->getDetail(),
+                ], $e->getStatusCode(), [], JSON_UNESCAPED_SLASHES);
+            }
+
+            throw $e;
+        }
+    }
+}

+ 139 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/LogApiRequests.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+use Webkul\BagistoApi\Jobs\LogApiRequestJob;
+
+/**
+ * Logs all API requests for security audit trail and monitoring
+ */
+class LogApiRequests
+{
+    /**
+     * Paths to exclude from logging
+     */
+    private const EXCLUDED_PATHS = [
+        'health',
+        'ping',
+        'docs',
+        'graphiql',
+        'swagger-ui',
+        'docs.json',
+        '.well-known',
+    ];
+
+    public function handle(Request $request, Closure $next)
+    {
+        $startTime = microtime(true);
+        $response = $next($request);
+        $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+        if ($this->shouldLog($request, $response)) {
+            $this->logRequestAsync($request, $response, $duration);
+        }
+
+        return $response;
+    }
+
+    private function shouldLog(Request $request, $response): bool
+    {
+        foreach (self::EXCLUDED_PATHS as $path) {
+            if (str_contains($request->path(), $path)) {
+                return false;
+            }
+        }
+
+        return str_starts_with($request->path(), 'api/');
+    }
+
+    private function logRequestAsync(Request $request, $response, float $duration): void
+    {
+        $logData = [
+            'method'            => $request->getMethod(),
+            'path'              => $request->path(),
+            'status'            => $response->getStatusCode(),
+            'duration_ms'       => $duration,
+            'ip'                => $request->ip(),
+            'user_agent'        => substr($request->userAgent() ?? '', 0, 255),
+            'user_id'           => auth()->id(),
+            'api_key'           => $this->maskApiKey($request->header('X-STOREFRONT-KEY')),
+            'graphql_operation' => $this->getGraphQLOperation($request),
+        ];
+
+        try {
+            // Skip async logging in testing environment to avoid job queue issues
+            if (app()->environment('testing')) {
+                $this->logSync($logData);
+            } elseif (class_exists(LogApiRequestJob::class)) {
+                dispatch(new LogApiRequestJob($logData))->onQueue('api-logs');
+            } else {
+                $this->logSync($logData);
+            }
+        } catch (\Throwable $e) {
+            $this->logSync($logData);
+        }
+    }
+
+    private function logSync(array $logData): void
+    {
+        $level = $this->getLogLevel($logData['status']);
+        
+        try {
+            // Use 'api' channel if configured, otherwise fallback to default
+            if (config('logging.channels.api')) {
+                Log::channel('api')->log($level, 'API Request', $logData);
+            } else {
+                Log::log($level, 'API Request', $logData);
+            }
+        } catch (\Throwable $e) {
+            // If channel logging fails, use default logger
+            Log::log($level, 'API Request', $logData);
+        }
+    }
+
+    private function getLogLevel(int $statusCode): string
+    {
+        if ($statusCode >= 500) {
+            return 'error';
+        }
+
+        if ($statusCode >= 400) {
+            return 'warning';
+        }
+
+        return 'info';
+    }
+
+    private function maskApiKey(?string $apiKey): string
+    {
+        if (! $apiKey || strlen($apiKey) < 6) {
+            return 'none';
+        }
+
+        return substr($apiKey, 0, 3).'***'.substr($apiKey, -3);
+    }
+
+    private function getGraphQLOperation(Request $request): ?string
+    {
+        if ($request->path() !== 'api/graphql') {
+            return null;
+        }
+
+        $input = $request->json('operationName')
+            ?? $this->extractOperationName($request->json('query') ?? '');
+
+        return $input;
+    }
+
+    private function extractOperationName(string $query): ?string
+    {
+        if (preg_match('/^\s*(query|mutation|subscription)\s+(\w+)/i', $query, $matches)) {
+            return strtolower($matches[1]).': '.$matches[2];
+        }
+
+        return null;
+    }
+}

+ 192 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/RateLimitApi.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Cache\RateLimiter;
+use Illuminate\Http\Request;
+
+/**
+ * Implements rate limiting for API endpoints to prevent abuse
+ */
+class RateLimitApi
+{
+    /**
+     * Cache key prefix for rate limiting
+     */
+    private const CACHE_PREFIX = 'api:rate-limit:';
+
+    public function __construct(protected RateLimiter $limiter) {}
+
+    public function handle(Request $request, Closure $next)
+    {
+        if ($this->shouldSkipRateLimit($request)) {
+            return $next($request);
+        }
+
+        $limit = $this->getRateLimit($request);
+
+        if (! $limit) {
+            return $next($request);
+        }
+
+        $key = $this->resolveRequestSignature($request);
+
+        if (! $this->allowRequest($key, $limit)) {
+            return $this->buildResponse($request, $limit, $key);
+        }
+
+        $response = $next($request);
+
+        return $this->addRateLimitHeaders($response, $key, $limit);
+    }
+
+    private function shouldSkipRateLimit(Request $request): bool
+    {
+        if (in_array($request->path(), ['health', 'ping', ''])) {
+            return true;
+        }
+
+        if ($request->ip() === '127.0.0.1' || $request->ip() === '::1') {
+            return config('api-platform.rate_limit.skip_localhost', true);
+        }
+
+        return false;
+    }
+
+    private function getRateLimit(Request $request): ?array
+    {
+        $path = $request->path();
+
+        if (str_starts_with($path, 'api/auth/')) {
+            return [
+                'max_attempts'  => config('api-platform.rate_limit.auth', 5),
+                'decay_minutes' => 1,
+                'message'       => 'Too many authentication attempts. Please try again later.',
+            ];
+        }
+
+        if (str_starts_with($path, 'api/admin/')) {
+            return [
+                'max_attempts'  => config('api-platform.rate_limit.admin', 60),
+                'decay_minutes' => 1,
+                'message'       => 'Rate limit exceeded for admin API.',
+            ];
+        }
+
+        if (str_starts_with($path, 'api/shop/')) {
+            return [
+                'max_attempts'  => config('api-platform.rate_limit.shop', 100),
+                'decay_minutes' => 1,
+                'message'       => 'Rate limit exceeded. Please try again later.',
+            ];
+        }
+
+        if (str_starts_with($path, 'api/graphql')) {
+            return [
+                'max_attempts'  => config('api-platform.rate_limit.graphql', 100),
+                'decay_minutes' => 1,
+                'message'       => 'Rate limit exceeded for GraphQL queries.',
+            ];
+        }
+
+        return null;
+    }
+
+    /**
+     * Resolve request signature for rate limiting
+     * Uses API key if available (better for tracking), otherwise IP address
+     *
+     * Prefers API key as it's:
+     * - More accurate (per-key limits vs per-IP)
+     * - Supports shared IPs (offices, proxies)
+     * - Can be different for different key tiers
+     */
+    private function resolveRequestSignature(Request $request): string
+    {
+        if ($apiKey = $request->header('X-STOREFRONT-KEY')) {
+            return self::CACHE_PREFIX.hash('sha256', $apiKey);
+        }
+
+        return self::CACHE_PREFIX.'ip:'.hash('sha256', $request->ip() ?? '');
+    }
+
+    /**
+     * Check if request is allowed based on rate limit
+     */
+    private function allowRequest(string $key, array $limit): bool
+    {
+        $attempts = cache()->get($key, 0);
+
+        if ($attempts >= $limit['max_attempts']) {
+            return false;
+        }
+
+        if ($attempts === 0) {
+            cache()->put($key, 1, now()->addMinutes($limit['decay_minutes']));
+        } else {
+            cache()->increment($key);
+        }
+
+        return true;
+    }
+
+    /**
+     * Build rate limit exceeded response
+     */
+    private function buildResponse(Request $request, array $limit, string $key)
+    {
+        $retryAfter = $this->getRetryAfter($key, $limit);
+
+        return response()->json([
+            'error'       => 'rate_limit_exceeded',
+            'message'     => $limit['message'],
+            'retry_after' => $retryAfter,
+        ], 429, [
+            'Retry-After' => (string) $retryAfter,
+        ]);
+    }
+
+    /**
+     * Get retry-after seconds from cache TTL
+     */
+    private function getRetryAfter(string $key, array $limit): int
+    {
+        // Try to get TTL from cache backend
+        $ttl = cache()->getStore()->connection()->ttl($key);
+
+        // Handle different TTL formats:
+        // - Positive seconds: Redis returns TTL in seconds (convert from -2/-1)
+        // - Negative: Redis returns -1 (key doesn't exist) or -2 (expired)
+        // - Large number: If > current timestamp, it's likely a Unix timestamp (Redis cluster)
+        if ($ttl > 0) {
+            // TTL is in seconds (correct format)
+            return $ttl;
+        }
+
+        // If TTL is -1 or -2, or if large number (Unix timestamp), fallback to decay minutes
+        // For Unix timestamps, calculate seconds remaining
+        if ($ttl > time()) {
+            // It's a Unix timestamp, calculate seconds remaining
+            return max(1, $ttl - time());
+        }
+
+        // Fallback to configured decay minutes
+        return max(1, $limit['decay_minutes'] * 60);
+    }
+
+    /**
+     * Add rate limit headers to response
+     */
+    private function addRateLimitHeaders($response, string $key, array $limit)
+    {
+        $attempts = cache()->get($key, 0);
+        $remaining = max(0, $limit['max_attempts'] - $attempts);
+
+        $response->headers->set('X-RateLimit-Limit', (string) $limit['max_attempts']);
+        $response->headers->set('X-RateLimit-Remaining', (string) $remaining);
+        $response->headers->set('X-RateLimit-Reset', (string) now()->addMinutes($limit['decay_minutes'])->timestamp);
+
+        return $response;
+    }
+}

+ 70 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/SecurityHeaders.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+
+/**
+ * Adds essential HTTP security headers to API responses
+ */
+class SecurityHeaders
+{
+    public function handle(Request $request, Closure $next)
+    {
+        $response = $next($request);
+
+        if (! $this->isApiRequest($request)) {
+            return $response;
+        }
+
+        $this->addSecurityHeaders($response);
+
+        return $response;
+    }
+
+    private function isApiRequest(Request $request): bool
+    {
+        return str_starts_with($request->path(), 'api/');
+    }
+
+    private function addSecurityHeaders($response): void
+    {
+        $headers = [
+            'X-Content-Type-Options'  => 'nosniff',
+            'X-Frame-Options'         => 'DENY',
+            'X-XSS-Protection'        => '1; mode=block',
+            'Referrer-Policy'         => 'strict-origin-when-cross-origin',
+            'Permissions-Policy'      => 'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()',
+            'Content-Security-Policy' => $this->getCSPHeader(),
+        ];
+
+        if ($this->shouldUseHSTS()) {
+            $headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload';
+        }
+
+        foreach ($headers as $key => $value) {
+            $response->headers->set($key, $value);
+        }
+    }
+
+    private function shouldUseHSTS(): bool
+    {
+        return app()->environment('production') || config('api-platform.force_https', false);
+    }
+
+    private function getCSPHeader(): string
+    {
+        $defaultCSP = "default-src 'self'; "
+            ."script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
+            ."style-src 'self' 'unsafe-inline'; "
+            ."img-src 'self' data: https:; "
+            ."font-src 'self'; "
+            ."connect-src 'self'; "
+            ."frame-ancestors 'none'; "
+            ."base-uri 'self'; "
+            ."form-action 'self'";
+
+        return config('api-platform.csp_header', $defaultCSP);
+    }
+}

+ 41 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/SetLocaleChannel.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+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.
+ *
+ * If the headers are not sent, the current application locale and
+ * default channel are used — existing behaviour is preserved.
+ *
+ * Headers:
+ *   X-Locale  — locale code, e.g. "en", "fr", "ar"
+ *   X-Channel — channel code, e.g. "default"
+ */
+class SetLocaleChannel
+{
+    /**
+     * Handle an incoming request.
+     */
+    public function handle(Request $request, Closure $next): Response
+    {
+        $locale  = $request->header('X-Locale');
+        $channel = $request->header('X-Channel');
+
+        if ($locale) {
+            app()->setLocale($locale);
+            $request->attributes->set('bagisto_locale', $locale);
+        }
+
+        if ($channel) {
+            $request->attributes->set('bagisto_channel', $channel);
+        }
+
+        return $next($request);
+    }
+}

+ 123 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/VerifyBearerToken.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Tymon\JWTAuth\Exceptions\JWTException;
+use Tymon\JWTAuth\Facades\JWTAuth;
+
+/**
+ * Validates Bearer token (JWT) for user authentication on protected routes
+ */
+class VerifyBearerToken
+{
+    /**
+     * Routes that require Bearer token authentication
+     * (in addition to X-STOREFRONT-KEY)
+     */
+    protected array $protectedRoutes = [
+        '/api/shop/orders',
+        '/api/shop/cart',
+        '/api/shop/checkout',
+        '/api/shop/profile',
+        '/api/shop/wishlist',
+        '/api/admin',  // All admin routes
+    ];
+
+    public function handle(Request $request, Closure $next): Response
+    {
+        $path = $request->getPathInfo();
+
+        // Check if this route requires Bearer token
+        if (! $this->requiresBearerToken($path)) {
+            return $next($request);
+        }
+
+        // Get Bearer token from Authorization header
+        $token = $this->getBearerToken($request);
+
+        if (! $token) {
+            return $this->sendAuthenticationError('Bearer token is required for this operation');
+        }
+
+        try {
+            // Verify and decode JWT token
+            $user = JWTAuth::parseToken()->authenticate();
+
+            if (! $user) {
+                return $this->sendAuthenticationError('Invalid token or user not found', 'invalid_token');
+            }
+
+            // Check if token is expired
+            if ($this->isTokenExpired($token)) {
+                return $this->sendAuthenticationError('Token has expired', 'token_expired');
+            }
+
+            // Store authenticated user in request
+            $request->attributes->set('auth_user', $user);
+            $request->setUserResolver(fn () => $user);
+
+        } catch (JWTException $e) {
+            return $this->sendAuthenticationError('Invalid token: '.$e->getMessage(), 'invalid_token');
+        } catch (\Exception $e) {
+            return $this->sendAuthenticationError('Authentication error: '.$e->getMessage());
+        }
+
+        return $next($request);
+    }
+
+    protected function requiresBearerToken(string $path): bool
+    {
+        foreach ($this->protectedRoutes as $route) {
+            if (str_starts_with($path, $route)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    protected function getBearerToken(Request $request): ?string
+    {
+        $authHeader = $request->header('Authorization');
+
+        if (! $authHeader || ! str_starts_with($authHeader, 'Bearer ')) {
+            return null;
+        }
+
+        return substr($authHeader, 7); // Remove 'Bearer ' prefix
+    }
+
+    protected function isTokenExpired(string $token): bool
+    {
+        try {
+            $decoded = JWTAuth::parseToken()->getPayload();
+            $exp = $decoded->get('exp');
+
+            return $exp && $exp < now()->timestamp;
+        } catch (\Exception $e) {
+            return true; // Treat any error as expired
+        }
+    }
+
+    /**
+     * Send authentication error response
+     */
+    protected function sendAuthenticationError(string $message, string $error = 'missing_token'): \Illuminate\Http\JsonResponse
+    {
+        return response()->json([
+            'message' => $message,
+            'error'   => $error,
+            'errors'  => [
+                [
+                    'message'    => $message,
+                    'extensions' => [
+                        'code' => 'UNAUTHENTICATED',
+                    ],
+                ],
+            ],
+        ], 401);
+    }
+}

+ 441 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/VerifyGraphQLStorefrontKey.php

@@ -0,0 +1,441 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use ReflectionClass;
+use ReflectionMethod;
+use Symfony\Component\HttpFoundation\Response;
+use Webkul\BagistoApi\Attributes\AllowPublic;
+use Webkul\BagistoApi\Attributes\RequiresStorefrontKey;
+use Webkul\BagistoApi\Services\ApiKeyService;
+
+/**
+ * Validates API keys for GraphQL operations using attributes
+ */
+class VerifyGraphQLStorefrontKey
+{
+    /**
+     * Create a new middleware instance.
+     */
+    public function __construct(
+        protected ApiKeyService $apiKeyService
+    ) {}
+
+    /**
+     * Handle an incoming request.
+     */
+    public function handle(Request $request, Closure $next): Response
+    {
+        $path = $request->getPathInfo();
+
+        // Only apply to GraphQL endpoints
+        if (! str_starts_with($path, '/api/graphql') && ! str_starts_with($path, '/graphql')) {
+            return $next($request);
+        }
+
+        // For GET requests, inject X-STOREFRONT-KEY header into API Platform's GraphiQL
+        if ($request->method() === 'GET') {
+            $response = $next($request);
+
+            // If this is HTML (GraphiQL interface), inject header injection script
+            if (str_contains($response->headers->get('Content-Type', ''), 'text/html')) {
+                $response = $this->injectHeaderScript($response);
+            }
+
+            return $response;
+        }
+
+        // Determine if this is an admin GraphQL request
+        $isAdminRequest = str_starts_with($path, '/admin/graphql') ||
+                         $request->header('X-Admin-Key') !== null;
+
+        // Get the request body to check operation type
+        $body = $request->getContent();
+
+        try {
+            $data = json_decode($body, true) ?? [];
+            $query = $data['query'] ?? '';
+            $operationName = $data['operationName'] ?? null;
+
+            // Check if operation requires authentication using attributes or config
+            if (! $this->requiresAuthentication($query, $operationName)) {
+                return $next($request);
+            }
+        } catch (\Exception $e) {
+            // If we can't parse the query, require authentication
+            return $this->sendAuthenticationError('Invalid request body');
+        }
+
+        // Determine which key type is needed
+        $keyType = $isAdminRequest ? ApiKeyService::KEY_TYPE_ADMIN : ApiKeyService::KEY_TYPE_SHOP;
+        $headerName = $keyType === ApiKeyService::KEY_TYPE_ADMIN ? 'X-Admin-Key' : 'X-STOREFRONT-KEY';
+
+        // Get the appropriate API key from request
+        $key = ApiKeyService::getKeyFromRequest($request, $keyType);
+
+        if (! $key) {
+            return $this->sendAuthenticationError(
+                "$headerName header is required for this operation",
+                'missing_key',
+                $headerName,
+                $keyType
+            );
+        }
+
+        // Validate the key
+        $ipAddress = $request->ip();
+        $validation = $this->apiKeyService->validate($key, $keyType, $ipAddress);
+
+        if (! $validation['valid']) {
+            return $this->sendAuthenticationError('Invalid API key', 'invalid_key', $headerName, $keyType);
+        }
+
+        // Check rate limit
+        $client = $validation['client'];
+        $rateLimit = $this->apiKeyService->checkRateLimit($client);
+
+ #       if (! $rateLimit['allowed']) {
+  #          return response()->json([
+   #             'message'     => 'Rate limit exceeded',
+    #            'error'       => 'rate_limit_exceeded',
+     #           'retry_after' => $rateLimit['reset_at'],
+      #      ], 429);
+       # }
+
+        // Store in request for downstream use
+        $request->attributes->set('storefront_key', $client);
+        $request->attributes->set('rate_limit', $rateLimit);
+        $request->attributes->set('key_type', $keyType);
+
+        $response = $next($request);
+
+        // Add rate limit headers if response supports them
+        if (isset($rateLimit) && method_exists($response, 'header')) {
+            $response->header('X-RateLimit-Limit', $rateLimit['limit'] ?? 100);
+            $response->header('X-RateLimit-Remaining', $rateLimit['remaining'] ?? 0);
+            $response->header('X-RateLimit-Reset', $rateLimit['reset_at'] ?? 0);
+        }
+
+        return $response;
+    }
+
+    /**
+     * Determine if a GraphQL operation requires authentication
+     *
+     * Uses attribute-based configuration for API Platform:
+     * - #[AllowPublic] marks operations that don't need authentication
+     * - #[RequiresStorefrontKey] marks operations that need authentication
+     * - Everything else defaults to requiring authentication (secure by default)
+     *
+     * @param  string  $query  The GraphQL query/mutation string
+     * @param  string|null  $operationName  The operation name from request
+     */
+    protected function requiresAuthentication(string $query, ?string $operationName): bool
+    {
+        // Allow empty queries
+        if (empty(trim($query))) {
+            return false;
+        }
+
+        // Extract operation name from query if not provided
+        if (! $operationName) {
+            $operationName = $this->extractOperationName($query);
+        }
+
+        // Check for introspection patterns (allow for playground)
+        if (config('graphql-auth.allow_introspection', true)) {
+            if (strpos($query, '__schema') !== false || strpos($query, '__type') !== false) {
+                return false;
+            }
+        }
+
+        // First, try to find resolver using operation name and check attributes
+        $operationAuth = $this->checkOperationAttributes($operationName);
+        if ($operationAuth !== null) {
+            return $operationAuth;
+        }
+
+        // Fallback to config-based approach
+        $publicOperations = config('graphql-auth.public_operations', []);
+        if ($operationName && in_array($operationName, $publicOperations, true)) {
+            return false;
+        }
+
+        // Default: require authentication (secure by default)
+        return true;
+    }
+
+    /**
+     * Check if a GraphQL operation has authentication attributes
+     *
+     * Searches for resolvers/mutations with the operation name and checks:
+     * - #[AllowPublic] attribute → doesn't require auth (return false)
+     * - #[RequiresStorefrontKey] attribute → requires auth (return true)
+     *
+     * @param  string|null  $operationName  The operation name to find
+     * @return bool|null null if no attribute found, bool if found
+     */
+    protected function checkOperationAttributes(?string $operationName): ?bool
+    {
+        if (! $operationName) {
+            return null;
+        }
+
+        try {
+            // Search in common GraphQL resolver directories
+            $paths = [
+                app_path('GraphQL/Queries'),
+                app_path('GraphQL/Mutations'),
+                app_path('GraphQL/Subscriptions'),
+                app_path('Http/GraphQL/Queries'),
+                app_path('Http/GraphQL/Mutations'),
+                // Bagisto packages
+                base_path('packages/*/src/GraphQL/Queries'),
+                base_path('packages/*/src/GraphQL/Mutations'),
+            ];
+
+            foreach ($paths as $basePath) {
+                // Handle glob patterns
+                if (strpos($basePath, '*') !== false) {
+                    $dirs = glob($basePath, GLOB_BRACE);
+                    foreach ($dirs as $dir) {
+                        $result = $this->findOperationInDirectory($dir, $operationName);
+                        if ($result !== null) {
+                            return $result;
+                        }
+                    }
+                } else {
+                    if (is_dir($basePath)) {
+                        $result = $this->findOperationInDirectory($basePath, $operationName);
+                        if ($result !== null) {
+                            return $result;
+                        }
+                    }
+                }
+            }
+        } catch (\Exception $e) {
+            // If reflection fails, fall back to config
+            if (config('graphql-auth.log_auth_checks', false)) {
+                \Log::debug('GraphQL auth attribute check failed: '.$e->getMessage());
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Search for a GraphQL operation resolver in a directory and check its attributes
+     */
+    protected function findOperationInDirectory(string $directory, string $operationName): ?bool
+    {
+        if (! is_dir($directory)) {
+            return null;
+        }
+
+        // Look for files that might match the operation
+        $patterns = [
+            $operationName.'.php',
+            ucfirst($operationName).'.php',
+            strtolower($operationName).'.php',
+        ];
+
+        foreach ($patterns as $pattern) {
+            $filepath = $directory.'/'.$pattern;
+            if (file_exists($filepath)) {
+                return $this->checkFileAttributes($filepath, $operationName);
+            }
+        }
+
+        // Also check all PHP files in the directory
+        $files = glob($directory.'/*.php');
+        foreach ($files as $filepath) {
+            $result = $this->checkFileAttributes($filepath, $operationName);
+            if ($result !== null) {
+                return $result;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Check attributes in a PHP file for a GraphQL operation
+     */
+    protected function checkFileAttributes(string $filepath, string $operationName): ?bool
+    {
+        try {
+            // Get the class name from file
+            $content = file_get_contents($filepath);
+
+            // Extract namespace and class name
+            if (preg_match('/namespace\s+([\w\\]+)/', $content, $nsMatches) &&
+                preg_match('/class\s+(\w+)/', $content, $classMatches)) {
+
+                $className = $nsMatches[1].'\\'.$classMatches[2];
+
+                if (! class_exists($className)) {
+                    return null;
+                }
+
+                $reflection = new ReflectionClass($className);
+
+                // Check class-level attributes
+                foreach ($reflection->getAttributes(AllowPublic::class) as $attr) {
+                    return false; // AllowPublic = no auth needed
+                }
+
+                foreach ($reflection->getAttributes(RequiresStorefrontKey::class) as $attr) {
+                    return true; // RequiresStorefrontKey = auth needed
+                }
+
+                // Check method-level attributes (for method-based resolvers)
+                foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+                    // Check if method name matches operation name (camelCase, snake_case, etc)
+                    if ($this->methodNameMatches($method->getName(), $operationName)) {
+                        foreach ($method->getAttributes(AllowPublic::class) as $attr) {
+                            return false;
+                        }
+
+                        foreach ($method->getAttributes(RequiresStorefrontKey::class) as $attr) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        } catch (\Exception $e) {
+            if (config('graphql-auth.log_auth_checks', false)) {
+                \Log::debug("Failed to check attributes in {$filepath}: ".$e->getMessage());
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Check if a method name matches the operation name
+     * Handles different naming conventions (camelCase, snake_case, PascalCase)
+     */
+    protected function methodNameMatches(string $methodName, string $operationName): bool
+    {
+        // Direct match
+        if ($methodName === $operationName) {
+            return true;
+        }
+
+        // CamelCase vs snake_case
+        $camelToSnake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $methodName));
+        $opSnake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $operationName));
+        if ($camelToSnake === $opSnake) {
+            return true;
+        }
+
+        // Case-insensitive match
+        if (strtolower($methodName) === strtolower($operationName)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Extract the operation name from a GraphQL query
+     * Handles queries like:
+     *   query GetProducts { ... }
+     *   mutation AddToCart { ... }
+     *   GetProducts { ... }
+     */
+    protected function extractOperationName(string $query): ?string
+    {
+        // Remove whitespace for easier matching
+        $query = preg_replace('/\s+/', ' ', $query);
+
+        // Match "query OperationName" or "mutation OperationName" or just "OperationName"
+        if (preg_match('/(?:query|mutation|subscription)\s+(\w+)/i', $query, $matches)) {
+            return $matches[1];
+        }
+
+        // Try to find operation name without query/mutation/subscription keyword
+        if (preg_match('/^\s*(\w+)\s*\{/', $query, $matches)) {
+            return $matches[1];
+        }
+
+        return null;
+    }
+
+    /**
+     * Send authentication error response
+     *
+     * @param  string  $headerName  The header name required (X-STOREFRONT-KEY or X-Admin-Key)
+     * @param  string  $keyType  The key type needed (shop or admin)
+     */
+    protected function sendAuthenticationError(
+        string $message,
+        string $error = 'missing_key',
+        string $headerName = 'X-STOREFRONT-KEY',
+        string $keyType = 'shop'
+    ): \Illuminate\Http\JsonResponse {
+        return response()->json([
+            'message'     => $message,
+            'error'       => $error,
+            'header_name' => $headerName,
+            'key_type'    => $keyType,
+            'errors'      => [
+                [
+                    'message'    => $message,
+                    'extensions' => [
+                        'code' => 'UNAUTHENTICATED',
+                    ],
+                ],
+            ],
+        ], 401);
+    }
+
+    /**
+     * Inject X-STOREFRONT-KEY header script into API Platform's GraphiQL HTML
+     * Intercepts fetch requests and adds the header automatically
+     */
+    protected function injectHeaderScript(Response $response): Response
+    {
+        $storefrontKey = env('STOREFRONT_PLAYGROUND_KEY') ?? 'pk_storefront_xxxxx';
+
+        $script = <<<'JS'
+<script>
+(function() {
+    const storefrontKey = localStorage.getItem('bagisto-api-key') || 'JS' . $storefrontKey . 'JS';
+    const originalFetch = window.fetch;
+    
+    // Override fetch to inject X-STOREFRONT-KEY header
+    window.fetch = function(...args) {
+        let options = args[1] || {};
+        options.headers = options.headers || {};
+        options.headers['X-STOREFRONT-KEY'] = storefrontKey;
+        return originalFetch.apply(this, [args[0], options]);
+    };
+
+    // Allow users to edit the key via browser console or dev tools
+    window.setBagistoApiKey = function(key) {
+        localStorage.setItem('bagisto-api-key', key);
+        console.log('Bagisto API Key updated:', key);
+    };
+
+    window.getBagistoApiKey = function() {
+        return localStorage.getItem('bagisto-api-key') || storefrontKey;
+    };
+
+    console.log('%cBagisto GraphQL API', 'color: #667eea; font-size: 14px; font-weight: bold;');
+    console.log('X-STOREFRONT-KEY: ' + getBagistoApiKey());
+    console.log('Change key with: setBagistoApiKey("your-key-here")');
+})();
+</script>
+JS;
+
+        $content = $response->getContent();
+        // Inject script before closing </body> tag
+        $content = str_replace('</body>', $script.'</body>', $content);
+        $response->setContent($content);
+
+        return $response;
+    }
+}

+ 219 - 0
packages/Webkul/BagistoApi/src/Http/Middleware/VerifyStorefrontKey.php

@@ -0,0 +1,219 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Webkul\BagistoApi\Services\ApiKeyService;
+
+/**
+ * Validates API keys for both shop and admin REST APIs
+ */
+class VerifyStorefrontKey
+{
+    public function __construct(
+        protected ApiKeyService $apiKeyService
+    ) {}
+
+    public function handle(Request $request, Closure $next): Response
+    {
+        $path = $request->getPathInfo();
+
+        // Determine which API key type is required based on path
+        $keyType = $this->getRequiredKeyType($path);
+
+        // If no API key required for this path, skip
+        if ($keyType === null) {
+            return $next($request);
+        }
+
+        // Skip authentication for GET requests to documentation endpoints
+        if ($this->isDocumentationEndpoint($request)) {
+            return $next($request);
+        }
+
+        // Get appropriate API key from request
+        $key = ApiKeyService::getKeyFromRequest($request, $keyType);
+
+        // Return 401 if key is missing
+        if (! $key) {
+            return $this->missingKeyResponse($keyType);
+        }
+
+        // In testing environment, allow test keys without database validation
+        if (app()->environment('testing') && $this->isTestKey($key)) {
+            $request->attributes->set('api_key', [
+                'id' => 'test-key',
+                'name' => 'Test Key',
+                'rate_limit' => 10000,
+            ]);
+            $request->attributes->set('key_type', $keyType);
+            $request->attributes->set('rate_limit', [
+                'allowed' => true,
+                'remaining' => 10000,
+                'reset_at' => 0,
+            ]);
+            return $next($request);
+        }
+
+        $ipAddress = $request->ip();
+
+        // Validate the key with its type
+        $validation = $this->apiKeyService->validate($key, $keyType, $ipAddress);
+
+        if (! $validation['valid']) {
+            return $this->unauthorizedResponse($validation['message']);
+        }
+
+        // Check rate limit
+        $client = $validation['client'];
+        $rateLimit = $this->apiKeyService->checkRateLimit($client);
+
+        // Store for later use in the request
+        $request->attributes->set('api_key', $client);
+        $request->attributes->set('key_type', $keyType);
+        $request->attributes->set('rate_limit', $rateLimit);
+
+        if (! $rateLimit['allowed']) {
+            return $this->rateLimitExceededResponse($rateLimit);
+        }
+
+        $response = $next($request);
+
+        // Add rate limit headers to response
+        return $this->addRateLimitHeaders($response, $rateLimit);
+    }
+
+    /**
+     * Determine which API key type is required for a route
+     *
+     * @return string|null ApiKeyService::KEY_TYPE_* or null
+     */
+    protected function getRequiredKeyType(string $path): ?string
+    {
+        // Admin routes require X-Admin-Key
+        if (str_starts_with($path, '/api/admin')) {
+            return ApiKeyService::KEY_TYPE_ADMIN;
+        }
+
+        // Shop routes require X-STOREFRONT-KEY
+        if (str_starts_with($path, '/api/shop')) {
+            return ApiKeyService::KEY_TYPE_SHOP;
+        }
+
+        // Other routes don't require API key
+        return null;
+    }
+
+    /**
+     * Check if the request is to a documentation endpoint.
+     * GET requests to API documentation and playgrounds bypass authentication.
+     */
+    protected function isDocumentationEndpoint(Request $request): bool
+    {
+        $path = $request->getPathInfo();
+        $method = $request->method();
+
+        // Only skip auth for GET requests (documentation views)
+        if ($method !== 'GET') {
+            return false;
+        }
+
+        // Skip authentication for API documentation and playground endpoints
+        $documentationPaths = [
+            '/api',                          // API documentation index
+            '/api/docs',                     // API documentation pages
+            '/api/shop',                     // Shop API documentation
+            '/api/shop/docs',                // Shop API OpenAPI spec
+            '/api/admin',                    // Admin API documentation
+            '/api/admin/docs',               // Admin API OpenAPI spec
+            '/api/graphql',                  // GraphQL Playground
+            '/graphql',                      // GraphQL alternate path
+            '/graphiql',                     // Shop GraphQL Playground
+            '/admin/graphiql',               // Admin GraphQL Playground
+        ];
+
+        // Check exact match or prefix match for documentation
+        foreach ($documentationPaths as $docPath) {
+            if ($path === $docPath || strpos($path, $docPath.'?') === 0) {
+                return true;
+            }
+        }
+
+        // Allow playground and GraphiQL specific paths
+        if (
+            strpos($path, '/api/graphiql') === 0 ||
+            strpos($path, '/api/graphql') === 0 ||
+            strpos($path, '/api/graphql/playground') === 0 
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Return missing API key response
+     *
+     * @param  string  $keyType  The required key type
+     */
+    protected function missingKeyResponse(string $keyType = 'shop'): \Illuminate\Http\JsonResponse
+    {
+        $headerName = $keyType === 'admin' ? 'X-Admin-Key' : 'X-STOREFRONT-KEY';
+
+        return response()->json([
+            'message'     => "{$headerName} header is required",
+            'error'       => 'missing_key',
+            'header_name' => $headerName,
+            'key_type'    => $keyType,
+        ], 401);
+    }
+
+    /**
+     * Return an unauthorized JSON response.
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    protected function unauthorizedResponse(string $message): Response
+    {
+        return response()->json([
+            'message' => $message,
+            'error'   => 'invalid_key',
+        ], 403);
+    }
+
+    /**
+     * Return rate limit exceeded response.
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    protected function rateLimitExceededResponse(array $rateLimit): Response
+    {
+        return response()->json([
+            'message'     => 'Rate limit exceeded',
+            'error'       => 'rate_limit_exceeded',
+            'retry_after' => $rateLimit['reset_at'] ?? 60,
+        ], 429);
+    }
+
+    /**
+     * Add rate limit headers to response.
+     */
+    protected function addRateLimitHeaders(Response $response, array $rateLimit): Response
+    {
+        $response->headers->set('X-RateLimit-Limit', (string) config('storefront.default_rate_limit', 100));
+        $response->headers->set('X-RateLimit-Remaining', (string) $rateLimit['remaining']);
+        $response->headers->set('X-RateLimit-Reset', (string) ($rateLimit['reset_at'] ?? time() + 60));
+
+        return $response;
+    }
+
+    /**
+     * Check if the key is a test key (for testing environment)
+     */
+    protected function isTestKey(string $key): bool
+    {
+        return str_starts_with($key, 'pk_test_') || $key === 'test-key';
+    }
+}

+ 109 - 0
packages/Webkul/BagistoApi/src/Http/Requests/Admin/ProductFormRequest.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace Webkul\BagistoApi\Http\Requests\Admin;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Webkul\Core\Rules\Slug;
+use Webkul\Product\Helpers\ProductType;
+
+class ProductFormRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        $productTypes = implode(',', array_keys(config('product_types', [])));
+
+        return [
+            'type'                => 'required|in:'.$productTypes,
+            'attributeFamily'     => 'required|exists:attribute_families,id',
+            'sku'                 => ['required', 'unique:products,sku', new Slug],
+            'name'                => 'required|string',
+            'description'         => 'nullable|string',
+            'shortDescription'    => 'nullable|string',
+            'status'              => 'nullable|boolean',
+            'new'                 => 'nullable|boolean',
+            'featured'            => 'nullable|boolean',
+            'price'               => 'nullable|numeric',
+            'special_price'       => 'nullable|numeric',
+            'weight'              => 'nullable|numeric',
+            'cost'                => 'nullable|numeric',
+            'length'              => 'nullable|numeric',
+            'width'               => 'nullable|numeric',
+            'height'              => 'nullable|numeric',
+            'color'               => 'nullable',
+            'size'                => 'nullable',
+            'brand'               => 'nullable',
+            'super_attributes'    => 'array|min:1',
+            'super_attributes.*'  => 'array|min:1',
+        ];
+    }
+
+    public function messages(): array
+    {
+        return [
+            'type.required'                => trans('api-resources.rest-api.admin.catalog.products.error.type-required'),
+            'type.in'                      => trans('api-resources.rest-api.admin.catalog.products.error.type-invalid'),
+            'attributeFamily.required'     => trans('api-resources.rest-api.admin.catalog.products.error.attribute-family-required'),
+            'attributeFamily.exists'       => trans('api-resources.rest-api.admin.catalog.products.error.attribute-family-exists'),
+            'sku.required'                 => trans('api-resources.rest-api.admin.catalog.products.error.sku-required'),
+            'sku.unique'                   => trans('api-resources.rest-api.admin.catalog.products.error.sku-unique'),
+            'super_attributes.array'       => trans('api-resources.rest-api.admin.catalog.products.error.super-attributes-array'),
+            'super_attributes.min'         => trans('api-resources.rest-api.admin.catalog.products.error.super-attributes-min'),
+            'super_attributes.*.array'     => trans('api-resources.rest-api.admin.catalog.products.error.super-attributes-array'),
+            'super_attributes.*.min'       => trans('api-resources.rest-api.admin.catalog.products.error.super-attributes-min'),
+        ];
+    }
+
+    protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator)
+    {
+        throw new \Illuminate\Validation\ValidationException($validator);
+    }
+
+    public function attributes(): array
+    {
+        return [
+            'type'                => trans('api-resources.rest-api.admin.catalog.products.type'),
+            'attributeFamily'     => 'attributeFamily',
+            'sku'                 => 'sku',
+            'name'                => 'name',
+            'description'         => 'description',
+            'shortDescription'    => 'shortDescription',
+            'new'                 => 'new',
+            'featured'            => 'featured',
+            'price'               => 'price',
+            'weight'              => 'weight',
+            'cost'                => 'cost',
+            'length'              => 'length',
+            'width'               => 'width',
+            'height'              => 'height',
+            'color'               => 'color',
+            'size'                => 'size',
+            'brand'               => 'brand',
+            'super_attributes'    => 'super_attributes',
+        ];
+    }
+
+    public function validated($key = null, $default = null)
+    {
+        $validated = parent::validated($key, $default);
+
+        if (
+            ProductType::hasVariants($this->input('type'))
+            && ! $this->has('super_attributes')
+        ) {
+            throw new \Illuminate\Validation\ValidationException(
+                \Illuminate\Support\Facades\Validator::make(
+                    $this->all(),
+                    ['super_attributes'          => 'required'],
+                    ['super_attributes.required' => trans('api-resources.rest-api.admin.catalog.products.error.configurable-error')]
+                )
+            );
+        }
+
+        return $validated;
+    }
+}

+ 68 - 0
packages/Webkul/BagistoApi/src/Input/CreateProductInput.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace Webkul\BagistoApi\Input;
+
+use Webkul\BagistoApi\Models\Product;
+
+class CreateProductInput extends Product
+{
+    public ?string $sku = null;
+
+    public ?string $type = null;
+
+    public ?string $attributeFamily = null;
+
+    public ?string $name = null;
+
+    public ?string $status = null;
+
+    public ?string $urlKey = null;
+
+    public ?string $description = null;
+
+    public ?string $shortDescription = null;
+
+    public ?string $weight = null;
+
+    public ?string $productNumber = null;
+
+    public ?float $price = null;
+
+    public ?float $specialPrice = null;
+
+    public ?float $cost = null;
+
+    public ?bool $new = null;
+
+    public ?bool $featured = null;
+
+    public ?bool $visibleIndividually = null;
+
+    public ?bool $guestCheckout = null;
+
+    public ?bool $manageStock = null;
+
+    public ?int $taxCategoryId = null;
+
+    public ?int $color = null;
+
+    public ?int $size = null;
+
+    public ?int $brand = null;
+
+    public ?string $specialPriceFrom = null;
+
+    public ?string $specialPriceTo = null;
+
+    public ?string $metaTitle = null;
+
+    public ?string $metaKeywords = null;
+
+    public ?string $metaDescription = null;
+
+    public ?string $length = null;
+
+    public ?string $width = null;
+
+    public ?string $height = null;
+}

+ 14 - 0
packages/Webkul/BagistoApi/src/Input/LoginInput.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Webkul\BagistoApi\Input;
+
+/**
+ * LoginInput DTO for customer login credentials
+ */
+class LoginInput
+{
+    public function __construct(
+        public ?string $email = null,
+        public ?string $password = null,
+    ) {}
+}

+ 73 - 0
packages/Webkul/BagistoApi/src/Jobs/LogApiRequestJob.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace Webkul\BagistoApi\Jobs;
+
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * Queued job for asynchronous API request logging
+ */
+class LogApiRequestJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(
+        public array $logData
+    ) {
+        $this->queue = 'api-logs';
+        $this->timeout = 30;
+    }
+
+    public function handle(): void
+    {
+        try {
+            $level = $this->getLogLevel($this->logData['status']);
+            
+            // Use 'api' channel if configured, otherwise fall back to default
+            try {
+                if (config('logging.channels.api')) {
+                    Log::channel('api')->log($level, 'API Request', $this->logData);
+                } else {
+                    Log::log($level, 'API Request', $this->logData);
+                }
+            } catch (\Throwable $e) {
+                // If channel logging fails, use default logger
+                Log::log($level, 'API Request', $this->logData);
+            }
+        } catch (\Throwable $e) {
+            Log::error('Failed to log API request', [
+                'error'    => $e->getMessage(),
+                'log_data' => $this->logData,
+            ]);
+
+            throw $e;
+        }
+    }
+
+    private function getLogLevel(int $statusCode): string
+    {
+        if ($statusCode >= 500) {
+            return 'error';
+        }
+
+        if ($statusCode >= 400) {
+            return 'warning';
+        }
+
+        return 'info';
+    }
+
+    public function failed(\Throwable $exception): void
+    {
+        Log::critical('API request logging job failed', [
+            'error'    => $exception->getMessage(),
+            'log_data' => $this->logData,
+            'attempts' => $this->attempts(),
+        ]);
+    }
+}

+ 42 - 0
packages/Webkul/BagistoApi/src/Metadata/CustomIdentifiersExtractor.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace Webkul\BagistoApi\Metadata;
+
+use ApiPlatform\Metadata\IdentifiersExtractorInterface;
+use ApiPlatform\Metadata\Operation;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * Extracts identifiers from Eloquent models for API Platform
+ */
+class CustomIdentifiersExtractor implements IdentifiersExtractorInterface
+{
+    public function __construct(
+        private IdentifiersExtractorInterface $decorated
+    ) {}
+
+    public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array
+    {
+        if ($item instanceof Model) {
+            $id = null;
+
+            if (method_exists($item, 'getId') && is_callable([$item, 'getId'])) {
+                $id = $item->getId();
+            }
+
+            if ($id === null) {
+                $id = $item->getKey();
+            }
+
+            // Only return identifier if it's not null
+            if ($id !== null) {
+                return ['id' => $id];
+            }
+
+            // For new items without an ID, return empty array to avoid IRI generation issues
+            return [];
+        }
+
+        return $this->decorated->getIdentifiersFromItem($item, $operation, $context);
+    }
+}

+ 266 - 0
packages/Webkul/BagistoApi/src/Models/AddProductInCart.php

@@ -0,0 +1,266 @@
+<?php
+
+namespace Webkul\BagistoApi\Models;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GraphQl\Mutation;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\OpenApi\Model;
+use Webkul\BagistoApi\Dto\CartData;
+use Webkul\BagistoApi\Dto\CartInput;
+use Webkul\BagistoApi\State\CartTokenMutationProvider;
+use Webkul\BagistoApi\State\CartTokenProcessor;
+
+/**
+ * AddProductInCart - GraphQL & REST API Resource for Adding Products to Cart
+ *
+ * Provides mutation for adding products to an existing shopping cart.
+ * Uses token-based authentication for guest users or bearer token for authenticated users.
+ */
+#[ApiResource(
+    routePrefix: '/api/shop',
+    shortName: 'AddProductInCart',
+    uriTemplate: '/add-product-in-cart',
+    operations: [
+        new Post(
+            name: 'addProduct',
+            uriTemplate: '/add-product-in-cart',
+            input: CartInput::class,
+            output: CartData::class,
+            provider: CartTokenMutationProvider::class,
+            processor: CartTokenProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+                'groups'                 => ['mutation'],
+            ],
+            normalizationContext: [
+                'groups'                 => ['mutation'],
+            ],
+            description: 'Add product to cart. Can be used for both authenticated users and guests.',
+            openapi: new Model\Operation(
+                summary: 'Add product to cart',
+                description: 'Add a product to the shopping cart with quantity and optional product options.',
+                requestBody: new Model\RequestBody(
+                    description: 'Product to add to cart',
+                    required: true,
+                    content: new \ArrayObject([
+                        'application/json' => [
+                            'schema' => [
+                                'type'       => 'object',
+                                'properties' => [
+                                    'productId' => [
+                                        'type'        => 'integer',
+                                        'example'     => 1,
+                                        'description' => 'Product ID',
+                                    ],
+                                    'quantity' => [
+                                        'type'        => 'integer',
+                                        'example'     => 1,
+                                        'description' => 'Quantity',
+                                    ],
+                                    'options' => [
+                                        'type'        => 'object',
+                                        'example'     => ['size' => 'M', 'color' => 'blue'],
+                                        'description' => 'Product options (optional)',
+                                    ],
+                                    'bundleOptions' => [
+                                        'type'        => 'string',
+                                        'example'     => '{"1":[1],"2":[2],"3":[3],"4":[4]}',
+                                        'description' => 'Bundle options JSON (optional)',
+                                    ],
+                                    'bundleOptionQty' => [
+                                        'type'        => 'string',
+                                        'example'     => '{"1":1,"2":2,"3":1,"4":2}',
+                                        'description' => 'Bundle option quantities JSON (optional)',
+                                    ],
+                                    'groupedQty' => [
+                                        'type'        => 'string',
+                                        'example'     => '{"101":2,"102":1}',
+                                        'description' => 'Grouped product associated quantities JSON (optional, required for grouped products)',
+                                    ],
+                                    'booking' => [
+                                        'type'        => 'string',
+                                        'example'     => '{"type":"appointment","date":"2026-03-12","slot":"10:00 AM - 11:00 AM"}',
+                                        'description' => 'Booking options JSON string (optional, required for booking products)',
+                                    ],
+                                    'specialNote' => [
+                                        'type'        => 'string',
+                                        'example'     => 'This is a special note',
+                                        'description' => 'Special request / note (optional; merged into booking.note)',
+                                    ],
+                                ],
+                            ],
+                            'examples' => [
+                                'simple_product' => [
+                                    'summary'     => 'Add Simple Product',
+                                    'description' => 'Add a simple product to cart',
+                                    'value'       => [
+                                        'productId' => 1,
+                                        'quantity'  => 1,
+                                    ],
+                                ],
+                                'product_with_options' => [
+                                    'summary'     => 'Add Product with Options',
+                                    'description' => 'Add a product with size and color options',
+                                    'value'       => [
+                                        'productId' => 2,
+                                        'quantity'  => 2,
+                                        'options'   => ['size' => 'M', 'color' => 'blue'],
+                                    ],
+                                ],
+                                'bundle_product' => [
+                                    'summary'     => 'Add Bundle Product',
+                                    'description' => 'Add a bundle product with selected bundle options',
+                                    'value'       => [
+                                        'productId'       => 6,
+                                        'quantity'        => 1,
+                                        'bundleOptions'   => '{"1":[1],"2":[2],"3":[3],"4":[4]}',
+                                        'bundleOptionQty' => '{"1":1,"2":2,"3":1,"4":2}',
+                                    ],
+                                ],
+                                'grouped_product' => [
+                                    'summary'     => 'Add Grouped Product',
+                                    'description' => 'Add a grouped product by specifying quantities for associated simple products',
+                                    'value'       => [
+                                        'productId'   => 5,
+                                        'quantity'    => 1,
+                                        'groupedQty'  => '{"101":2,"102":1}',
+                                    ],
+                                ],
+                                'booking_product' => [
+                                    'summary'     => 'Add Booking Product',
+                                    'description' => 'Add a booking product (appointment/default/table/rental/event) to cart by passing booking options as JSON string',
+                                    'value'       => [
+                                        'productId' => 2555,
+                                        'quantity'  => 1,
+                                        'booking'   => '{"type":"appointment","date":"2026-03-12","slot":"10:00 AM - 11:00 AM"}',
+                                    ],
+                                ],
+                                'event_booking_product' => [
+                                    'summary'     => 'Add Event Booking',
+                                    'description' => 'Add an event booking product by selecting one or more ticket quantities (at least one ticket qty > 0 required)',
+                                    'value'       => [
+                                        'productId' => 2564,
+                                        'quantity'  => 1,
+                                        'booking'   => '{"type":"event","qty":{"501":1,"502":2}}',
+                                    ],
+                                ],
+                                'table_booking_product_with_note' => [
+                                    'summary'     => 'Add Table Booking (with note)',
+                                    'description' => 'Add a table booking product and send special request/note separately',
+                                    'value'       => [
+                                        'productId'    => 2563,
+                                        'quantity'     => 1,
+                                        'booking'      => '{"type":"table","date":"2026-03-25","slot":"12:00 PM - 12:45 PM"}',
+                                        'specialNote'  => 'This is a special note',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ]),
+                ),
+            ),
+        ),
+    ],
+    graphQlOperations: [
+        new Mutation(
+            name: 'create',
+            input: CartInput::class,
+            output: CartData::class,
+            provider: CartTokenMutationProvider::class,
+            processor: CartTokenProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+                'groups'                 => ['mutation'],
+            ],
+            normalizationContext: [
+                'groups'                 => ['mutation'],
+            ],
+            description: 'Add product to cart. Can be used for both authenticated users and guests.',
+        ),
+    ]
+)]
+class AddProductInCart
+{
+    #[ApiProperty(readable: true, writable: false)]
+    public ?int $id = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $cartToken = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?int $customerId = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?int $channelId = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?int $itemsCount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?array $items = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $subtotal = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $baseSubtotal = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $discountAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $baseDiscountAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $taxAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $baseTaxAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $shippingAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $baseShippingAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $grandTotal = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?float $baseGrandTotal = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $formattedSubtotal = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $formattedDiscountAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $formattedTaxAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $formattedShippingAmount = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $formattedGrandTotal = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $couponCode = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?bool $success = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $message = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?array $carts = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?string $sessionToken = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    public ?bool $isGuest = null;
+}

+ 101 - 0
packages/Webkul/BagistoApi/src/Models/ApplyCoupon.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace Webkul\BagistoApi\Models;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\GraphQl\Mutation;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\OpenApi\Model;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Webkul\BagistoApi\Dto\CartData;
+use Webkul\BagistoApi\Dto\CartInput;
+use Webkul\BagistoApi\State\CartTokenMutationProvider;
+use Webkul\BagistoApi\State\CartTokenProcessor;
+
+/**
+ * ApplyCoupon - GraphQL & REST API Resource for Applying Coupon Code
+ *
+ * Provides mutation for applying a coupon code to cart.
+ */
+#[ApiResource(
+    routePrefix: '/api/shop',
+    shortName: 'ApplyCoupon',
+    uriTemplate: '/apply-coupon',
+    operations: [
+        new Post(
+            name: 'apply',
+            uriTemplate: '/apply-coupon',
+            input: CartInput::class,
+            output: CartData::class,
+            provider: CartTokenMutationProvider::class,
+            processor: CartTokenProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+                'groups'                 => ['mutation'],
+            ],
+            normalizationContext: [
+                'groups'                 => ['mutation'],
+            ],
+            description: 'Apply coupon code to cart.',
+            openapi: new Model\Operation(
+                summary: 'Apply coupon to cart',
+                description: 'Apply a discount coupon code to the shopping cart.',
+                requestBody: new Model\RequestBody(
+                    description: 'Coupon code to apply',
+                    required: true,
+                    content: new \ArrayObject([
+                        'application/json' => [
+                            'schema' => [
+                                'type'       => 'object',
+                                'properties' => [
+                                    'couponCode' => [
+                                        'type'        => 'string',
+                                        'example'     => 'DISCOUNT10',
+                                        'description' => 'Coupon code',
+                                    ],
+                                ],
+                            ],
+                            'examples' => [
+                                'apply_coupon' => [
+                                    'summary'     => 'Apply Coupon',
+                                    'description' => 'Apply a discount coupon code',
+                                    'value'       => [
+                                        'couponCode' => 'DISCOUNT10',
+                                    ],
+                                ],
+                            ],
+                        ],
+                    ]),
+                ),
+            ),
+        ),
+    ],
+    graphQlOperations: [
+        new Mutation(
+            name: 'create',
+            input: CartInput::class,
+            output: CartData::class,
+            provider: CartTokenMutationProvider::class,
+            processor: CartTokenProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+                'groups'                 => ['mutation'],
+            ],
+            normalizationContext: [
+                'groups'                 => ['mutation'],
+            ],
+            description: 'Apply coupon code to cart. Use token and couponCode.',
+        ),
+    ]
+)]
+class ApplyCoupon
+{
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['query', 'mutation'])]
+    public ?int $id = null;
+
+    #[ApiProperty(readable: true, writable: false)]
+    #[Groups(['query', 'mutation'])]
+    public ?string $cartToken = null;
+}

+ 0 - 0
packages/Webkul/BagistoApi/src/Models/Attribute.php


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است