| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- <?php
- namespace Webkul\BagistoApi\State;
- use ApiPlatform\Metadata\Operation;
- use ApiPlatform\State\ProcessorInterface;
- use Illuminate\Support\Facades\Request;
- use Webkul\BagistoApi\Dto\CartData;
- use Webkul\BagistoApi\Dto\CheckoutAddressInput;
- use Webkul\BagistoApi\Dto\CheckoutAddressOutput;
- use Webkul\BagistoApi\Dto\PaymentInitiateInput;
- use Webkul\BagistoApi\Exception\AuthenticationException;
- use Webkul\BagistoApi\Exception\OperationFailedException;
- use Webkul\BagistoApi\Exception\ResourceNotFoundException;
- use Webkul\BagistoApi\Facades\CartTokenFacade;
- use Webkul\BagistoApi\Facades\TokenHeaderFacade;
- use Webkul\BagistoApi\Services\PaymentService;
- use Webkul\Checkout\Facades\Cart;
- use Webkul\Checkout\Repositories\CartRepository;
- use Webkul\Customer\Repositories\CustomerRepository;
- use Webkul\Sales\Repositories\OrderRepository;
- /**
- * Handles checkout operations including address, shipping, payment, and order creation.
- */
- class CheckoutProcessor implements ProcessorInterface
- {
- public function __construct(
- protected CustomerRepository $customerRepository,
- protected OrderRepository $orderRepository,
- protected CartRepository $cartRepository,
- protected ?PaymentService $paymentService = null,
- ) {}
- /**
- * Process checkout operation.
- */
- public function process(
- mixed $data,
- Operation $operation,
- array $uriVariables = [],
- array $context = []
- ): mixed {
- $request = Request::instance() ?? ($context['request'] ?? null);
- $operationName = $this->mapOperation($operation);
- if ($operationName === 'read') {
- // Extract token from Authorization header only (no context/input parameters)
- $token = TokenHeaderFacade::getAuthorizationBearerToken($request);
- if (! $token) {
- throw new AuthenticationException(__('bagistoapi::app.graphql.cart.authentication-required'));
- }
- $cart = CartTokenFacade::getCartByToken($token);
- if (! $cart) {
- throw new ResourceNotFoundException(__('bagistoapi::app.graphql.cart.invalid-token'));
- }
- return $this->fetchAddresses($cart);
- }
- if (! $data instanceof CheckoutAddressInput) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-input'));
- }
- // Extract token from Authorization header (Bearer token) via TokenHeaderFacade
- // Token is NOT a DTO property - it's extracted from Authorization header only
- $token = null;
- if ($request) {
- $token = TokenHeaderFacade::getAuthorizationBearerToken($request);
- }
- if (! $token) {
- throw new AuthenticationException(__('bagistoapi::app.graphql.cart.authentication-required'));
- }
- $cart = CartTokenFacade::getCartByToken($token);
- if (! $cart) {
- throw new ResourceNotFoundException(__('bagistoapi::app.graphql.cart.invalid-token'));
- }
- return match ($operationName) {
- 'saveAddress' => $this->saveAddress($cart, $data),
- 'saveShippingMethod' => $this->saveShippingMethod($cart, $data),
- 'savePaymentMethod' => $this->savePaymentMethod($cart, $data),
- 'createOrder' => $this->createOrder($cart, $data),
- default => throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.unknown-operation', ['operation' => $operationName])),
- };
- }
- /**
- * Map BagistoApi operation name to internal operation type
- */
- private function mapOperation(Operation $operation): string
- {
- $operationName = $operation->getName();
- $resourceClass = $operation->getClass();
- $resourceClassName = $resourceClass ? class_basename($resourceClass) : '';
- return match ($resourceClassName) {
- 'CheckoutAddress' => 'saveAddress',
- 'CheckoutShippingMethod' => 'saveShippingMethod',
- 'CheckoutPaymentMethod' => 'savePaymentMethod',
- 'CheckoutOrder' => 'createOrder',
- default => $operationName,
- };
- }
- /**
- * Save billing and shipping addresses for cart.
- */
- private function saveAddress($cart, CheckoutAddressInput $input)
- {
- try {
- if (! $input->billingFirstName && ! $input->billingAddress) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.billing-address-required'));
- }
- if ($cart->haveStockableItems()) {
- $hasShippingData = $input->shippingFirstName || $input->shippingAddress || $input->useForShipping;
- if (! $hasShippingData) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.shipping-address-required'));
- }
- }
- cart()->setCart($cart);
- Cart::saveAddresses($this->buildAddressPayload($input));
- $cart->refresh();
- $billingAddress = $cart->billing_address;
- $shippingAddress = $cart->shipping_address;
- if (! $billingAddress) {
- throw new OperationFailedException('No billing address was provided');
- }
- Cart::collectTotals();
- if ($cart->haveStockableItems()) {
- \Webkul\Shipping\Facades\Shipping::collectRates();
- }
- return $this->buildAddressOutput($billingAddress, $shippingAddress);
- } catch (\Exception $e) {
- throw new OperationFailedException($e->getMessage(), 0, $e);
- }
- }
- /**
- * Build payload expected by Cart::saveAddresses from GraphQL input.
- */
- private function buildAddressPayload(CheckoutAddressInput $input): array
- {
- $payload = [
- 'billing' => [
- 'first_name' => $input->billingFirstName,
- 'last_name' => $input->billingLastName,
- 'email' => $input->billingEmail,
- 'company_name' => $input->billingCompanyName,
- 'address' => $this->normalizeAddressLines($input->billingAddress),
- 'country' => $input->billingCountry,
- 'state' => $input->billingState,
- 'city' => $input->billingCity,
- 'postcode' => $input->billingPostcode,
- 'phone' => $input->billingPhoneNumber,
- 'use_for_shipping' => (bool) $input->useForShipping,
- ],
- ];
- if (! $input->useForShipping) {
- $payload['shipping'] = [
- 'first_name' => $input->shippingFirstName,
- 'last_name' => $input->shippingLastName,
- 'email' => $input->shippingEmail,
- 'company_name' => $input->shippingCompanyName,
- 'address' => $this->normalizeAddressLines($input->shippingAddress),
- 'country' => $input->shippingCountry,
- 'state' => $input->shippingState,
- 'city' => $input->shippingCity,
- 'postcode' => $input->shippingPostcode,
- 'phone' => $input->shippingPhoneNumber,
- ];
- }
- return $payload;
- }
- /**
- * Convert textarea-like address string into Cart::saveAddresses line array.
- */
- private function normalizeAddressLines(?string $address): array
- {
- if ($address === null) {
- return [];
- }
- $lines = preg_split('/\r\n|\r|\n/', $address) ?: [];
- $lines = array_values(array_filter(array_map('trim', $lines), static fn ($line) => $line !== ''));
- return $lines ?: [''];
- }
- /**
- * Save shipping method for cart.
- */
- private function saveShippingMethod($cart, CheckoutAddressInput $input)
- {
- try {
- cart()->setCart($cart);
- if (! $input->shippingMethod) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.shipping-method-required'));
- }
- \Webkul\Shipping\Facades\Shipping::collectRates();
- if (! \Webkul\Shipping\Facades\Shipping::isMethodCodeExists($input->shippingMethod)) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-shipping-method'));
- }
- if (! \Webkul\Checkout\Facades\Cart::saveShippingMethod($input->shippingMethod)) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.shipping-method-save-failed'));
- }
- \Webkul\Checkout\Facades\Cart::collectTotals();
- return (object) [
- 'id' => (string) $cart->id,
- 'success' => true,
- 'message' => __('bagistoapi::app.graphql.checkout.shipping-method-saved'),
- 'cartToken' => (string) ($cart->guest_cart_token ?? $cart->customer_id),
- 'shippingMethod' => (string) ($cart->shipping_method ?? ''),
- ];
- } catch (\Exception $e) {
- throw new OperationFailedException($e->getMessage(), 0, $e);
- }
- }
- /**
- * Save payment method for cart.
- */
- private function savePaymentMethod($cart, CheckoutAddressInput $input)
- {
- cart()->setCart($cart);
- if (! $input->paymentMethod) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.payment-method-required'));
- }
- $paymentMethodConfig = config('payment_methods.'.$input->paymentMethod);
- if (! $paymentMethodConfig || ! isset($paymentMethodConfig['class'])) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-payment-method'));
- }
- if (! \Webkul\Checkout\Facades\Cart::savePaymentMethod(['method' => $input->paymentMethod])) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.payment-method-save-failed'));
- }
- try {
- \Webkul\Checkout\Facades\Cart::collectTotals();
- $cart = \Webkul\Checkout\Facades\Cart::getCart();
- $response = (object) [
- 'success' => true,
- 'message' => __('bagistoapi::app.graphql.checkout.payment-method-saved'),
- 'cartToken' => (string) ($cart->guest_cart_token ?? $cart->customer_id),
- 'paymentMethod' => (string) ($cart->payment?->method ?? ''),
- ];
- if ($cart->payment) {
- $paymentMethodClass = app($paymentMethodConfig['class']);
- if (method_exists($paymentMethodClass, 'getPaymentUrl') && method_exists($paymentMethodClass, 'getPaymentData')) {
- $paymentData = $paymentMethodClass->getPaymentData($cart);
- if ($input->paymentSuccessUrl) {
- $paymentData['surl'] = $input->paymentSuccessUrl;
- }
- if ($input->paymentFailureUrl) {
- $paymentData['furl'] = $input->paymentFailureUrl;
- }
- if ($input->paymentCancelUrl) {
- $paymentData['curl'] = $input->paymentCancelUrl;
- }
- if ($input->paymentSuccessUrl || $input->paymentFailureUrl || $input->paymentCancelUrl) {
- if (method_exists($paymentMethodClass, 'generateHash')) {
- $paymentData['hash'] = $paymentMethodClass->generateHash(
- $paymentData['txnid'],
- $paymentData['amount'],
- $paymentData['productinfo'],
- $paymentData['firstname'],
- $paymentData['email'],
- $paymentData['udf1']
- );
- }
- }
- $response->paymentGatewayUrl = $paymentMethodClass->getPaymentUrl();
- $response->paymentData = json_encode($paymentData);
- }
- }
- return $response;
- } catch (\Exception $e) {
- throw new OperationFailedException($e->getMessage(), 0, $e);
- }
- }
- /**
- * Create order from cart data.
- *
- * Kept for backwards compatibility — the actual implementation now
- * lives in `PaymentService::initiate()` so the unified payment
- * initiation/replay/callback pipeline can share the same logic.
- */
- private function createOrder($cart, CheckoutAddressInput $input)
- {
- try {
- $paymentService = $this->paymentService ?: app(PaymentService::class);
- $result = $paymentService->initiate($cart, $this->toInitiateInput($input));
- $order = $result['order'];
- return (object) [
- 'id' => $cart->id,
- 'cartToken' => (string) ($cart->guest_cart_token ?? $cart->customer_id),
- 'orderId' => (string) $order->id,
- 'paymentTransactionId' => $result['gatewayOrderId'],
- ];
- } catch (\Exception $e) {
- throw new OperationFailedException($e->getMessage(), 0, $e);
- }
- }
- /**
- * Map the legacy CheckoutAddressInput onto the new PaymentInitiateInput.
- * This keeps `checkoutOrderCreate` byte-for-byte compatible while the
- * actual logic moves to PaymentService.
- */
- private function toInitiateInput(CheckoutAddressInput $input): PaymentInitiateInput
- {
- $payload = new PaymentInitiateInput;
- $payload->expressCheckout = false;
- $payload->paymentMethod = $input->paymentMethod;
- $payload->paymentSuccessUrl = $input->paymentSuccessUrl;
- $payload->paymentFailureUrl = $input->paymentFailureUrl;
- $payload->paymentCancelUrl = $input->paymentCancelUrl;
- $payload->billingFirstName = $input->billingFirstName;
- $payload->billingLastName = $input->billingLastName;
- $payload->billingEmail = $input->billingEmail;
- $payload->billingCompanyName = $input->billingCompanyName;
- $payload->billingAddress = $input->billingAddress;
- $payload->billingCountry = $input->billingCountry;
- $payload->billingState = $input->billingState;
- $payload->billingCity = $input->billingCity;
- $payload->billingPostcode = $input->billingPostcode;
- $payload->billingPhoneNumber = $input->billingPhoneNumber;
- $payload->shippingFirstName = $input->shippingFirstName;
- $payload->shippingLastName = $input->shippingLastName;
- $payload->shippingEmail = $input->shippingEmail;
- $payload->shippingCompanyName = $input->shippingCompanyName;
- $payload->shippingAddress = $input->shippingAddress;
- $payload->shippingCountry = $input->shippingCountry;
- $payload->shippingState = $input->shippingState;
- $payload->shippingCity = $input->shippingCity;
- $payload->shippingPostcode = $input->shippingPostcode;
- $payload->shippingPhoneNumber = $input->shippingPhoneNumber;
- $payload->useForShipping = $input->useForShipping;
- $payload->shippingMethod = $input->shippingMethod;
- return $payload;
- }
- /**
- * Validate order can be created.
- *
- * Retained because tests assert on its error messages; the actual
- * order creation now runs through PaymentService::initiate which
- * re-applies the same checks internally.
- */
- private function validateOrderCreation($cart, CheckoutAddressInput $input): void
- {
- if (! $cart || $cart->items()->count() === 0) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.cart-empty'));
- }
- if (auth()->guard('customer')->check()) {
- $customer = auth()->guard('customer')->user();
- if ($customer && $customer->is_suspended) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.account-suspended'));
- }
- if ($customer && ! $customer->status) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.account-inactive'));
- }
- }
- $minimumOrderAmount = core()->getConfigData('sales.order_settings.minimum_order.minimum_order_amount') ?: 0;
- if (! \Webkul\Checkout\Facades\Cart::haveMinimumOrderAmount()) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.minimum-order-not-met', ['amount' => core()->currency($minimumOrderAmount)]));
- }
- $hasBillingAddress = $input->billingAddress || $cart->billing_address()->exists();
- if (! $hasBillingAddress) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.billing-address-required'));
- }
- $hasShippingAddress = $input->shippingAddress || $input->useForShipping || $cart->shipping_address()->exists();
- if (! $hasShippingAddress && $cart->haveStockableItems()) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.shipping-address-required'));
- }
- $hasEmail = $cart->customer_email || $input->billingEmail || ($cart->billing_address && $cart->billing_address->email);
- if (! $hasEmail) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.email-required'));
- }
- if ($cart->haveStockableItems()) {
- $hasShippingMethod = $input->shippingMethod || $cart->shipping_method;
- if (! $hasShippingMethod) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.shipping-method-required'));
- }
- if (! $cart->selected_shipping_rate) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.invalid-shipping-method'));
- }
- }
- $hasPaymentMethod = $input->paymentMethod || $cart->payment()->exists();
- if (! $hasPaymentMethod) {
- throw new OperationFailedException(__('bagistoapi::app.graphql.checkout.payment-method-required'));
- }
- }
- /**
- * Build CartData from cart model.
- */
- private function buildCartData($cart): CartData
- {
- $cartData = CartData::fromModel($cart);
- return $cartData;
- }
- /**
- * Build CheckoutAddressOutput from cart address models.
- */
- private function buildAddressOutput($billingAddress = null, $shippingAddress = null)
- {
- $output = (object) [
- 'success' => true,
- 'message' => __('bagistoapi::app.graphql.checkout.address-saved'),
- ];
- if ($billingAddress) {
- $output->id = $billingAddress->id;
- $output->cartToken = (string) ($billingAddress->cart->guest_cart_token ?? $billingAddress->cart->customer_id);
- $output->customerId = $billingAddress->cart->customer_id;
- $output->billingFirstName = (string) ($billingAddress->first_name ?? '');
- $output->billingLastName = (string) ($billingAddress->last_name ?? '');
- $output->billingEmail = (string) ($billingAddress->email ?? '');
- $output->billingCompanyName = (string) ($billingAddress->company_name ?? '');
- $output->billingAddress = (string) ($billingAddress->address ?? '');
- $output->billingCountry = (string) ($billingAddress->country ?? '');
- $output->billingState = (string) ($billingAddress->state ?? '');
- $output->billingCity = (string) ($billingAddress->city ?? '');
- $output->billingPostcode = (string) ($billingAddress->postcode ?? '');
- $output->billingPhoneNumber = (string) ($billingAddress->phone ?? '');
- }
- if ($shippingAddress) {
- $output->shippingFirstName = (string) ($shippingAddress->first_name ?? '');
- $output->shippingLastName = (string) ($shippingAddress->last_name ?? '');
- $output->shippingEmail = (string) ($shippingAddress->email ?? '');
- $output->shippingCompanyName = (string) ($shippingAddress->company_name ?? '');
- $output->shippingAddress = (string) ($shippingAddress->address ?? '');
- $output->shippingCountry = (string) ($shippingAddress->country ?? '');
- $output->shippingState = (string) ($shippingAddress->state ?? '');
- $output->shippingCity = (string) ($shippingAddress->city ?? '');
- $output->shippingPostcode = (string) ($shippingAddress->postcode ?? '');
- $output->shippingPhoneNumber = (string) ($shippingAddress->phone ?? '');
- }
- return $output;
- }
- /**
- * Fetch billing and shipping addresses for cart.
- */
- private function fetchAddresses($cart)
- {
- try {
- $output = new \Webkul\BagistoApi\Dto\CheckoutAddressOutput;
- $output->id = $cart->id;
- $output->cartToken = $cart->guest_cart_token ?? $cart->customer_id;
- $output->customerId = $cart->customer_id;
- $billingAddress = $cart->billing_address;
- if ($billingAddress) {
- $output->billingFirstName = $billingAddress->first_name;
- $output->billingLastName = $billingAddress->last_name;
- $output->billingEmail = $billingAddress->email;
- $output->billingCompanyName = $billingAddress->company_name;
- $output->billingAddress = $billingAddress->address;
- $output->billingCountry = $billingAddress->country;
- $output->billingState = $billingAddress->state;
- $output->billingCity = $billingAddress->city;
- $output->billingPostcode = $billingAddress->postcode;
- $output->billingPhoneNumber = $billingAddress->phone;
- }
- $shippingAddress = $cart->shipping_address;
- if ($shippingAddress) {
- $output->shippingFirstName = $shippingAddress->first_name;
- $output->shippingLastName = $shippingAddress->last_name;
- $output->shippingEmail = $shippingAddress->email;
- $output->shippingCompanyName = $shippingAddress->company_name;
- $output->shippingAddress = $shippingAddress->address;
- $output->shippingCountry = $shippingAddress->country;
- $output->shippingState = $shippingAddress->state;
- $output->shippingCity = $shippingAddress->city;
- $output->shippingPostcode = $shippingAddress->postcode;
- $output->shippingPhoneNumber = $shippingAddress->phone;
- }
- $output->success = true;
- $output->message = __('bagistoapi::app.graphql.address.retrieved');
- return $output;
- } catch (\Exception $e) {
- throw new OperationFailedException($e->getMessage(), 0, $e);
- }
- }
- }
|