From f80e85e2d89d1953c4965ce794faea988615538a Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 18 Mar 2026 11:05:39 -0400 Subject: [PATCH 1/4] CASH-2472 - Update Custom Exposure Logging for MultiLocationPopup --- src/layouts/schedule/schedule.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index c13026d36..e69dab9f4 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2054,13 +2054,8 @@ export default { } }, async showMultiLocationModal() { - const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( - experimentSettings.SHOW_MULTI_LOCATION_APPT - ); - if ( - !this.selectedRouteCodeData?.routeCode && - showMultiLocationAppointment?.toLowerCase() === "true" + !this.selectedRouteCodeData?.routeCode ) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION From 335fe7576839629dea8e743e64917868989f1513 Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 18 Mar 2026 11:12:17 -0400 Subject: [PATCH 2/4] CASH-2472 - Fixed formatting --- src/layouts/schedule/schedule.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index e69dab9f4..ec8051cc4 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2054,9 +2054,7 @@ export default { } }, async showMultiLocationModal() { - if ( - !this.selectedRouteCodeData?.routeCode - ) { + if (!this.selectedRouteCodeData?.routeCode) { let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION ); From 80e77bc18461015ccc5297bce5d25b7186530892 Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 19 Mar 2026 12:01:00 -0400 Subject: [PATCH 3/4] Revert "Reapply "Merge pull request #3075 from Safelite/feature/CASH-1684"" This reverts commit c9e5ccff54ad208fdbdb7cd695448bf8a36f4f25. --- src/assets/img/icons/alert-circle-yellow.svg | 3 -- src/helpers/cms-content-helper.js | 11 ----- src/layouts/schedule/schedule.vue | 42 ++------------------ src/ux-components/alert/alert.vue | 21 ---------- 4 files changed, 4 insertions(+), 73 deletions(-) delete mode 100644 src/assets/img/icons/alert-circle-yellow.svg diff --git a/src/assets/img/icons/alert-circle-yellow.svg b/src/assets/img/icons/alert-circle-yellow.svg deleted file mode 100644 index 904aefb98..000000000 --- a/src/assets/img/icons/alert-circle-yellow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 450875d0f..727add296 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -343,14 +343,3 @@ export function splitCMSCopyOnParagraphTag(copy) { export function splitCMSCopyOnBR(copy) { return copy.split("
"); } - -// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not. -export function formatToUSDollar(amount) { - const isInteger = amount % 1 === 0; - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: isInteger ? 0 : 2, - maximumFractionDigits: 2, - }).format(amount); -} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index ec8051cc4..d33695833 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -81,16 +81,6 @@ cmsWidgetName="AlertNoShopsWidget" v-if="displayNoShopsAlert" alertClass="alert-warning" /> -

Info Icon - Warning Icon {{ alertHeadline }}


@@ -69,9 +63,7 @@ export default { name: "alert", props: { showInfoIcon: Boolean, - showWarningIcon: Boolean, showHorizontalRow: Boolean, - hasBorder: Boolean, isDismissible: Boolean, alertClass: String, /* @@ -104,9 +96,6 @@ export default { infoIcon() { return require(`@/assets/img/icons/info-circle-blue.svg`); }, - warningIcon() { - return require(`@/assets/img/icons/alert-circle-yellow.svg`); - }, pageQueryString() { return applicationConfig.PAGE_QUERYSTRING; }, @@ -222,19 +211,12 @@ export default { background-color: $yellow-100; .alert-heading { color: $yellow-600; - - .warning-icon { - height: 1rem; - } } svg { fill: $yellow-600; width: 1rem; height: 1rem; } - hr { - border-color: $yellow-800; - } } &.alert-success { background-color: $green-100; @@ -247,9 +229,6 @@ export default { height: 1rem; } } - &.bordered { - border: 1px solid; - } & p { font-size: 0.875rem; } From fe05b61fa635980a8543bbf5b63c3a46a68da35e Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Thu, 19 Mar 2026 13:53:25 -0400 Subject: [PATCH 4/4] Change idempotency to be cached with invalidating fields. --- src/constants/store-actions.js | 2 + src/constants/store-mutations.js | 1 + src/layouts/payment-adyen/payment-adyen.vue | 20 ++-- src/store/index.js | 103 +++++++++++++++++++- 4 files changed, 112 insertions(+), 14 deletions(-) diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 3afd4df24..26bcc25e1 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -123,6 +123,8 @@ const storeActions = { SAVE_LOGGING_OPTION: "saveLoggingOption", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", + GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey", + CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 9b4b3719c..5c6c2e22d 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -98,6 +98,7 @@ const storeMutations = { UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LOGGING_OPTION: "updateLoggingOption", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", + UPDATE_IDEMPOTENCY_KEY: "updateIdempotencyKey", // EXPERIMENT MUTATIONS UPDATE_EXPERIMENTS: "updateExperiments", diff --git a/src/layouts/payment-adyen/payment-adyen.vue b/src/layouts/payment-adyen/payment-adyen.vue index adb0a1210..4a4936a7f 100644 --- a/src/layouts/payment-adyen/payment-adyen.vue +++ b/src/layouts/payment-adyen/payment-adyen.vue @@ -163,7 +163,7 @@ export default { console.log(`Price = ${this.amountDue}`); console.log(`Adyen Price = ${this.adyenPriceTotal}`); - const requestBody = this.getAdyenInitRequestInfo(); + const requestBody = await this.getAdyenInitRequestInfo(); console.log(`Calling with:`); console.log(requestBody); @@ -257,6 +257,7 @@ export default { this.dropinComponent = dropin; dropin.mount("#adyen-container"); + await this.dispatchStoreAction(storeActions.CORRECT_IDEMPOTENCY_KEY_EXPIRY, expiryTime); }, async handleCompletedPayment(result) { @@ -438,19 +439,13 @@ export default { }, // Now a Method so it is always freshly called and not cached. - getIdempotencyKey() { - const currentDateTime = new Date(); - const currentHour = currentDateTime.getUTCHours(); - const currentDate = currentDateTime.getUTCDate(); - const id = this?.$store?.getters?.order?.referralCorrelationId; - const system = this.sourceSystem; - const total = this.adyenPriceTotal; - - return `${id}-${system}-${currentDate}-${currentHour}-${total}`; + async getIdempotencyKey() { + return await this.dispatchStoreAction(storeActions.GET_VALID_IDEMPOTENCY_KEY); }, // Now a Method so it is always freshly called and not cached. - getAdyenInitRequestInfo() { + async getAdyenInitRequestInfo() { + const key = await this.getIdempotencyKey(); return { sourceSystem: this.sourceSystem, referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, @@ -464,10 +459,9 @@ export default { stateOrProvince: this.locationInfo.state, returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL, email: this.$store.getters.order.customer.emailAddress, - IP: "127.0.0.1", // TODO firstName: this.$store.getters.order.customer.firstName, lastName: this.$store.getters.order.customer.lastName, - idempotencyKey: this.getIdempotencyKey(), + idempotencyKey: key, }; }, }, diff --git a/src/store/index.js b/src/store/index.js index 10218892c..e740bf0d3 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -59,7 +59,7 @@ import { } from "@/helpers/recal-helper"; import { externalParameterStatus } from "@/constants/external-parameters"; import { experimentSettings } from "@/constants/experiments"; -import { addPricesToLineItems } from "@/helpers/pricing-helper"; +import { addPricesToLineItems, getAmountDue } from "@/helpers/pricing-helper"; // Export State const getDefaultState = () => { @@ -207,6 +207,12 @@ const getDefaultState = () => { loggingOption: false, hasAlreadyTriggeredError: false, }, + idempotencyKeyFields: { + referralCorrelationId: null, + totalInCents: 0, + expiryTime: null, + idempotencyKey: null, + }, }; }; @@ -862,6 +868,15 @@ export const mutations = { updateHasTriggeredError(state, hasTriggeredError) { state.applicationUser.hasAlreadyTriggeredError = hasTriggeredError; }, + updateIdempotencyKey( + state, + { idempotencyKey, referralCorrelationId, totalInCents, expiryTime } + ) { + state.idempotencyKeyFields.idempotencyKey = idempotencyKey; + state.idempotencyKeyFields.referralCorrelationId = referralCorrelationId; + state.idempotencyKeyFields.totalInCents = totalInCents; + state.idempotencyKeyFields.expiryTime = expiryTime; + }, }; // Export Getters @@ -1105,6 +1120,35 @@ export const getters = { state.order.customer.emailAddress ); }, + + isIdempotencyKeyValid: (state) => { + if (!state.idempotencyKeyFields?.idempotencyKey) { + return false; + } + + const referralCorrelationId = state.order?.referralCorrelationId; + if ( + !referralCorrelationId || + referralCorrelationId !== state.idempotencyKeyFields?.referralCorrelationId + ) { + return false; + } + + const total = getAmountDue(state.order?.lineItems); + const totalInCents = Math.round(total * 100); + if (totalInCents !== state.idempotencyKeyFields?.totalInCents) { + return false; + } + + const currentTime = new Date(); + const expiryTime = new Date(state.idempotencyKeyFields.expiryTime); + if (!expiryTime || expiryTime.getTime() < currentTime.getTime()) { + return false; + } + + // If all checks pass, then the key is valid. + return true; + }, }; // Export Actions @@ -3665,6 +3709,63 @@ export const actions = { updateHasTriggeredError(context, hasAlreadyTriggeredError) { context.commit(storeMutations.UPDATE_HAS_TRIGGERED_ERROR, hasAlreadyTriggeredError); }, + + getValidIdempotencyKey(context) { + if (context.getters.isIdempotencyKeyValid) { + return context.state.idempotencyKeyFields.idempotencyKey; + } + + // If invalid, need to regenerate. + const referralCorrelationId = context.state.order?.referralCorrelationId; + if (!referralCorrelationId) { + // Can't generate! + return null; + } + + const total = getAmountDue(context.state.order?.lineItems); + const totalInCents = Math.round(total * 100); + if (Number.isNaN(totalInCents)) { + // Can't generate! + return null; + } + + const currentTime = new Date(); + const nextHour = currentTime.getUTCHours() + 1; + currentTime.setUTCHours(nextHour); + + const newId = crypto.randomUUID(); + + const newKeyInfo = { + idempotencyKey: newId, + referralCorrelationId: referralCorrelationId, + totalInCents: totalInCents, + expiryTime: currentTime, + }; + + context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKeyInfo); + + return newId; + }, + + correctIdempotencyKeyExpiry(context, expiryTime) { + if (!context.getters.isIdempotencyKeyValid) { + return; + } + + const currentExpiryTime = new Date(context.state.idempotencyKeyFields.expiryTime); + const expiryTimeAsDate = new Date(expiryTime); + + if (expiryTimeAsDate.getTime() < currentExpiryTime.getTime()) { + const existingKey = context.state.idempotencyKeyFields; + const newKey = { + referralCorrelationId: existingKey.referralCorrelationId, + totalInCents: existingKey.totalInCents, + idempotencyKey: existingKey.idempotencyKey, + expiryTime: expiryTimeAsDate, + }; + context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey); + } + }, }; export default createStore({