From f9e27c13ba247299fae9f012b4d9821bb294ac53 Mon Sep 17 00:00:00 2001 From: Sagnik Mitra Date: Thu, 12 Sep 2024 17:40:54 +0530 Subject: [PATCH 1/2] feat(applepay): calculate tax amount dynamically while changing shipping address in apple pay sdk Calculate tax amount and shipping cost dynamically while changing shipping address in apple pay sdk and update the new amount. --- src/Components/SavedMethods.res | 2 +- src/Payments/ApplePay.res | 2 +- src/Payments/PaymentMethodsRecord.res | 3 + src/Types/ApplePayTypes.res | 9 +++ src/Utilities/ApplePayHelpers.res | 83 ++++++++++++++++++++++- src/Utilities/PaymentHelpers.res | 78 +++++++++++++++++++++ src/Utilities/TaxCalculation.res | 27 ++++++++ src/orca-loader/Elements.res | 6 +- src/orca-loader/PaymentSessionMethods.res | 2 + src/orca-log-catcher/OrcaLogger.res | 2 + 10 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 src/Utilities/TaxCalculation.res diff --git a/src/Components/SavedMethods.res b/src/Components/SavedMethods.res index 6e65020b9..ab0324f1e 100644 --- a/src/Components/SavedMethods.res +++ b/src/Components/SavedMethods.res @@ -160,7 +160,7 @@ let make = ( | Some("apple_pay") => switch applePayToken { | ApplePayTokenOptional(optToken) => - ApplePayHelpers.handleApplePayButtonClicked(~sessionObj=optToken, ~componentName) + ApplePayHelpers.handleApplePayButtonClicked(~sessionObj=optToken, ~componentName, ~paymentMethodListValue) | _ => // TODO - To be replaced with proper error message intent( diff --git a/src/Payments/ApplePay.res b/src/Payments/ApplePay.res index 840fc9736..afbc951e0 100644 --- a/src/Payments/ApplePay.res +++ b/src/Payments/ApplePay.res @@ -241,7 +241,7 @@ let make = (~sessionObj: option, ~walletOptions, ~paymentType: CardTheme ~isManualRetryEnabled, ) } else { - ApplePayHelpers.handleApplePayButtonClicked(~sessionObj, ~componentName) + ApplePayHelpers.handleApplePayButtonClicked(~sessionObj, ~componentName, ~paymentMethodListValue) } } else { let bodyDict = PaymentBody.applePayRedirectBody(~connectors) diff --git a/src/Payments/PaymentMethodsRecord.res b/src/Payments/PaymentMethodsRecord.res index f72b3b3f0..5803f6aba 100644 --- a/src/Payments/PaymentMethodsRecord.res +++ b/src/Payments/PaymentMethodsRecord.res @@ -804,6 +804,7 @@ type paymentMethodList = { payment_type: payment_type, merchant_name: string, collect_billing_details_from_wallets: bool, + is_tax_calculation_enabled: bool, } let defaultPaymentMethodType = { @@ -826,6 +827,7 @@ let defaultList = { payment_type: NONE, merchant_name: "", collect_billing_details_from_wallets: true, + is_tax_calculation_enabled: false, } let getPaymentExperienceType = str => { @@ -1025,6 +1027,7 @@ let itemToObjMapper = dict => { "collect_billing_details_from_wallets", true, ), + is_tax_calculation_enabled: getBool(dict,"is_tax_calculation_enabled",false) } } diff --git a/src/Types/ApplePayTypes.res b/src/Types/ApplePayTypes.res index d7a4040eb..c58af9d75 100644 --- a/src/Types/ApplePayTypes.res +++ b/src/Types/ApplePayTypes.res @@ -25,6 +25,13 @@ type shippingContact = { type paymentResult = {token: JSON.t, billingContact: JSON.t, shippingContact: JSON.t} type event = {validationURL: string, payment: paymentResult} +type lineItem = { + label: string, + amount: string, + @optional \"type": string, +} +type shippingAddressChangeEvent = {shippingContact: JSON.t} +type updatedOrderDetails = {newTotal: lineItem, newLineItems: array} type innerSession type session = { begin: unit => unit, @@ -34,6 +41,8 @@ type session = { mutable onvalidatemerchant: event => unit, completeMerchantValidation: JSON.t => unit, mutable onpaymentauthorized: event => unit, + mutable onshippingcontactselected: shippingAddressChangeEvent => unit, + completeShippingContactSelection: updatedOrderDetails => unit, completePayment: JSON.t => unit, \"STATUS_SUCCESS": string, \"STATUS_FAILURE": string, diff --git a/src/Utilities/ApplePayHelpers.res b/src/Utilities/ApplePayHelpers.res index d23f0ae2c..fb071b68d 100644 --- a/src/Utilities/ApplePayHelpers.res +++ b/src/Utilities/ApplePayHelpers.res @@ -1,5 +1,6 @@ open ApplePayTypes open Utils +open TaxCalculation let processPayment = ( ~bodyArr, @@ -75,7 +76,11 @@ let startApplePaySession = ( ~applePayEvent: option=None, ~callBackFunc, ~resolvePromise: option unit>=None, + ~clientSecret, + ~publishableKey, + ~isTaxCalculationEnabled=false, ) => { + open Promise let ssn = applePaySession(3, paymentRequest) switch applePaySessionRef.contents->Nullable.toOption { | Some(session) => @@ -100,6 +105,69 @@ let startApplePaySession = ( ssn.completeMerchantValidation(merchantSession) } + ssn.onshippingcontactselected = shippingAddressChangeEvent => { + if isTaxCalculationEnabled { + let newShippingContact = + shippingAddressChangeEvent.shippingContact + ->getDictFromJson + ->shippingContactItemToObjMapper + let newShippingAddress = + [ + ("state", newShippingContact.locality), + ("country", newShippingContact.countryCode), + ("zip", newShippingContact.postalCode), + ] + ->Array.map(((key, val)) => (key, val->Js.Json.string)) + ->getJsonFromArrayOfJson + + let paymentMethodType = "apple_pay"->JSON.Encode.string + + calculateTax( + ~shippingAddress=[("address", newShippingAddress)]->getJsonFromArrayOfJson, + ~logger, + ~publishableKey, + ~clientSecret, + ~paymentMethodType, + ) + ->then(response => { + let taxCalculationResponse = response->getDictFromJson->taxResponseToObjMapper + let (netAmount, ordertaxAmount, shippingCost) = ( + taxCalculationResponse.net_amount, + taxCalculationResponse.order_tax_amount, + taxCalculationResponse.shipping_cost, + ) + let newTotal: lineItem = { + label: "Net Amount", + amount: netAmount->Int.toString, + \"type": "final", + } + let newLineItems: array = [ + { + label: "Bag Subtotal", + amount: (netAmount - ordertaxAmount - shippingCost)->Int.toString, + \"type": "final", + }, + { + label: "Order Tax Amount", + amount: ordertaxAmount->Int.toString, + \"type": "final", + }, + { + label: "Shipping Cost", + amount: shippingCost->Int.toString, + \"type": "final", + }, + ] + let updatedOrderDetails: updatedOrderDetails = { + newTotal, + newLineItems, + } + ssn.completeShippingContactSelection(updatedOrderDetails)->resolve + }) + ->ignore + } + } + ssn.onpaymentauthorized = event => { ssn.completePayment({"status": ssn.\"STATUS_SUCCESS"}->Identity.anyTypeToJson) applePaySessionRef := Nullable.null @@ -231,11 +299,19 @@ let useHandleApplePayResponse = ( )) } -let handleApplePayButtonClicked = (~sessionObj, ~componentName) => { +let handleApplePayButtonClicked = ( + ~sessionObj, + ~componentName, + ~paymentMethodListValue: PaymentMethodsRecord.paymentMethodList, +) => { let paymentRequest = ApplePayTypes.getPaymentRequestFromSession(~sessionObj, ~componentName) let message = [ ("applePayButtonClicked", true->JSON.Encode.bool), ("applePayPaymentRequest", paymentRequest), + ( + "isTaxCalculationEnabled", + paymentMethodListValue.is_tax_calculation_enabled->JSON.Encode.bool, + ), ] messageParentWindow(message) } @@ -245,13 +321,16 @@ let useSubmitCallback = (~isWallet, ~sessionObj, ~componentName) => { let areRequiredFieldsEmpty = Recoil.useRecoilValueFromAtom(RecoilAtoms.areRequiredFieldsEmpty) let options = Recoil.useRecoilValueFromAtom(RecoilAtoms.optionAtom) let {localeString} = Recoil.useRecoilValueFromAtom(RecoilAtoms.configAtom) + let paymentMethodListValue = Recoil.useRecoilValueFromAtom(PaymentUtils.paymentMethodListValue) React.useCallback((ev: Window.event) => { if !isWallet { let json = ev.data->safeParse let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper if confirm.doSubmit && areRequiredFieldsValid && !areRequiredFieldsEmpty { - options.readOnly ? () : handleApplePayButtonClicked(~sessionObj, ~componentName) + options.readOnly + ? () + : handleApplePayButtonClicked(~sessionObj, ~componentName, ~paymentMethodListValue) } else if areRequiredFieldsEmpty { postFailedSubmitResponse( ~errortype="validation_error", diff --git a/src/Utilities/PaymentHelpers.res b/src/Utilities/PaymentHelpers.res index e0c1e47fd..665ffb1c1 100644 --- a/src/Utilities/PaymentHelpers.res +++ b/src/Utilities/PaymentHelpers.res @@ -1972,3 +1972,81 @@ let deletePaymentMethod = (~ephemeralKey, ~paymentMethodId, ~logger, ~customPodU JSON.Encode.null->resolve }) } + +let calculateTax = ( + ~apiKey, + ~paymentId, + ~clientSecret, + ~paymentMethodType, + ~shippingAddress, + ~logger, + ~customPodUri, +) => { + open Promise + let endpoint = ApiEndpoint.getApiEndPoint() + let headers = [("Content-Type", "application/json"), ("api-key", apiKey)] + let uri = `${endpoint}/payments/${paymentId}/calculate_tax` + let body = [ + ("client_secret", clientSecret), + ("shipping", shippingAddress), + ("payment_method_type", paymentMethodType), + ] + logApi( + ~optLogger=Some(logger), + ~url=uri, + ~apiLogType=Request, + ~eventName=EXTERNAL_TAX_CALCULATION, + ~logType=INFO, + ~logCategory=API, + ) + fetchApi( + uri, + ~method=#POST, + ~headers=headers->ApiEndpoint.addCustomPodHeader(~customPodUri), + ~bodyStr=body->getJsonFromArrayOfJson->JSON.stringify, + ) + ->then(resp => { + let statusCode = resp->Fetch.Response.status->Int.toString + if statusCode->String.charAt(0) !== "2" { + resp + ->Fetch.Response.json + ->then(data => { + logApi( + ~optLogger=Some(logger), + ~url=uri, + ~data, + ~statusCode, + ~apiLogType=Err, + ~eventName=EXTERNAL_TAX_CALCULATION, + ~logType=ERROR, + ~logCategory=API, + ) + JSON.Encode.null->resolve + }) + } else { + logApi( + ~optLogger=Some(logger), + ~url=uri, + ~statusCode, + ~apiLogType=Response, + ~eventName=EXTERNAL_TAX_CALCULATION, + ~logType=INFO, + ~logCategory=API, + ) + resp->Fetch.Response.json + } + }) + ->catch(err => { + let exceptionMessage = err->formatException + logApi( + ~optLogger=Some(logger), + ~url=uri, + ~apiLogType=NoResponse, + ~eventName=EXTERNAL_TAX_CALCULATION, + ~logType=ERROR, + ~logCategory=API, + ~data=exceptionMessage, + ) + JSON.Encode.null->resolve + }) +} diff --git a/src/Utilities/TaxCalculation.res b/src/Utilities/TaxCalculation.res new file mode 100644 index 000000000..cab4ef996 --- /dev/null +++ b/src/Utilities/TaxCalculation.res @@ -0,0 +1,27 @@ +open Utils + +type calculateTaxResponse = { + payment_id: string, + net_amount: int, + order_tax_amount: int, + shipping_cost: int, +} + +let taxResponseToObjMapper = dict => { + payment_id: dict->getString("payment_id", ""), + net_amount: dict->getInt("net_amount", 0), + order_tax_amount: dict->getInt("order_tax_amount", 0), + shipping_cost: dict->getInt("shipping_cost", 0), +} + +let calculateTax = (~shippingAddress, ~logger, ~clientSecret, ~publishableKey, ~paymentMethodType) => { + PaymentHelpers.calculateTax( + ~clientSecret=clientSecret->Js.Json.string, + ~apiKey=publishableKey, + ~paymentId=clientSecret->getPaymentId, + ~paymentMethodType, + ~shippingAddress, + ~logger, + ~customPodUri="", + ) +} diff --git a/src/orca-loader/Elements.res b/src/orca-loader/Elements.res index 96bb201cd..88c16f66b 100644 --- a/src/orca-loader/Elements.res +++ b/src/orca-loader/Elements.res @@ -775,8 +775,9 @@ let make = ( switch ( dict->Dict.get("applePayButtonClicked"), dict->Dict.get("applePayPaymentRequest"), + dict->Dict.get("isTaxCalculationEnabled")->Option.flatMap(JSON.Decode.bool)->Option.getOr(false), ) { - | (Some(val), Some(paymentRequest)) => + | (Some(val), Some(paymentRequest), isTaxCalculationEnabled) => if val->JSON.Decode.bool->Option.getOr(false) { let isDelayedSessionToken = applePayPresent @@ -810,6 +811,9 @@ let make = ( ~logger, ~applePayEvent=Some(applePayEvent), ~callBackFunc, + ~clientSecret, + ~publishableKey, + ~isTaxCalculationEnabled, ) } } else { diff --git a/src/orca-loader/PaymentSessionMethods.res b/src/orca-loader/PaymentSessionMethods.res index 39633ef0d..0130a64d7 100644 --- a/src/orca-loader/PaymentSessionMethods.res +++ b/src/orca-loader/PaymentSessionMethods.res @@ -192,6 +192,8 @@ let getCustomerSavedPaymentMethods = ( ~applePayPresent=Some(applePayTokenRef.contents), ~logger, ~callBackFunc=processPayment, + ~clientSecret, + ~publishableKey, ) } diff --git a/src/orca-log-catcher/OrcaLogger.res b/src/orca-log-catcher/OrcaLogger.res index fa0ca071b..bdcc4e4cb 100644 --- a/src/orca-log-catcher/OrcaLogger.res +++ b/src/orca-log-catcher/OrcaLogger.res @@ -83,6 +83,7 @@ type eventName = | DELETE_SAVED_PAYMENT_METHOD | DELETE_PAYMENT_METHODS_CALL_INIT | DELETE_PAYMENT_METHODS_CALL + | EXTERNAL_TAX_CALCULATION let eventNameToStrMapper = eventName => { switch eventName { @@ -166,6 +167,7 @@ let eventNameToStrMapper = eventName => { | DELETE_SAVED_PAYMENT_METHOD => "DELETE_SAVED_PAYMENT_METHOD" | DELETE_PAYMENT_METHODS_CALL_INIT => "DELETE_PAYMENT_METHODS_CALL_INIT" | DELETE_PAYMENT_METHODS_CALL => "DELETE_PAYMENT_METHODS_CALL" + | EXTERNAL_TAX_CALCULATION => "EXTERNAL_TAX_CALCULATION" } } From 421ba6bb2426dddb7f2ac197e5acd3a362ef2dd3 Mon Sep 17 00:00:00 2001 From: Sagnik Mitra Date: Fri, 13 Sep 2024 13:56:22 +0530 Subject: [PATCH 2/2] refactor: refactor refactor --- src/Types/ApplePayTypes.res | 2 +- src/Utilities/ApplePayHelpers.res | 16 +++++++--------- src/Utilities/TaxCalculation.res | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Types/ApplePayTypes.res b/src/Types/ApplePayTypes.res index c58af9d75..29b00e88c 100644 --- a/src/Types/ApplePayTypes.res +++ b/src/Types/ApplePayTypes.res @@ -28,7 +28,7 @@ type event = {validationURL: string, payment: paymentResult} type lineItem = { label: string, amount: string, - @optional \"type": string, + \"type": string, } type shippingAddressChangeEvent = {shippingContact: JSON.t} type updatedOrderDetails = {newTotal: lineItem, newLineItems: array} diff --git a/src/Utilities/ApplePayHelpers.res b/src/Utilities/ApplePayHelpers.res index fb071b68d..b481b55f2 100644 --- a/src/Utilities/ApplePayHelpers.res +++ b/src/Utilities/ApplePayHelpers.res @@ -113,12 +113,10 @@ let startApplePaySession = ( ->shippingContactItemToObjMapper let newShippingAddress = [ - ("state", newShippingContact.locality), - ("country", newShippingContact.countryCode), - ("zip", newShippingContact.postalCode), - ] - ->Array.map(((key, val)) => (key, val->Js.Json.string)) - ->getJsonFromArrayOfJson + ("state", newShippingContact.locality->JSON.Encode.string), + ("country", newShippingContact.countryCode->JSON.Encode.string), + ("zip", newShippingContact.postalCode->JSON.Encode.string), + ]->getJsonFromArrayOfJson let paymentMethodType = "apple_pay"->JSON.Encode.string @@ -328,9 +326,9 @@ let useSubmitCallback = (~isWallet, ~sessionObj, ~componentName) => { let json = ev.data->safeParse let confirm = json->getDictFromJson->ConfirmType.itemToObjMapper if confirm.doSubmit && areRequiredFieldsValid && !areRequiredFieldsEmpty { - options.readOnly - ? () - : handleApplePayButtonClicked(~sessionObj, ~componentName, ~paymentMethodListValue) + if !options.readOnly { + handleApplePayButtonClicked(~sessionObj, ~componentName, ~paymentMethodListValue) + } } else if areRequiredFieldsEmpty { postFailedSubmitResponse( ~errortype="validation_error", diff --git a/src/Utilities/TaxCalculation.res b/src/Utilities/TaxCalculation.res index cab4ef996..c48b1ae5b 100644 --- a/src/Utilities/TaxCalculation.res +++ b/src/Utilities/TaxCalculation.res @@ -16,7 +16,7 @@ let taxResponseToObjMapper = dict => { let calculateTax = (~shippingAddress, ~logger, ~clientSecret, ~publishableKey, ~paymentMethodType) => { PaymentHelpers.calculateTax( - ~clientSecret=clientSecret->Js.Json.string, + ~clientSecret=clientSecret->JSON.Encode.string, ~apiKey=publishableKey, ~paymentId=clientSecret->getPaymentId, ~paymentMethodType,