diff --git a/src/constants/query-strings.js b/src/constants/query-strings.js index 1857643be..034e0a6ef 100644 --- a/src/constants/query-strings.js +++ b/src/constants/query-strings.js @@ -48,6 +48,7 @@ const queryStrings = { FROM_HERITAGE: "fromheritage", PHONE_NUMBER: "phonenumber", OFFER_QUOTE: "offerquote", + PIA_AUTH_AMOUNT_ADJUST: "piaauthamountadjust", }; export { queryStrings }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 742e1c461..00875bb6f 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -118,6 +118,7 @@ const storeActions = { SAVE_CCTOKEN: "saveCCToken", SAVE_PAYPAL_TOKEN: "savePaypalToken", SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount", + SAVE_AUTHORIZATION_AMOUNT: "saveAuthorizationAmount", SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn", SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING: "saveIsRecalAcknowledgedForScheduling", SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 8437a395e..d60bca6ea 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -3,6 +3,7 @@ const storeMutations = { UPDATE_CCTOKEN: "updateCCToken", UPDATE_PAYPAL_TOKEN: "updatePaypalToken", UPDATE_NEXTGEN_SETTLED_AMOUNT: "updateNextGenSettledAmount", + UPDATE_AUTHORIZATION_AMOUNT: "updateAuthorizationAmount", // VEHICLE MUTATIONS UPDATE_YEAR: "updateYear", diff --git a/src/helpers/pricing-helper.js b/src/helpers/pricing-helper.js index fff87d82f..aab72b97e 100644 --- a/src/helpers/pricing-helper.js +++ b/src/helpers/pricing-helper.js @@ -3,6 +3,10 @@ import { storeActions } from "@/constants/store-actions"; import baseMixin from "@/mixins/base-mixin.js"; import { deepClone } from "@/helpers/object-helper"; import { partNumberStrings } from "@/constants/part-number-strings"; +import { applicationConfig } from "@/constants/application-config"; +import { queryStrings } from "@/constants/query-strings"; +import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash"; export function getDisplayAmountDue(lineItemsObject, includeTax = true) { return getAmountDue(lineItemsObject, includeTax).toLocaleString("en-US", { @@ -56,6 +60,35 @@ export function getAmountDue(lineItemsObject, includeTax = true) { return ((amountDue * 100) / 100).toFixed(2); } +function getPiaAuthAmountAdjustment() { + if (applicationConfig.CURRENT_ENVIRONMENT === "Prod") { + return 0; + } + + const adjustParam = + peekQueryFromStash(queryStrings.PIA_AUTH_AMOUNT_ADJUST) ?? + getQuerystringParameter(queryStrings.PIA_AUTH_AMOUNT_ADJUST); + + if (adjustParam == null || adjustParam === "") { + return 0; + } + + const adjustment = parseFloat(adjustParam); + return Number.isFinite(adjustment) ? adjustment : 0; +} + +export function getPiaAuthorizationAmount(lineItemsObject) { + const amountDue = parseFloat(getAmountDue(lineItemsObject)); + const adjustment = getPiaAuthAmountAdjustment(); + + if (adjustment === 0) { + return amountDue.toFixed(2); + } + + const adjustedAmount = Math.max(0.01, amountDue + adjustment); + return adjustedAmount.toFixed(2); +} + export function getSubTotal(lineItemsObject) { // this is amountDue without sales tax return getAmountDue(lineItemsObject, false); diff --git a/src/helpers/pricing-helper.spec.js b/src/helpers/pricing-helper.spec.js index 8a49bb5ae..2d136fbe4 100644 --- a/src/helpers/pricing-helper.spec.js +++ b/src/helpers/pricing-helper.spec.js @@ -1,9 +1,22 @@ import { getDisplayAmountDue, getAmountDue, + getPiaAuthorizationAmount, getSubTotal, getSalesTax, } from "@/helpers/pricing-helper.js"; +import { applicationConfig } from "@/constants/application-config"; + +jest.mock("@/helpers/querystring-helper", () => ({ + getQuerystringParameter: jest.fn(), +})); + +jest.mock("@/router/methods/helpers/querystring-stash", () => ({ + peekQueryFromStash: jest.fn(), +})); + +import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { peekQueryFromStash } from "@/router/methods/helpers/querystring-stash"; const lineItems = { glassParts: [], @@ -61,4 +74,30 @@ describe("pricing-helper", () => { expect(result).toBe("3.00"); }); }); + + describe("getPiaAuthorizationAmount", () => { + beforeEach(() => { + jest.clearAllMocks(); + peekQueryFromStash.mockReturnValue(null); + getQuerystringParameter.mockReturnValue(null); + applicationConfig.CURRENT_ENVIRONMENT = "QA"; + }); + + it("returns amount due when no adjustment is configured", () => { + expect(getPiaAuthorizationAmount(lineItems)).toBe("53.00"); + }); + + it("ignores adjustment in production", () => { + applicationConfig.CURRENT_ENVIRONMENT = "Prod"; + peekQueryFromStash.mockReturnValue("-5"); + + expect(getPiaAuthorizationAmount(lineItems)).toBe("53.00"); + }); + + it("applies non-production query string adjustment", () => { + peekQueryFromStash.mockReturnValue("-5"); + + expect(getPiaAuthorizationAmount(lineItems)).toBe("48.00"); + }); + }); }); diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index fc3ad668d..f8f67449c 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -72,7 +72,7 @@ import { endpoints } from "../../constants/endpoints"; import { AppointmentTypeStrings } from "../../constants/schedule-constants"; import baseMixin from "@/mixins/base-mixin.js"; import { storeActions } from "@/constants/store-actions"; -import { getAmountDue } from "@/helpers/pricing-helper.js"; +import { getAmountDue, getPiaAuthorizationAmount } from "@/helpers/pricing-helper.js"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; import { createAdyenCheckout } from "@/helpers/adyen-helper"; import { Dropin } from "@adyen/adyen-web/auto"; @@ -364,14 +364,22 @@ export default { ); } - await baseMixin.methods.dispatchStoreAction( - storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, - this.amountDue, - false - ); + await this.savePiaPaymentAmounts(); await this.saveAndSubmitWorkOrder(); }, + async savePiaPaymentAmounts() { + await baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, + this.piaAuthorizationAmount, + false + ); + await baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_AUTHORIZATION_AMOUNT, + this.piaAuthorizationAmount, + false + ); + }, async handleFailedPayment(result) { console.log(`Result =`); console.log(result); @@ -555,8 +563,12 @@ export default { return getAmountDue(this.$store.getters.order.lineItems); }, + piaAuthorizationAmount() { + return getPiaAuthorizationAmount(this.$store.getters.order.lineItems); + }, + adyenPriceTotal() { - return Math.round(this.amountDue * 100); + return Math.round(parseFloat(this.piaAuthorizationAmount) * 100); }, // Cart info diff --git a/src/layouts/payment-pia-return/payment-pia-return.vue b/src/layouts/payment-pia-return/payment-pia-return.vue index c905f3d78..0bc7d3e22 100644 --- a/src/layouts/payment-pia-return/payment-pia-return.vue +++ b/src/layouts/payment-pia-return/payment-pia-return.vue @@ -14,12 +14,7 @@ import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import { Form } from "vee-validate"; import { paymentMethods } from "@/constants/payment-method-constants"; import baseMixin from "@/mixins/base-mixin.js"; -import { - getDisplayAmountDue, - getAmountDue, - getSubTotal, - getSalesTax, -} from "@/helpers/pricing-helper.js"; +import { getPiaAuthorizationAmount } from "@/helpers/pricing-helper.js"; // iframeResizer IS loaded into the page and necessary for the package to // to auto scale the iFrame this page is loaded in // Do not remove despite showing as "unused" CASH-309 @@ -103,11 +98,7 @@ export default { const payerId = getQuerystringParameter(queryStrings.PAYERID); baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PAYPAL_TOKEN, token, false); - baseMixin.methods.dispatchStoreAction( - storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, - this.getAmountDue(), - false - ); + this.savePiaPaymentAmounts(); await this.saveAndSubmitWorkOrder(); }, @@ -160,16 +151,22 @@ export default { console.log(ccToken); baseMixin.methods.dispatchStoreAction(storeActions.SAVE_CCTOKEN, ccToken, false); - baseMixin.methods.dispatchStoreAction( - storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, - this.getAmountDue(), - false - ); + this.savePiaPaymentAmounts(); await this.saveAndSubmitWorkOrder(); } }, - getAmountDue() { - return getAmountDue(store.getters.order.lineItems); + savePiaPaymentAmounts() { + const authorizationAmount = getPiaAuthorizationAmount(store.getters.order.lineItems); + baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, + authorizationAmount, + false + ); + baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_AUTHORIZATION_AMOUNT, + authorizationAmount, + false + ); }, async saveAndSubmitWorkOrder() { // Final work order submit after returning from PIA. diff --git a/src/router/methods/route-logic/adyen-return.js b/src/router/methods/route-logic/adyen-return.js index f15168861..9a287b243 100644 --- a/src/router/methods/route-logic/adyen-return.js +++ b/src/router/methods/route-logic/adyen-return.js @@ -2,7 +2,7 @@ import { routeData } from "@/router/constants/routes"; import { createAdyenCheckout, getSessionInfo } from "@/helpers/adyen-helper"; import { mapAdyenToFmgPaymentMethod, generateCcToken } from "@/helpers/adyen-helper"; import baseMixin from "@/mixins/base-mixin.js"; -import { getAmountDue } from "@/helpers/pricing-helper"; +import { getPiaAuthorizationAmount } from "@/helpers/pricing-helper"; import store from "@/store"; import { storeActions } from "@/constants/store-actions"; import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js"; @@ -49,7 +49,7 @@ export async function adyenReturnBeforeEnter(to, from) { // TODO once afterpay info is returned, create cc token and submit order. const paymentMethod = mapAdyenToFmgPaymentMethod(sessionInfo?.paymentMethod); - const amountDue = getAmountDue(store.getters.order.lineItems); + const authorizationAmount = getPiaAuthorizationAmount(store.getters.order.lineItems); const ccToken = generateCcToken(sessionInfo); ccToken.authCode = "831001"; @@ -68,7 +68,13 @@ export async function adyenReturnBeforeEnter(to, from) { await baseMixin.methods.dispatchStoreAction( storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT, - amountDue, + authorizationAmount, + false + ); + + await baseMixin.methods.dispatchStoreAction( + storeActions.SAVE_AUTHORIZATION_AMOUNT, + authorizationAmount, false ); diff --git a/src/store/index.js b/src/store/index.js index d9ca5c6fe..a34307ba9 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -162,6 +162,7 @@ const getDefaultState = () => { inactivePromos: null, paypalToken: null, nextGenSettledAmount: 0, + authorizationAmount: null, ccToken: { subscriptionId: null, expMonth: null, @@ -488,6 +489,9 @@ export const mutations = { updateNextGenSettledAmount(state, nextGenSettledAmount) { state.order.payment.nextGenSettledAmount = nextGenSettledAmount; }, + updateAuthorizationAmount(state, authorizationAmount) { + state.order.payment.authorizationAmount = authorizationAmount; + }, updateInsuranceVerifiedStatus(state, isVerified) { state.order.payment.insuranceCoverage.isVerified = isVerified; }, @@ -2893,6 +2897,9 @@ export const actions = { order.payment.piaType == paymentMethods.APPLE_PAY ? true : false, paypalToken: order.payment.paypalToken, nextGenSettledAmount: order.payment.nextGenSettledAmount, + ...(order.payment.authorizationAmount > 0 + ? { authorizationAmount: order.payment.authorizationAmount } + : {}), ccToken: { subscriptionId: order.payment.ccToken.subscriptionId, expMonth: order.payment.ccToken.expMonth, @@ -3047,6 +3054,10 @@ export const actions = { context.commit(storeMutations.UPDATE_NEXTGEN_SETTLED_AMOUNT, nextGenSettledAmount); }, + saveAuthorizationAmount(context, authorizationAmount) { + context.commit(storeMutations.UPDATE_AUTHORIZATION_AMOUNT, authorizationAmount); + }, + savePaypalToken(context, ppToken) { context.commit(storeMutations.UPDATE_PAYPAL_TOKEN, ppToken); }, diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 74cda6c9f..72eaac142 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -920,6 +920,105 @@ describe("Actions", () => { ); }); + it("saveSession action, includes authorizationAmount when PIA was authorized", async () => { + const context = createAuthorizationAmountSaveSessionContext(312.45); + + globalMethods.callHttpClient.mockImplementation(() => { + return Promise.resolve({ data: { referralNumber: 123 } }); + }); + + await actions.saveSession(context, { pageNameToLog: "test" }); + + expect(globalMethods.callHttpClient).toHaveBeenCalledWith( + expect.objectContaining({ + payload: expect.objectContaining({ + order: expect.objectContaining({ + payment: expect.objectContaining({ + authorizationAmount: 312.45, + }), + }), + }), + }) + ); + }); + + it("saveSession action, omits authorizationAmount when PIA was not authorized", async () => { + const context = createAuthorizationAmountSaveSessionContext(null); + + globalMethods.callHttpClient.mockImplementation(() => { + return Promise.resolve({ data: { referralNumber: 123 } }); + }); + globalMethods.callHttpClient.mockClear(); + + await actions.saveSession(context, { pageNameToLog: "test" }); + + const callPayload = globalMethods.callHttpClient.mock.calls[0][0].payload; + expect(callPayload.order.payment.authorizationAmount).toBeUndefined(); + }); + + function createAuthorizationAmountSaveSessionContext(authorizationAmount) { + const context = state; + const damage = { + numberOfChips: "2", + partQuestionAnswers: {}, + moldingQuestionAnswers: {}, + capabilityQuestionAnswers: {}, + }; + + context.getters = { + vehicle: { registration: {} }, + order: { damage }, + damage: {}, + applicationUser: { + lastPageVisited: "test-page", + crmCustomerId: "xxx-xxx-xxx", + savedSessionId: "xxx-xxx-xxx", + }, + }; + context.state = { + order: { + damage, + payment: { + insuranceCoverage: { isVerified: false }, + isInsurance: false, + authorizationAmount, + ccToken: { + subscriptionId: authorizationAmount ? "sub-123" : null, + expMonth: null, + expYear: null, + cardType: null, + billToPostalCode: null, + billToFirstName: null, + billToLastName: null, + referenceNumber: null, + authCode: authorizationAmount ? "831001" : null, + transactionId: null, + transReferenceNumber: null, + lastFour: null, + }, + }, + serviceLocation: {}, + customer: { + emailAddress: "test@safelite.com", + firstName: "John", + lastName: "Doe", + isSmsOptIn: true, + phoneNumber: "1234567890", + address: { + streetAddress: "123 Main St", + streetAddress2: "Apt 1", + city: "Anytown", + state: "OH", + zipCode: "12345", + }, + }, + lineItems: {}, + }, + }; + + return context; + } + it("loadSession action, returns order information, calls mutation", async () => { // Arrange const context = state;