From efc72cdee0a2e75814dc73fba42859097f28b4df Mon Sep 17 00:00:00 2001 From: leon-zhang-awx Date: Sun, 21 Apr 2024 17:55:36 +0800 Subject: [PATCH] fix: place order failed should remove intent, magento internel bug github.com/magento/magento2/issues/16368 --- Api/ServiceInterface.php | 9 + Model/Service.php | 219 ++++++++++++++---- etc/webapi.xml | 7 + .../address/address-handler.js | 43 ++-- .../method-renderer/express-checkout.js | 45 +++- .../method-renderer/express/googlepay.js | 34 ++- .../payment/method-renderer/express/utils.js | 23 +- 7 files changed, 282 insertions(+), 98 deletions(-) diff --git a/Api/ServiceInterface.php b/Api/ServiceInterface.php index d07e7c4..baf5dc4 100644 --- a/Api/ServiceInterface.php +++ b/Api/ServiceInterface.php @@ -64,7 +64,16 @@ public function redirectUrl(): string; public function expressData(); /** + * Add to cart + * * @return string */ public function addToCart(); + + /** + * Post Address to get method and quote data + * + * @return string + */ + public function postAddress(); } diff --git a/Model/Service.php b/Model/Service.php index 8fa7b71..13d2a5e 100644 --- a/Model/Service.php +++ b/Model/Service.php @@ -25,18 +25,19 @@ use GuzzleHttp\Exception\GuzzleException; use JsonException; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterface; use Magento\Checkout\Api\GuestPaymentInformationManagementInterface; use Magento\Checkout\Api\PaymentInformationManagementInterface; use Magento\Checkout\Helper\Data as CheckoutData; +use Magento\Directory\Model\RegionFactory; use Magento\Framework\App\CacheInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\DataObject; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Payment\Model\MethodInterface; -use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\PaymentInterface; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; @@ -45,6 +46,10 @@ use Magento\Framework\Filter\LocalizedToNormalized; use Magento\Framework\Locale\Resolver; use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Api\ShipmentEstimationInterface; +use Magento\Customer\Api\Data\RegionInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; class Service implements ServiceInterface { @@ -66,7 +71,11 @@ class Service implements ServiceInterface private CartRepositoryInterface $quoteRepository; private QuoteIdMaskFactory $quoteIdMaskFactory; private QuoteIdMaskResourceModel $quoteIdMaskResourceModel; - + private ShipmentEstimationInterface $shipmentEstimation; + private RegionInterfaceFactory $regionInterfaceFactory; + private RegionFactory $regionFactory; + private ShippingInformationManagementInterface $shippingInformationManagement; + private ShippingInformationInterfaceFactory $shippingInformationFactory; /** * Index constructor. * @@ -95,7 +104,12 @@ public function __construct( Resolver $localeResolver, CartRepositoryInterface $quoteRepository, QuoteIdMaskFactory $quoteIdMaskFactory, - QuoteIdMaskResourceModel $quoteIdMaskResourceModel + QuoteIdMaskResourceModel $quoteIdMaskResourceModel, + ShipmentEstimationInterface $shipmentEstimation, + RegionInterfaceFactory $regionInterfaceFactory, + RegionFactory $regionFactory, + ShippingInformationManagementInterface $shippingInformationManagement, + ShippingInformationInterfaceFactory $shippingInformationFactory ) { $this->paymentIntents = $paymentIntents; $this->configuration = $configuration; @@ -114,6 +128,11 @@ public function __construct( $this->quoteRepository = $quoteRepository; $this->quoteIdMaskFactory = $quoteIdMaskFactory; $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + $this->shipmentEstimation = $shipmentEstimation; + $this->regionInterfaceFactory = $regionInterfaceFactory; + $this->regionFactory = $regionFactory; + $this->shippingInformationManagement = $shippingInformationManagement; + $this->shippingInformationFactory = $shippingInformationFactory; } /** * Return URL @@ -178,17 +197,22 @@ public function airwallexGuestPlaceOrder( 'client_secret' => $intent['clientSecret'] ]); } else { - $orderId = $this->guestPaymentInformationManagement->savePaymentInformationAndPlaceOrder( - $cartId, - $email, - $paymentMethod, - $billingAddress - ); - - $response->setData([ - 'response_type' => 'success', - 'order_id' => $orderId - ]); + try { + $orderId = $this->guestPaymentInformationManagement->savePaymentInformationAndPlaceOrder( + $cartId, + $email, + $paymentMethod, + $billingAddress + ); + + $response->setData([ + 'response_type' => 'success', + 'order_id' => $orderId + ]); + } catch (CouldNotSaveException $e) { + $this->paymentIntents->removeIntents(); + throw $e; + } } return $response; @@ -219,16 +243,21 @@ public function airwallexPlaceOrder( 'client_secret' => $intent['clientSecret'] ]); } else { - $orderId = $this->paymentInformationManagement->savePaymentInformationAndPlaceOrder( - $cartId, - $paymentMethod, - $billingAddress - ); - - $response->setData([ - 'response_type' => 'success', - 'order_id' => $orderId - ]); + try { + $orderId = $this->paymentInformationManagement->savePaymentInformationAndPlaceOrder( + $cartId, + $paymentMethod, + $billingAddress + ); + + $response->setData([ + 'response_type' => 'success', + 'order_id' => $orderId + ]); + } catch (CouldNotSaveException $e) { + $this->paymentIntents->removeIntents(); + throw $e; + } } return $response; @@ -240,6 +269,23 @@ public function airwallexPlaceOrder( * @return string */ public function expressData() + { + $res = $this->quoteData(); + $res['settings'] = $this->settings(); + + if ($this->request->getParam("is_product_page") === '1') { + $res['product_type'] = $this->getProductType(); + } + + return json_encode($res); + } + + /** + * Get quote data + * + * @return array + */ + private function quoteData() { $quote = $this->checkoutHelper->getQuote(); $cartId = $quote->getId() ?? 0; @@ -253,7 +299,7 @@ public function expressData() ? $quote->getBillingAddress()->getTaxAmount() : $quote->getShippingAddress()->getTaxAmount(); - $res = [ + return [ 'subtotal' => $quote->getSubtotal() ?? 0, 'grand_total' => $quote->getGrandTotal() ?? 0, 'shipping_amount' => $quote->getShippingAddress()->getShippingAmount() ?? 0, @@ -266,27 +312,35 @@ public function expressData() 'quote_currency_code' => $quote->getQuoteCurrencyCode(), 'email' => $quote->getCustomer()->getEmail(), 'items_qty' => $quote->getItemsQty() ?? 0, - 'settings' => [ - 'mode' => $this->configuration->getMode(), - 'express_seller_name' => $this->configuration->getExpressSellerName(), - 'is_express_active' => $this->configuration->isExpressActive(), - 'is_express_phone_required' => $this->configuration->isExpressPhoneRequired(), - 'is_express_capture_enabled' => $this->configuration->isExpressCaptureEnabled(), - 'express_style' => $this->configuration->getExpressStyle(), - 'express_button_sort' => $this->configuration->getExpressButtonSort(), - 'country_code' => $this->configuration->getCountryCode(), - 'store_code' => $this->storeManager->getStore()->getCode(), - 'display_area' => $this->configuration->expressDisplayArea() - ] ]; + } - if ($this->request->getParam("is_product_page") === '1') { - $res['product_type'] = $this->getProductType(); - } - - return json_encode($res); + /** + * Get admin settings + * + * @return array + */ + private function settings() + { + return [ + 'mode' => $this->configuration->getMode(), + 'express_seller_name' => $this->configuration->getExpressSellerName(), + 'is_express_active' => $this->configuration->isExpressActive(), + 'is_express_phone_required' => $this->configuration->isExpressPhoneRequired(), + 'is_express_capture_enabled' => $this->configuration->isExpressCaptureEnabled(), + 'express_style' => $this->configuration->getExpressStyle(), + 'express_button_sort' => $this->configuration->getExpressButtonSort(), + 'country_code' => $this->configuration->getCountryCode(), + 'store_code' => $this->storeManager->getStore()->getCode(), + 'display_area' => $this->configuration->expressDisplayArea() + ]; } + /** + * Get product type from product_id from request + * + * @return string + */ private function getProductType() { $product = $this->productRepository->getById( @@ -299,6 +353,8 @@ private function getProductType() } /** + * Add product when click pay in product page + * * @return string */ public function addToCart() @@ -347,7 +403,6 @@ public function addToCart() $quote->collectTotals(); $this->quoteRepository->save($quote); - try { $maskCartId = $this->quoteIdToMaskedQuoteId->execute($quote->getId()); } catch (NoSuchEntityException $e) { @@ -367,4 +422,80 @@ public function addToCart() throw new CouldNotSaveException(__($e->getMessage()), $e); } } + + /** + * Post Address to get method and quote data + * + * @return string + */ + public function postAddress(): string + { + $countryId = $this->request->getParam('country_id'); + $regionName = $this->request->getParam('region'); + + $regionId = $this->regionFactory->create()->loadByName($regionName, $countryId)->getRegionId(); + $region = $this->regionInterfaceFactory->create()->setRegion($regionName)->setRegionId($regionId); + + $quote = $this->checkoutHelper->getQuote(); + + $cartId = $quote->getId(); + if (!is_numeric($cartId)) { + $cartId = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id')->getQuoteId(); + } + + $address = $quote->getBillingAddress(); + $address->setCountryId($this->request->getParam('country_id')); + $address->setCity($this->request->getParam('city')); + $address->setRegion($region->getRegion()); + $address->setPostcode($this->request->getParam('postcode')); + $methods = $this->shipmentEstimation->estimateByExtendedAddress($cartId, $address); + + if (!count($methods)) { + throw new \Exception(__('There are no available shipping method found.')); + } + + $selectedMethod = $methods[0]; + foreach ($methods as $method) { + if ($method->getMethodCode() === $this->request->getParam('methodId')) { + $selectedMethod = $method; + break; + } + } + + $shippingInformation = $this->shippingInformationFactory->create([ + 'data' => [ + ShippingInformationInterface::SHIPPING_ADDRESS => $address, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => $selectedMethod->getCarrierCode(), + ShippingInformationInterface::SHIPPING_METHOD_CODE => $selectedMethod->getMethodCode(), + ], + ]); + $this->shippingInformationManagement->saveAddressInformation($cartId, $shippingInformation); + + $res = []; + foreach ($methods as $method) { + if ($method->getAvailable()) { + $res['methods'][]=$this->formatShippingMethod($method); + } + } + $res['selected_method'] = $this->formatShippingMethod($selectedMethod); + $res['quote_data'] = $this->quoteData(); + $res['region_id'] = $regionId; // we need this because magento internal bug + return json_encode($res); + } + + /** + * Format shipping method + * + * @return array + */ + private function formatShippingMethod($method) + { + return [ + 'carrier_code' => $method->getCarrierCode(), + 'carrier_title' => $method->getCarrierTitle(), + 'amount' => $method->getAmount(), + 'method_code' => $method->getMethodCode(), + 'method_title' => $method->getMethodTitle(), + ]; + } } diff --git a/etc/webapi.xml b/etc/webapi.xml index c4c846d..2f14a07 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -53,4 +53,11 @@ + + + + + + + diff --git a/view/frontend/web/js/view/payment/method-renderer/address/address-handler.js b/view/frontend/web/js/view/payment/method-renderer/address/address-handler.js index e810d77..ff9c150 100644 --- a/view/frontend/web/js/view/payment/method-renderer/address/address-handler.js +++ b/view/frontend/web/js/view/payment/method-renderer/address/address-handler.js @@ -8,6 +8,9 @@ define([ 'use strict'; return { + selectedMethod: {}, + regionId: "", + postBillingAddress(payload, isLoggedIn, cartId) { let url = 'rest/V1/carts/mine/billing-address'; if (!isLoggedIn) { @@ -61,18 +64,13 @@ define([ } }, - constructAddressInformationFromGoogle(isRequireShippingAddress, data, methods) { + constructAddressInformationFromGoogle(data) { let billingAddress = {} if (data.paymentMethodData) { let addr = data.paymentMethodData.info.billingAddress billingAddress = this.getBillingAddressFromGoogle(addr) } - let selectedMethod = methods.find(item => item.carrier_code === data.shippingOptionData.id) || methods[0]; - if (!selectedMethod) { - selectedMethod = {} - } - let firstname = '', lastname = '' if (data.shippingAddress && data.shippingAddress.name) { let names = data.shippingAddress.name.split(' ') || []; @@ -84,24 +82,23 @@ define([ "addressInformation": { "shipping_address": {}, "billing_address": billingAddress, - "shipping_method_code": selectedMethod.method_code, - "shipping_carrier_code": selectedMethod.carrier_code, + "shipping_method_code": this.selectedMethod.method_code, + "shipping_carrier_code": this.selectedMethod.carrier_code, "extension_attributes": {} } } - if (isRequireShippingAddress) { - information.addressInformation.shipping_address = { - "countryId": data.shippingAddress.countryCode, - "region": data.shippingAddress.administrativeArea, - "street": [data.shippingAddress.address1 + data.shippingAddress.address2 + data.shippingAddress.address3], - "telephone": data.shippingAddress.phoneNumber, - "postcode": data.shippingAddress.postalCode, - "city": data.shippingAddress.locality, - firstname, - lastname, - } + information.addressInformation.shipping_address = { + "countryId": data.shippingAddress.countryCode, + "regionId": this.regionId, + "region": data.shippingAddress.administrativeArea, + "street": [data.shippingAddress.address1 + data.shippingAddress.address2 + data.shippingAddress.address3], + "telephone": data.shippingAddress.phoneNumber, + "postcode": data.shippingAddress.postalCode, + "city": data.shippingAddress.locality, + firstname, + lastname, } - return {information, selectedMethod} + return information }, setIntentConfirmBillingAddressFromGoogle(data) { @@ -140,15 +137,15 @@ define([ formatShippingMethodsToGoogle(methods, selectedMethod) { const shippingOptions = methods.map(addr => { return { - id: addr.carrier_code, - label: addr.method_code, + id: addr.method_code, + label: addr.method_title, description: addr.carrier_title, }; }); return { shippingOptions, - defaultSelectedOptionId: selectedMethod.carrier_code + defaultSelectedOptionId: selectedMethod.method_code }; }, } diff --git a/view/frontend/web/js/view/payment/method-renderer/express-checkout.js b/view/frontend/web/js/view/payment/method-renderer/express-checkout.js index a428db4..8f5ae21 100644 --- a/view/frontend/web/js/view/payment/method-renderer/express-checkout.js +++ b/view/frontend/web/js/view/payment/method-renderer/express-checkout.js @@ -55,12 +55,43 @@ define( } const resp = await storage.get(url, undefined, 'application/json', {}); let obj = JSON.parse(resp) - this.expressData = obj - this.paymentConfig = Object.assign(this.paymentConfig, obj.settings) - utils.expressData = obj - utils.paymentConfig = this.paymentConfig - googlepay.expressData = obj - googlepay.paymentConfig = this.paymentConfig + this.updateExpressData(obj) + this.updatePaymentConfig(obj.settings) + }, + + async postAddress(address, methodId) { + let url = urlBuilder.build('rest/V1/airwallex/payments/post-address'); + if (!utils.isLoggedIn()) { + + } + let postOptions = utils.postOptions(address, url) + postOptions.data.append('methodId', methodId) + let resp = await $.ajax(postOptions) + + let obj = JSON.parse(resp) + this.updateExpressData(obj.quote_data) + this.updateMethods(obj.methods, obj.selected_method) + addressHandler.regionId = obj.region_id + return obj + }, + + updateExpressData(expressData) { + Object.assign(this.expressData, expressData) + Object.assign(utils.expressData, expressData) + Object.assign(googlepay.expressData, expressData) + }, + + updatePaymentConfig(paymentConfig) { + this.paymentConfig = paymentConfig + utils.paymentConfig = paymentConfig + googlepay.paymentConfig = paymentConfig + }, + + updateMethods(methods, selectedMethod) { + addressHandler.methods = methods + googlepay.methods = methods + addressHandler.selectedMethod = selectedMethod + googlepay.selectedMethod = selectedMethod }, initMinicartClickEvents() { @@ -198,6 +229,8 @@ define( ); resolve(endResult); } catch (e) { + Airwallex.destroyElement('googlePayButton'); + googlepay.create(this) reject(e); } })).then(response => { diff --git a/view/frontend/web/js/view/payment/method-renderer/express/googlepay.js b/view/frontend/web/js/view/payment/method-renderer/express/googlepay.js index d40faef..e9f0ce9 100644 --- a/view/frontend/web/js/view/payment/method-renderer/express/googlepay.js +++ b/view/frontend/web/js/view/payment/method-renderer/express/googlepay.js @@ -17,6 +17,7 @@ define([ paymentConfig: {}, from: '', methods: [], + selectedMethod: {}, create(that) { this.googlepay = Airwallex.createElement('googlePayButton', this.getRequestOptions()) @@ -31,6 +32,7 @@ define([ attachEvents(that) { let updateQuoteByShipment = async (event) => { + console.log(event); if (utils.isProductPage() && utils.isSetActiveInProductPage()) { try { let res = await $.ajax(utils.addToCartOptions()) @@ -41,26 +43,19 @@ define([ customerData.invalidate(['cart']); customerData.reload(['cart'], true); } - // 1. estimateShippingMethods - if (utils.isRequireShippingAddress()) { - let addr = addressHandler.getIntermediateShippingAddressFromGoogle(event.detail.intermediatePaymentData.shippingAddress) - this.methods = await addressHandler.estimateShippingMethods(addr, utils.isLoggedIn(), utils.getCartId()) - } - // 2. postShippingInformation - let {information, selectedMethod} = addressHandler.constructAddressInformationFromGoogle( - utils.isRequireShippingAddress(), event.detail.intermediatePaymentData, this.methods - ) - await addressHandler.postShippingInformation(information, utils.isLoggedIn(), utils.getCartId()) + let addr = addressHandler.getIntermediateShippingAddressFromGoogle(event.detail.intermediatePaymentData.shippingAddress) - // 3. update quote - await that.fetchExpressData() - - let options = this.getRequestOptions(); - if (utils.isRequireShippingOption()) { - options.shippingOptionParameters = addressHandler.formatShippingMethodsToGoogle(this.methods, selectedMethod) + try { + await that.postAddress(addr, event.detail.intermediatePaymentData.shippingOptionData.id); + let options = this.getRequestOptions(); + if (utils.isRequireShippingOption()) { + options.shippingOptionParameters = addressHandler.formatShippingMethodsToGoogle(this.methods, this.selectedMethod) + } + this.googlepay.update(options); + } catch (e) { + utils.error(e) } - this.googlepay.update(options); } this.googlepay.on('shippingAddressChange', updateQuoteByShipment); @@ -68,12 +63,13 @@ define([ this.googlepay.on('shippingMethodChange', updateQuoteByShipment); this.googlepay.on('authorized', async (event) => { + console.log(event) that.setGuestEmail(event.detail.paymentData.email) if (utils.isRequireShippingAddress()) { // this time google provide full shipping address, we should post to magento - let {information} = addressHandler.constructAddressInformationFromGoogle( - utils.isRequireShippingAddress(), event.detail.paymentData, this.methods + let information = addressHandler.constructAddressInformationFromGoogle( + event.detail.paymentData ) await addressHandler.postShippingInformation(information, utils.isLoggedIn(), utils.getCartId()) } else { diff --git a/view/frontend/web/js/view/payment/method-renderer/express/utils.js b/view/frontend/web/js/view/payment/method-renderer/express/utils.js index 6da4a7b..68b3486 100644 --- a/view/frontend/web/js/view/payment/method-renderer/express/utils.js +++ b/view/frontend/web/js/view/payment/method-renderer/express/utils.js @@ -108,15 +108,20 @@ define([ return !this.expressData.is_virtual }, - addToCartOptions() { + postOptions(data, url) { let formData = new FormData(); - let serializedArray = $(this.productFormSelector).serializeArray(); - $.each(serializedArray, function (index, field) { - formData.append(field.name, field.value); - }); + if (Array.isArray(data)) { + $.each(data, function (index, field) { + formData.append(field.name, field.value); + }); + } else { + for (let k in data) { + formData.append(k, data[k]); + } + } return { - url: urlBuilder.build('rest/V1/airwallex/payments/add-to-cart'), + url, data: formData, processData: false, contentType: false, @@ -124,6 +129,12 @@ define([ }; }, + addToCartOptions() { + let arr = $(this.productFormSelector).serializeArray(); + let url = urlBuilder.build('rest/V1/airwallex/payments/add-to-cart') + return this.postOptions(arr, url) + }, + getCartId() { return this.isLoggedIn() ? this.expressData.cart_id : this.expressData.mask_cart_id },