Merge pull request #3113 from Safelite/rlsmerge/2026.03.26-to-develop

Rlsmerge/2026.03.26 to develop
This commit is contained in:
Chris 2026-03-24 13:41:44 -04:00 committed by GitHub
commit 67a034659d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 117 additions and 95 deletions

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="21" viewBox="0 0 20 21">
<path fill="#E86421" fill-rule="nonzero" d="M10 .5c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10S4.477.5 10 .5zm.043 13.6a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm.77-9.2h-1.54l-.088.009a.502.502 0 0 0-.299.193.66.66 0 0 0-.125.47l.747 6.806.017.096c.065.249.263.425.495.426l.084-.008c.22-.042.396-.246.427-.51l.793-6.805.004-.102a.651.651 0 0 0-.127-.37.489.489 0 0 0-.388-.205z"/>
</svg>

Before

Width:  |  Height:  |  Size: 474 B

View file

@ -123,6 +123,8 @@ const storeActions = {
SAVE_LOGGING_OPTION: "saveLoggingOption", SAVE_LOGGING_OPTION: "saveLoggingOption",
UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError",
GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey",
CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry",
}; };
export { storeActions }; export { storeActions };

View file

@ -98,6 +98,7 @@ const storeMutations = {
UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited",
UPDATE_LOGGING_OPTION: "updateLoggingOption", UPDATE_LOGGING_OPTION: "updateLoggingOption",
UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError", UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError",
UPDATE_IDEMPOTENCY_KEY: "updateIdempotencyKey",
// EXPERIMENT MUTATIONS // EXPERIMENT MUTATIONS
UPDATE_EXPERIMENTS: "updateExperiments", UPDATE_EXPERIMENTS: "updateExperiments",

View file

@ -343,14 +343,3 @@ export function splitCMSCopyOnParagraphTag(copy) {
export function splitCMSCopyOnBR(copy) { export function splitCMSCopyOnBR(copy) {
return copy.split("<br>"); return copy.split("<br>");
} }
// 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);
}

View file

@ -163,7 +163,7 @@ export default {
console.log(`Price = ${this.amountDue}`); console.log(`Price = ${this.amountDue}`);
console.log(`Adyen Price = ${this.adyenPriceTotal}`); console.log(`Adyen Price = ${this.adyenPriceTotal}`);
const requestBody = this.getAdyenInitRequestInfo(); const requestBody = await this.getAdyenInitRequestInfo();
console.log(`Calling with:`); console.log(`Calling with:`);
console.log(requestBody); console.log(requestBody);
@ -257,6 +257,7 @@ export default {
this.dropinComponent = dropin; this.dropinComponent = dropin;
dropin.mount("#adyen-container"); dropin.mount("#adyen-container");
await this.dispatchStoreAction(storeActions.CORRECT_IDEMPOTENCY_KEY_EXPIRY, expiryTime);
}, },
async handleCompletedPayment(result) { async handleCompletedPayment(result) {
@ -438,19 +439,13 @@ export default {
}, },
// Now a Method so it is always freshly called and not cached. // Now a Method so it is always freshly called and not cached.
getIdempotencyKey() { async getIdempotencyKey() {
const currentDateTime = new Date(); return await this.dispatchStoreAction(storeActions.GET_VALID_IDEMPOTENCY_KEY);
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}`;
}, },
// Now a Method so it is always freshly called and not cached. // Now a Method so it is always freshly called and not cached.
getAdyenInitRequestInfo() { async getAdyenInitRequestInfo() {
const key = await this.getIdempotencyKey();
return { return {
sourceSystem: this.sourceSystem, sourceSystem: this.sourceSystem,
referralSequenceNumber: this.$store.getters.order.referralSequenceNumber, referralSequenceNumber: this.$store.getters.order.referralSequenceNumber,
@ -464,10 +459,9 @@ export default {
stateOrProvince: this.locationInfo.state, stateOrProvince: this.locationInfo.state,
returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL, returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL,
email: this.$store.getters.order.customer.emailAddress, email: this.$store.getters.order.customer.emailAddress,
IP: "127.0.0.1", // TODO
firstName: this.$store.getters.order.customer.firstName, firstName: this.$store.getters.order.customer.firstName,
lastName: this.$store.getters.order.customer.lastName, lastName: this.$store.getters.order.customer.lastName,
idempotencyKey: this.getIdempotencyKey(), idempotencyKey: key,
}; };
}, },
}, },

View file

@ -81,16 +81,6 @@
cmsWidgetName="AlertNoShopsWidget" cmsWidgetName="AlertNoShopsWidget"
v-if="displayNoShopsAlert" v-if="displayNoShopsAlert"
alertClass="alert-warning" /> alertClass="alert-warning" />
<alert
ref="alertMSRNotCoveredByInsurance"
class="mt-5 mb-5"
v-if="isMSRFeeNotCoveredByInsurance"
:manualHeadline="MSRFeeAlertHeadlineText"
:manualCopy="MSRFeeAlertBodyText"
:showWarningIcon="true"
:hasBorder="true"
:showHorizontalRow="true"
alertClass="alert-warning" />
</div> </div>
</div> </div>
<div <div
@ -239,11 +229,7 @@ import {
RECAL_ACK_YES, RECAL_ACK_YES,
} from "@/constants/schedule-constants"; } from "@/constants/schedule-constants";
import { import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
fetchCmsContentForPage,
splitCopyOnCMSPlaceHolder,
formatToUSDollar,
} from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { import {
getPricedMobileFeePart, getPricedMobileFeePart,
@ -259,6 +245,7 @@ import { partNumberStrings } from "@/constants/part-number-strings";
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { splitCopyOnCMSPlaceHolder } from "@/helpers/cms-content-helper";
import { import {
calcDaysBetweenDates, calcDaysBetweenDates,
convertDateStringToDate, convertDateStringToDate,
@ -705,7 +692,8 @@ export default {
isMobileStaticRecalibrationApplicable() { isMobileStaticRecalibrationApplicable() {
return ( return (
this.displayMSR && this.displayMSR &&
this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
(this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
); );
}, },
displayMSR() { displayMSR() {
@ -715,28 +703,6 @@ export default {
?.toLowerCase() === "true" ?.toLowerCase() === "true"
); );
}, },
isMSRFeeNotCoveredByInsurance() {
return (
this.isMobileSelected &&
this.isMobileStaticRecalibrationApplicable &&
this.mobileFeeHasPrice &&
this.isInsurance &&
!this.mobileFeePart?.isInsurable
);
},
MSRFeeAlertHeadlineText() {
return this.getCmsContent("AlertMSRNotCoveredByInsuranceWidget", "HeadlineText");
},
MSRFeeAlertBodyText() {
const cmsContentText = this.getCmsContent(
"AlertMSRNotCoveredByInsuranceWidget",
"BodyText"
);
return cmsContentText.replaceAll(
"{custom:mobileFee}",
formatToUSDollar(this.mobileFee)
);
},
isCashItacNoComp() { isCashItacNoComp() {
return !this.isInsurance || this.isITAC || this.isNoComp; return !this.isInsurance || this.isITAC || this.isNoComp;
}, },
@ -2054,14 +2020,7 @@ export default {
} }
}, },
async showMultiLocationModal() { async showMultiLocationModal() {
const showMultiLocationAppointment = experimentMixin.methods.getSettingValue( if (!this.selectedRouteCodeData?.routeCode) {
experimentSettings.SHOW_MULTI_LOCATION_APPT
);
if (
!this.selectedRouteCodeData?.routeCode &&
showMultiLocationAppointment?.toLowerCase() === "true"
) {
let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue( let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue(
experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION
); );

View file

@ -59,7 +59,7 @@ import {
} from "@/helpers/recal-helper"; } from "@/helpers/recal-helper";
import { externalParameterStatus } from "@/constants/external-parameters"; import { externalParameterStatus } from "@/constants/external-parameters";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { addPricesToLineItems } from "@/helpers/pricing-helper"; import { addPricesToLineItems, getAmountDue } from "@/helpers/pricing-helper";
// Export State // Export State
const getDefaultState = () => { const getDefaultState = () => {
@ -207,6 +207,12 @@ const getDefaultState = () => {
loggingOption: false, loggingOption: false,
hasAlreadyTriggeredError: false, hasAlreadyTriggeredError: false,
}, },
idempotencyKeyFields: {
referralCorrelationId: null,
totalInCents: 0,
expiryTime: null,
idempotencyKey: null,
},
}; };
}; };
@ -862,6 +868,15 @@ export const mutations = {
updateHasTriggeredError(state, hasTriggeredError) { updateHasTriggeredError(state, hasTriggeredError) {
state.applicationUser.hasAlreadyTriggeredError = 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 // Export Getters
@ -1105,6 +1120,35 @@ export const getters = {
state.order.customer.emailAddress 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 // Export Actions
@ -3665,6 +3709,63 @@ export const actions = {
updateHasTriggeredError(context, hasAlreadyTriggeredError) { updateHasTriggeredError(context, hasAlreadyTriggeredError) {
context.commit(storeMutations.UPDATE_HAS_TRIGGERED_ERROR, 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({ export default createStore({

View file

@ -7,15 +7,9 @@
isDismissible ? 'alert-dismissible' : '', isDismissible ? 'alert-dismissible' : '',
this.alertClass, this.alertClass,
this.cssClassNameForCmsWidget, this.cssClassNameForCmsWidget,
this.hasBorder ? 'bordered' : '',
]"> ]">
<p class="mx-6 my-0 fw-bold alert-heading"> <p class="mx-6 my-0 fw-bold alert-heading">
<img v-if="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" /> <img v-if="showInfoIcon" :src="infoIcon" alt="Info Icon" class="info-icon" />
<img
v-if="showWarningIcon"
:src="warningIcon"
alt="Warning Icon"
class="warning-icon" />
{{ alertHeadline }} {{ alertHeadline }}
</p> </p>
<hr v-if="showHorizontalRow" /> <hr v-if="showHorizontalRow" />
@ -69,9 +63,7 @@ export default {
name: "alert", name: "alert",
props: { props: {
showInfoIcon: Boolean, showInfoIcon: Boolean,
showWarningIcon: Boolean,
showHorizontalRow: Boolean, showHorizontalRow: Boolean,
hasBorder: Boolean,
isDismissible: Boolean, isDismissible: Boolean,
alertClass: String, alertClass: String,
/* /*
@ -104,9 +96,6 @@ export default {
infoIcon() { infoIcon() {
return require(`@/assets/img/icons/info-circle-blue.svg`); return require(`@/assets/img/icons/info-circle-blue.svg`);
}, },
warningIcon() {
return require(`@/assets/img/icons/alert-circle-yellow.svg`);
},
pageQueryString() { pageQueryString() {
return applicationConfig.PAGE_QUERYSTRING; return applicationConfig.PAGE_QUERYSTRING;
}, },
@ -222,19 +211,12 @@ export default {
background-color: $yellow-100; background-color: $yellow-100;
.alert-heading { .alert-heading {
color: $yellow-600; color: $yellow-600;
.warning-icon {
height: 1rem;
}
} }
svg { svg {
fill: $yellow-600; fill: $yellow-600;
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
} }
hr {
border-color: $yellow-800;
}
} }
&.alert-success { &.alert-success {
background-color: $green-100; background-color: $green-100;
@ -247,9 +229,6 @@ export default {
height: 1rem; height: 1rem;
} }
} }
&.bordered {
border: 1px solid;
}
& p { & p {
font-size: 0.875rem; font-size: 0.875rem;
} }