Merge branch 'develop' into feature/CSR-1382-loose-ends

This commit is contained in:
Leah Schumann 2023-10-30 07:27:07 -04:00 committed by GitHub
commit 46473c5ba0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 686 additions and 241 deletions

View file

@ -3,11 +3,11 @@
This public/css folder uses a standalone .scss > .css setup.
## Usage
From a Terminal window, run the Sass Watch command on this folder only:
From a Terminal window, run the Sass Watch command from the public folder only:
```bash
sass --no-source-map --watch public/css/hop-styling.scss:public/css/hop-styling.css
sass --no-source-map --watch scss:css
```
This will output a CSS file that can be consumed by the Payment page.
This will compile and output .css files (into the public/css folder) that can be consumed by the Payment page.
# Do not use the .css file on its own. You must use the .scss file and have Sass compile it to the .css file.
# Do not use the .css files on their own. You must use the .scss files and have Sass compile them to .css files.

View file

@ -15,6 +15,7 @@ body .validation-info {
}
body .italicSmall {
font-style: normal;
font-size: 14px;
}
body span {
line-height: 24px;
@ -27,7 +28,7 @@ body .form-group {
display: flex;
flex-direction: column;
}
body h2 {
body .headerlinecontainer .headerline1 {
font-size: 20px;
line-height: 32px;
font-weight: 400;
@ -76,12 +77,12 @@ body .creditCardSpecific div {
body .creditCardSpecific div .headerlinecontainer {
padding: 0;
}
body .creditCardSpecific div .headerlinecontainer .headerline5v2 {
display: none;
}
body .creditCardSpecific div label {
font-weight: 500;
}
body .creditCardSpecific div:first-child {
display: none;
}
body .creditCardSpecific #infoRow2 {
margin-bottom: 0;
}
@ -91,7 +92,7 @@ body #cardExpirationContainer {
justify-content: space-between;
padding: 0;
}
@media (min-width: 368px) {
@media (min-width: 400px) {
body #cardExpirationContainer {
flex-direction: row;
}
@ -116,7 +117,6 @@ body .optional-label:after {
}
body .buttonContainer {
height: 48px;
margin-top: 36px;
}
body .buttonContainer button {
position: relative;

View file

@ -0,0 +1,90 @@
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* Style the modal when PayPal button is selected */
#pageLoadingModal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.35);
display: flex;
}
#pageLoadingModal .modal-content {
margin: auto auto;
}
#pageLoadingModal p {
margin: 0;
text-transform: uppercase;
font-size: 14px;
}
#pageLoadingModal .modal {
display: none;
overflow: hidden;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
-webkit-overflow-scrolling: touch;
outline: 0;
}
#pageLoadingModal .modal-content {
position: relative;
background-color: #fff;
border-radius: 8px;
background-clip: padding-box;
outline: 0;
width: 168px;
height: 168px;
display: flex;
justify-content: center;
align-items: center;
}
#pageLoadingModal .modal-content:before, #pageLoadingModal .modal-content:after {
content: "";
border-radius: 50%;
position: absolute;
inset: 0;
box-shadow: 0 0 3px 2px rgba(0, 0, 0, 0.25) inset;
top: 24px;
bottom: 24px;
right: 24px;
left: 24px;
}
#pageLoadingModal .modal-content:after {
box-shadow: 0 3px 0 #da291c inset;
animation: rotate 1s linear infinite;
}
#pageLoadingModal .modal-backdrop {
position: absolute;
top: 0;
right: 0;
left: 0;
background-color: #000000;
}
#pageLoadingModal .modal-backdrop.fade {
opacity: 0;
filter: alpha(opacity=0);
}
#pageLoadingModal.hide {
display: none;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.creditCardSpecific {
display: none;
}

View file

@ -16,6 +16,7 @@ body {
.italicSmall {
font-style: normal;
font-size: 14px;
}
span {
@ -32,11 +33,13 @@ body {
flex-direction: column;
}
h2 {
font-size: 20px;
line-height: 32px;
font-weight: 400;
margin: 24px 0;
.headerlinecontainer {
.headerline1 {
font-size: 20px;
line-height: 32px;
font-weight: 400;
margin: 24px 0;
}
}
input,
@ -86,13 +89,13 @@ body {
padding: 8px 0;
.headerlinecontainer {
padding: 0;
.headerline5v2 {
display: none;
}
}
label {
font-weight: 500;
}
&:first-child {
display: none;
}
}
#infoRow2 {
margin-bottom: 0;
@ -104,7 +107,7 @@ body {
flex-direction: column;
justify-content: space-between;
padding: 0;
@media (min-width: 368px) {
@media (min-width: 400px) {
flex-direction: row;
.card_expirationYear,
.card_expirationMonth {
@ -132,7 +135,6 @@ body {
.buttonContainer {
height: 48px;
margin-top: 36px;
button {
position: relative;
background: linear-gradient(270deg, #1574a1 0%, #003d58 100%);

View file

@ -0,0 +1,95 @@
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */
/* Style the modal when PayPal button is selected */
#pageLoadingModal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,.35);
display: flex;
.modal-content {
margin: auto auto;
}
p {
margin: 0;
text-transform: uppercase;
font-size: 14px;
}
.modal {
display: none;
overflow: hidden;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1040;
-webkit-overflow-scrolling: touch;
outline: 0;
}
.modal-content {
position: relative;
background-color: #fff;
border-radius: 8px;
background-clip: padding-box;
outline: 0;
width: 168px;
height: 168px;
display: flex;
justify-content: center;
align-items: center;
&:before,
&:after {
content: '';
border-radius: 50%;
position: absolute;
inset: 0;
box-shadow: 0 0 3px 2px rgba(0, 0, 0, 0.25) inset;
top: 24px;
bottom: 24px;
right: 24px;
left: 24px;
}
&:after {
box-shadow: 0 3px 0 #da291c inset;
animation: rotate 1s linear infinite;
}
}
.modal-backdrop {
position: absolute;
top: 0;
right: 0;
left: 0;
background-color: #000000;
&.fade {
opacity: 0;
filter: alpha(opacity=0);
}
}
&.hide {
display: none;
}
}
@keyframes rotate {
0% { transform: rotate(0)}
100% { transform: rotate(360deg)}
}
.creditCardSpecific {
display: none;
}

View file

@ -89,9 +89,6 @@ const storeActions = {
SAVE_PROMOS: "savePromos",
SAVE_CUSTOMER_DETAILS: "saveCustomerDetails",
SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes",
SAVE_WORK_ORDER_FLAG: "saveWorkOrderFlag",
SAVE_PIA_ERROR_CODE: "savePiaErrorCode",
SAVE_PIA_WORK_ORDER: "savePiaWorkOrder",
SAVE_CCTOKEN: "saveCCToken",
SAVE_PAYPAL_TOKEN: "savePaypalToken",
};

View file

@ -1,8 +1,5 @@
const storeMutations = {
// PAYMENT MUTATIONS
UPDATE_WORK_ORDER_FLAG: "updateWorkOrderFlag",
UPDATE_PIA_ERROR_CODE: "updatePiaErrorCode",
UPDATE_PIA_WORK_ORDER: "updatePiaWorkOrder",
UPDATE_CCTOKEN: "updateCCToken",
UPDATE_PAYPAL_TOKEN: "updatePaypalToken",

View file

@ -53,17 +53,30 @@ export async function loadSessionIfPresent(isConceptInsurance, pageNameToLog) {
This will also set Referral information in the store after saving, and then
update the cookie. To force synchronous behavior pass in 'true' for shouldAwaitSaveSessionQueue
*/
export async function saveSession({ pageNameToLog, shouldAwaitSaveSessionQueue = false }) {
export async function saveSession({
pageNameToLog,
shouldAwaitSaveSessionQueue = false,
submitAfterSave = false,
createDeleteStatusWorkOrderForPia = false,
}) {
var saveSessionPromise;
if (store.getters.applicationUser.saveSessionPromise) {
// queue newest request after current saveSessionPromise resolves
saveSessionPromise = store.getters.applicationUser.saveSessionPromise.then(() => {
// get a new saveSessionPromise
return saveSessionHelper(pageNameToLog);
return saveSessionHelper(
pageNameToLog,
submitAfterSave,
createDeleteStatusWorkOrderForPia
);
});
} else {
// create an initial saveSessionPromise
saveSessionPromise = saveSessionHelper(pageNameToLog);
saveSessionPromise = saveSessionHelper(
pageNameToLog,
submitAfterSave,
createDeleteStatusWorkOrderForPia
);
}
store.commit(storeMutations.UPDATE_SAVE_SESSION_PROMISE, saveSessionPromise);
// await here to allow for a caller to await and make the function synchronous
@ -72,10 +85,16 @@ export async function saveSession({ pageNameToLog, shouldAwaitSaveSessionQueue =
}
}
export async function submitWorkOrder({ pageNameToLog }) {
export async function submitWorkOrder({
pageNameToLog,
submitAfterSave = false,
createDeleteStatusWorkOrderForPia = false,
}) {
await saveSession({
pageNameToLog: pageNameToLog,
shouldAwaitSaveSessionQueue: true,
submitAfterSave: submitAfterSave,
createDeleteStatusWorkOrderForPia: createDeleteStatusWorkOrderForPia,
});
}
@ -117,10 +136,17 @@ async function loadSession(
/*
Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing
*/
async function saveSessionHelper(pageNameToLog) {
async function saveSessionHelper(
pageNameToLog,
submitAfterSave = false,
createDeleteStatusWorkOrderForPia = false
) {
const savedSessionInfo = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.SAVE_SESSION,
null,
{
submitAfterSave: submitAfterSave,
createDeleteStatusWorkOrderForPia: createDeleteStatusWorkOrderForPia,
},
pageNameToLog
);
// Update the store with information received from the saveSession response

View file

@ -179,7 +179,7 @@ describe("saveSession", () => {
// Assert
expect(mocks.baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
storeActions.SAVE_SESSION,
null,
{ createDeleteStatusWorkOrderForPia: false, submitAfterSave: false },
"test"
);
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
@ -282,12 +282,12 @@ describe("submitWorkOrder", () => {
const mocks = setupMocksForJsFiles(mockData);
// Act
await submitWorkOrder({ pageNameToLog: "test" });
await submitWorkOrder({ pageNameToLog: "test", submitAfterSave: true });
// Assert
expect(mocks.baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
storeActions.SAVE_SESSION,
null,
{ createDeleteStatusWorkOrderForPia: false, submitAfterSave: true },
"test"
);
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
@ -345,7 +345,7 @@ describe("submitWorkOrder", () => {
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
// Act
await submitWorkOrder({ pageNameToLog: "test" });
await submitWorkOrder({ pageNameToLog: "test", submitAfterSave: true });
// Assert
expect(cookieHelper.getFunnelCookie().DidHeritageFunnelUpdateLast).toEqual(false);

View file

@ -244,7 +244,7 @@ export default {
<style lang="scss">
.main {
padding: 1rem 0;
padding: 1rem 1.5rem 1.5rem;
box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);
border-radius: 5px;
}
@ -260,6 +260,7 @@ export default {
+ p {
font-size: 1rem;
margin-top: 0.5rem;
font-weight: 500;
}
}
@ -270,7 +271,7 @@ export default {
}
.calendar-section .add-to-calendar {
padding: 10px 0;
padding: 1rem 0;
text-align: center;
}
@ -279,6 +280,10 @@ export default {
background-color: #fff;
}
.appoinmenttext p {
margin: 0;
}
.calendar-section .add-to-calendar .add-to-calendar-span {
cursor: pointer;
}
@ -296,12 +301,12 @@ export default {
}
.header-container {
text-align: center;
padding: 1rem 0 1.5rem 0;
p {
display: inline-block;
font-weight: 400;
font-size: 1.25rem;
padding: 0.3rem;
color: $black;
}
}

View file

@ -3,39 +3,75 @@
<div class="add-vaps-buttons" aria-live="polite">
<buttonQuestion
ref="buttonQuestion"
:answers="answersToDisplay"
:answers="buttonsToDisplay"
groupName="addVaps"
v-model="answersToDisplay"
v-model="buttonsToDisplay"
buttonTypeString="addVapsButton"
:buttonTypeObject="addVapsButton"
:isWide="answersToDisplay.length > 1 ? false : true"
:isWide="buttonsToDisplay.length > 1 ? false : true"
@update:modelValue="openModal" />
<contentGroupModal
ref="RainDefenseModal"
cmsWidgetName="RainDefenseModal"
v-if="rainDefenseItem"
@footer-button-action="addToCart('RainDefenseModal', rainDefenseItem)"
v-if="rainDefenseModal"
@footer-button-action="addRainDefenseToCart('RainDefenseModal', rainDefenseModal)"
footerButtonActionName="footer-button-action">
<p class="price">{{ rainDefenseItem.listPrice }}</p>
<p class="price">+${{ rainDefenseModal.listPrice }}</p>
</contentGroupModal>
<contentGroupModal
ref="WipersModal"
cmsWidgetName="WipersModal"
v-if="wipersItem"
@footer-button-action="addToCart('WipersModal', wipersItem)"
v-if="wipersModal"
@footer-button-action="addWipersToCart('WipersModal', wipersModal)"
footerButtonActionName="footer-button-action">
<p class="price">{{ wipersItem.listPrice }}</p>
<div v-if="wipersToDisplay === wipersToDisplayStrings.BOTH" class="wiper-options">
<div>
<checkboxQuestion
class="mb-5"
cmsWidgetName="AddFrontBladesQuestionWidget"
v-model="selectedFrontWipers" />
<p class="price">+${{ wipersModal.totalFrontPrice }}</p>
</div>
<div>
<checkboxQuestion
class="mb-5"
cmsWidgetName="AddRearBladesQuestionWidget"
v-model="selectedRearWipers" />
<p class="price">+${{ wipersModal.totalRearPrice }}</p>
</div>
</div>
<div
v-else-if="wipersToDisplay === wipersToDisplayStrings.FRONT"
class="wiper-options">
<p class="price">+${{ wipersModal.totalFrontPrice }}</p>
</div>
<div
v-else-if="wipersToDisplay === wipersToDisplayStrings.REAR"
class="wiper-options">
<p class="price">+${{ wipersModal.totalRearPrice }}</p>
</div>
</contentGroupModal>
</div>
</transition>
</template>
<script>
// COMPONENTS
import buttonQuestion from "@/digital-components/button-question/button-question";
import addVapsButton from "./add-vaps-button/add-vaps-button";
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
// SUPPORTING
import { partTypeStrings } from "@/constants/part-type-strings";
const wipersToDisplayStrings = {
FRONT: "FRONT",
REAR: "REAR",
BOTH: "BOTH",
};
export default {
name: "add-vaps-modal-buttons",
@ -47,54 +83,73 @@ export default {
data() {
return {
addVapsButton: addVapsButton,
selectedFrontWipers: false,
selectedRearWipers: false,
wipersToDisplay: null,
};
},
methods: {
openModal(modalName) {
this.$refs[modalName].openModal();
},
addToCart(modalName, part) {
const vapsItems = this.currentCartItems.vaps;
const alreadyInVaps = vapsItems.some((item) => {
addRainDefenseToCart(modalName, part) {
const alreadyInCart = this.currentCartItems.vaps.some((item) => {
return item.partType === part.partType;
});
if (!alreadyInVaps) {
if (part.subItems) {
// for wipers, a "combined" part (w/ subItems)
part.subItems.forEach((part) => {
if (!alreadyInCart) {
this.currentCartItems.vaps.push(part);
}
this.$refs[modalName].closeModal();
},
addWipersToCart(modalName, part) {
const frontWipersAlreadyInCart = this.currentCartItems.vaps.some((item) => {
return item.partType === part.frontWiperLineItems[0].partType;
});
const rearWipersAlreadyInCart = this.currentCartItems.vaps.some((item) => {
return item.partType === part.rearWiperLineItems[0]?.partType;
});
if (this.wipersToDisplay === wipersToDisplayStrings.FRONT) {
this.selectedFrontWipers = true; // set as true if front wipers is only option
}
if (this.wipersToDisplay === wipersToDisplayStrings.REAR) {
this.selectedRearWipers = true; // set as true if rear wipers is only option
}
if (!frontWipersAlreadyInCart && this.selectedFrontWipers) {
if (part.frontWiperLineItems) {
// for wipers, a "combined" part (frontWiperLineItems)
part.frontWiperLineItems.forEach((part) => {
this.currentCartItems.vaps.push(part);
});
}
}
if (!rearWipersAlreadyInCart && this.selectedRearWipers) {
if (part.rearWiperLineItems) {
part.rearWiperLineItems.forEach((part) => {
this.currentCartItems.vaps.push(part);
});
} else {
// for rain defense, a "single" part (w/o subItems)
this.currentCartItems.vaps.push(part);
}
}
this.$refs[modalName].closeModal();
if (this.wipersToDisplay === wipersToDisplayStrings.BOTH) {
this.selectedFrontWipers = false; // set as false if both options are given
this.selectedRearWipers = false; // set as false if both options are given
}
},
updateWipersToDisplay(value) {
this.wipersToDisplay = value;
},
},
computed: {
currentCartItems() {
return this.modelValue;
const modelValue = this.modelValue;
if (!modelValue.vaps) modelValue.vaps = [];
return modelValue;
},
showAddRainDefense() {
// show if not in cart
return !this.currentCartItems.vaps?.some((vap) => {
return vap?.partType === "RAIN DEFENSE";
});
},
showAddWipers() {
// show if not in cart
if (this.currentCartItems && this.currentCartItems.vaps) {
return !this.currentCartItems.vaps.some((vap) => {
return vap?.partType?.includes(" WIPER");
});
} else {
return false;
}
},
rainDefenseItem() {
rainDefenseModal() {
const lineItem = this.availableLineItems.find((item) => {
return item.partType === "RAIN DEFENSE";
return item.partType === partTypeStrings.RAIN_DEFENSE;
});
if (lineItem)
lineItem.listPrice = parseFloat(
@ -102,63 +157,161 @@ export default {
).toFixed(2);
return lineItem;
},
wipersItem() {
const wiperLineItems = this.availableLineItems.filter((item) => {
return item.partType.includes("WIPER");
wipersModal() {
// create a single wiper item for modal
const frontWiperLineItems = this.availableLineItems.filter((item) => {
return item.partType.includes(partTypeStrings.FRONT_WIPER);
});
let wiperLineItem;
if (wiperLineItems.length === 1) {
wiperLineItem = wiperLineItems[0];
wiperLineItem.listPrice = parseFloat(
wiperLineItem.laborAmount + wiperLineItem.sellingPrice + wiperLineItem.kitPrice
).toFixed(2);
return wiperLineItem;
} else if (wiperLineItems.length > 1) {
let combinedPrice = 0;
wiperLineItems?.forEach((item) => {
combinedPrice += item?.laborAmount + item?.sellingPrice + item?.kitPrice;
});
wiperLineItem = {
description: "COMBINED ITEM",
partType: "WIPERS",
subItems: [],
listPrice: parseFloat(combinedPrice).toFixed(2),
};
wiperLineItems?.forEach((item) => {
wiperLineItem.subItems.push(item);
});
return wiperLineItem;
}
return [];
const rearWiperLineItems = this.availableLineItems.filter((item) => {
return item.partType.includes(partTypeStrings.REAR_WIPER);
});
let wipersModal;
let totalFrontPrice = 0;
let totalRearPrice = 0;
// calculate prices to display
frontWiperLineItems?.forEach((item) => {
totalFrontPrice += item?.laborAmount + item?.sellingPrice + item?.kitPrice;
});
rearWiperLineItems?.forEach((item) => {
totalRearPrice += item?.laborAmount + item?.sellingPrice + item?.kitPrice;
});
wipersModal = {
description: "WIPERS ITEM",
partType: "WIPERS",
frontWiperLineItems: frontWiperLineItems,
rearWiperLineItems: rearWiperLineItems,
totalFrontPrice: parseFloat(totalFrontPrice).toFixed(2),
totalRearPrice: parseFloat(totalRearPrice).toFixed(2),
};
return wipersModal;
},
answersCmsData() {
return this.getCmsContent(this.vapsTilesCmsName, "Answers");
},
answersToDisplay() {
const answers = [];
if (!this.answersCmsData) return answers;
buttonsToDisplay() {
const buttons = [];
if (!this.answersCmsData) return buttons;
const rainDefenseInCart = this.currentCartItems.vaps?.some((vap) => {
return vap?.partType === partTypeStrings.RAIN_DEFENSE;
});
const frontWipersInCart = this.currentCartItems.vaps?.some((vap) => {
return vap?.partType?.includes(partTypeStrings.FRONT_WIPER);
});
const rearWipersInCart = this.currentCartItems.vaps?.some((vap) => {
return vap?.partType?.includes(partTypeStrings.REAR_WIPER);
});
const getAnswer = (name, answers) => {
const answerIndex = answers.findIndex((answer) => {
return answer.Name === name;
});
return answers[answerIndex];
};
if (this.showAddWipers) {
const answer = getAnswer("WipersModal", this.answersCmsData);
answer.SubText = "+$" + this.wipersItem.listPrice;
answers.push(answer);
if (!rainDefenseInCart) {
// NO RAIN DEFENSE IN CART; SHOW RAIN DEFENSE BUTTON
const modalData = getAnswer("RainDefenseModal", this.answersCmsData);
modalData.SubText = "+$" + this.rainDefenseModal?.listPrice;
buttons.push(modalData);
}
if (this.showAddRainDefense) {
const answer = getAnswer("RainDefenseModal", this.answersCmsData);
answer.SubText = "+$" + this.rainDefenseItem.listPrice;
answers.push(answer);
const modalData = getAnswer("WipersModal", this.answersCmsData);
if (!frontWipersInCart) {
if (!rearWipersInCart && this.wipersModal?.rearWiperLineItems.length > 0) {
// NO REAR WIPERS or FRONT WIPERS IN CART; SHOW COMBO BUTTON
const prices = [
this.wipersModal?.totalFrontPrice,
this.wipersModal?.totalRearPrice,
];
prices.sort((a, b) => a - b);
modalData.SubText = "+$" + prices[0] + " - $" + prices[prices.length - 1];
this.updateWipersToDisplay(wipersToDisplayStrings.BOTH);
buttons.push(modalData);
} else {
// NO FRONT WIPERS IN CART; SHOW ONLY FRONT WIPERS BUTTON
modalData.SubText = "+$" + this.wipersModal?.totalFrontPrice;
this.updateWipersToDisplay(wipersToDisplayStrings.FRONT);
buttons.push(modalData);
}
} else if (!rearWipersInCart && this.wipersModal?.rearWiperLineItems.length > 0) {
// NO REAR WIPERS IN CART; SHOW ONLY REAR WIPERS BUTTON
modalData.SubText = "+$" + this.wipersModal?.totalRearPrice;
this.updateWipersToDisplay(wipersToDisplayStrings.REAR);
buttons.push(modalData);
}
return answers;
return buttons;
},
wipersToDisplayStrings() {
return wipersToDisplayStrings;
},
},
components: {
buttonQuestion,
contentGroupModal,
checkboxQuestion,
},
};
</script>
<style lang="scss">
.add-vaps-buttons {
// necessary overrides for modals
.modal {
&.modal-component {
.modal-dialog {
.modal-content {
.modal-body {
padding-bottom: 1.5rem;
& > img {
margin-bottom: 1.5rem;
line-height: 2;
text-align: center;
}
& > h5 {
text-align: center;
}
p {
margin-bottom: 1rem;
}
p.subheader-text {
text-align: center;
}
p.price {
text-align: center;
}
.wiper-options {
& > div {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
.form-check {
margin-bottom: 0;
}
}
p {
margin-bottom: 0;
}
p.price {
font-size: 0.875rem;
padding-left: 0.5rem;
color: $green;
}
}
}
}
}
}
}
}
</style>

View file

@ -50,12 +50,13 @@ function setupMocks() {
lineItems: [],
order: {
payment: {
piaErrorCode: "",
isPia: false,
},
},
};
const mountOptions = getMountOptions({
route: { query: {}, params: {} },
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),

View file

@ -35,7 +35,7 @@
<alert
ref="piaErrorAlert"
class="my-4"
v-if="displayPiaAlert"
v-if="shouldDisplayPiaAlert"
cmsWidgetName="PIAErrorAlertWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
@ -262,7 +262,6 @@ export default {
}
},
backButtonAction() {
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
@ -276,42 +275,38 @@ export default {
await this.dispatchStoreAction(storeActions.SAVE_PROMOS, this.lineItems.promos, false);
switch (this.paymentMethod) {
case paymentMethods.CREDIT_CARD:
this.setupPia(this.paymentMethod);
break;
case paymentMethods.PAYPAL:
this.setupPia(this.paymentMethod);
break;
case paymentMethods.AFTERPAY:
this.setupPia(this.paymentMethod);
break;
default:
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
this.dispatchStoreAction(this.storeActions.SAVE_WORK_ORDER_FLAG, true, false);
await submitWorkOrder({ pageNameToLog: "payment-method" });
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
if (this.paymentMethod == paymentMethods.LATER) {
// this creates the final work order
await submitWorkOrder({ pageNameToLog: "payment-method", submitAfterSave: true });
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
} else {
this.setupPia();
}
},
async setupPia(payNowType) {
async setupPia() {
this.$refs.loadingModal.showModal();
// since we're leaving the site for pia, clear any save session promises that we will not be able to resolve when we return
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
// make sure pia error codes are reset
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
// if we don't have a work order in delete substatus, set the flag and submit
// if we don't have a work order in delete substatus, set the flag and submit.
// we need a work order number for pia so we can pass it to safeliteHop.
if (!store.getters.order.workOrderNumber) {
this.dispatchStoreAction(storeActions.SAVE_PIA_WORK_ORDER, true);
await submitWorkOrder({ pageNameToLog: "payment-method" });
this.dispatchStoreAction(storeActions.SAVE_PIA_WORK_ORDER, false);
try {
await submitWorkOrder({
pageNameToLog: "payment-method",
submitAfterSave: false,
createDeleteStatusWorkOrderForPia: true,
});
} catch (error) {
console.log("error: response from pia submit work order:" + error.message);
this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
this.$route.params[this.routerParams.DISPLAY_PIA_ALERT] = true;
return;
}
}
this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_PAY_NOW,
this.$route
@ -340,11 +335,8 @@ export default {
isPiaDisabled() {
const piaExperience =
this.getSettingValue(experimentSettings.PIA_EXPERIENCE) === "PIA Optional";
const enablePIA =
this.getSettingValue(experimentSettings.SUBMIT_ORDER_ENABLE_PIA) === "true";
return false;
//return !(piaExperience && enablePIA);
return !piaExperience;
},
paymentMethod() {
if (this.isPiaDisabled) {
@ -353,8 +345,8 @@ export default {
return this.paymentMethodInternalModel;
},
displayPiaAlert() {
return store.getters.order.payment.piaErrorCode ? true : false;
shouldDisplayPiaAlert() {
return this.$route.params[this.routerParams.DISPLAY_PIA_ALERT];
},
},
watch: {

View file

@ -14,6 +14,7 @@ import baseMixin from "@/mixins/base-mixin.js";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form } from "vee-validate";
import { paymentMethods } from "@/constants/payment-method-constants";
import { routerParams } from "@/router/router-constants/router-params";
export default {
name: "payment-pia-return",
@ -23,12 +24,12 @@ export default {
if (piaError) {
console.log("Error during payment: " + piaError);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
piaError,
false
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: true }
);
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_ERROR, this.$route);
} else {
switch (store.getters.order.payment.piaType) {
case paymentMethods.CREDIT_CARD:
@ -41,14 +42,11 @@ export default {
default:
var msg = "Unknown Pia type: " + store.getters.order.payment.piaType;
console.log(msg);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
msg,
false
);
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.$route
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: true }
);
}
}
@ -60,16 +58,12 @@ export default {
async processPaypalResponse() {
const token = getQuerystringParameter(queryStrings.TOKEN);
const payerId = getQuerystringParameter(queryStrings.PAYERID);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_WORK_ORDER_FLAG, true, false);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PAYPAL_TOKEN, token, false);
await this.saveAndSubmitWorkOrder();
},
async processCreditCardResponse() {
const subscriptionID = getQuerystringParameter(queryStrings.SUBSCRIPTIONID);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, null, false);
baseMixin.methods.dispatchStoreAction(storeActions.SAVE_WORK_ORDER_FLAG, true, false);
const referralSeqNum = getQuerystringParameter(queryStrings.REFERRAL_SEQ_NUM);
if (referralSeqNum != store.getters.order.referralSequenceNumber) {
@ -79,12 +73,12 @@ export default {
" " +
store.getters.order.referralSequenceNumber
);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_PIA_ERROR_CODE,
"unknown error",
false
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: true }
);
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_ERROR, this.$route);
} else {
const expMonth = getQuerystringParameter(queryStrings.CARD_EXPIRATION_MONTH);
const expYear = getQuerystringParameter(queryStrings.CARD_EXPIRATION_YEAR);
@ -119,7 +113,23 @@ export default {
async saveAndSubmitWorkOrder() {
// Final work order submit after returning from PIA.
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
await submitWorkOrder({ pageNameToLog: "payment-pia-return" });
try {
await submitWorkOrder({
pageNameToLog: "payment-pia-return",
submitAfterSave: true,
});
} catch (error) {
console.log("error: response from submit work order:" + error.message);
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: true }
);
this.$refs.loadingModal.isModalVisible = false;
return;
}
this.$refs.loadingModal.isModalVisible = false;
this.$router.navigateWithoutSaving(this.navigationScenarios.PIA_SUCCESS, this.$route);
},

View file

@ -20,21 +20,52 @@
scrolling="no">
</iframe>
</div>
<div class="fade-on-route-transition sub-container make-tall">
<navbar
cmsWidgetName="FunnelFooterWidget"
isForwardActionDisabled="true"
:isBackButtonHidden="shouldHideBackButton"
isSubmitHidden="true"
@back-clicked="backButtonAction" />
<div>{{ switchPaymentText }}</div>
<div>
<button
v-if="paymentType !== 'cc'"
ref="switchToCCButton"
type="button"
@mousedown="switchPIAMethod('cc')"
aria-label="Switch to Credit Card">
Switch to Credit Card
</button>
</div>
<div v-if="paymentType !== 'cc'">or</div>
<div>
<button
v-if="paymentType !== 'pp'"
ref="switchToPPButton"
type="button"
@mousedown="switchPIAMethod('pp')"
aria-label="Switch to PayPal">
Switch to PayPal
</button>
</div>
<div v-if="paymentType === 'cc'">or</div>
<div>
<button
v-if="paymentType !== 'ap'"
ref="switchToAPButton"
type="button"
@mousedown="switchPIAMethod('ap')"
aria-label="Switch to Afterpay">
Switch to Afterpay
</button>
</div>
<navbar
cmsWidgetName="FunnelFooterWidget"
isForwardActionDisabled="true"
:isBackButtonHidden="shouldHideBackButton"
isSubmitHidden="true"
@back-clicked="backButtonAction" />
</div>
</div>
</div>
</Form>
<form
ref="myForm"
ref="hopForm"
id="card-data"
:target="paypalPayment ? '_top' : 'card-frame'"
method="POST"
@ -54,6 +85,7 @@
<input type="hidden" name="sgNoKeystrokeProcessing" value="false" />
<input type="hidden" name="maskCharacter" value="*" />
<input type="hidden" name="styleSheetCode" :value="dynamicCSSUrl" />
<input type="hidden" name="styleSheetCode2" :value="dynamicHopCSSUrl" />
<input type="hidden" name="sgReceiptResponseURL" :value="piaResponseUrl" />
@ -167,6 +199,7 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { applicationConfig } from "@/constants/application-config";
import { paymentMethods } from "@/constants/payment-method-constants";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import baseMixin from "@/mixins/base-mixin.js";
// Validation
@ -234,6 +267,10 @@ export default {
});
},
computed: {
dynamicCSSUrl() {
const { protocol, hostname, port } = window.location;
return `${protocol}//${hostname}:${port}/fmg/css/site-2020Styling.css`;
},
dynamicHopCSSUrl() {
const { protocol, hostname, port } = window.location;
return `${protocol}//${hostname}:${port}/fmg/css/hop-styling.css`;
@ -250,11 +287,71 @@ export default {
}
return "";
},
switchPaymentText() {
return this.getCmsContent("PaymentMethodWidget", "QuestionText");
},
},
methods: {
arePagePrerequisitesValid() {
// TODO
return true;
// Line Items
const packageReqs = !!store.getters.order.lineItems.supportingItems;
// Service Location
const serviceLocation = store.getters.order.serviceLocation;
const mobileReqs = !!(
serviceLocation.address &&
serviceLocation.city &&
serviceLocation.state &&
serviceLocation.zipCode
);
const providerLocation = serviceLocation.provider.address;
const dropOffInshopReqs = !!(
providerLocation.streetAddress &&
providerLocation.city &&
providerLocation.state &&
providerLocation.zipCode
);
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
const serviceLocationReqs =
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
// Insurance
const isInsuranceSet = store.getters.order.payment.isInsurance !== null;
// Schedule
const schedule = store.getters.order.schedule;
const scheduleReqs = !!(
schedule.date &&
schedule.startTime &&
(!isMobile || schedule.endTime) &&
schedule.jobMaxMinutes &&
schedule.jobMinMinutes
);
// Customer
const customer = store.getters.order.customer;
const customerReqs = !!(
customer.firstName &&
customer.lastName &&
customer.phoneNumber &&
customer.emailAddress
);
const paymentMethodReqs =
store.getters.order.payment.isPia !== null &&
(store.getters.order.payment.isPia || !!store.getters.order.payment.piaType);
return (
packageReqs &&
serviceLocationReqs &&
isInsuranceSet &&
scheduleReqs &&
customerReqs &&
paymentMethodReqs
);
},
getWOrkOrderNumber() {
if (store.getters.order.workOrderNumber) {
@ -262,8 +359,6 @@ export default {
return items[1];
}
// no work order, set pia error and return
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, "NO WORK ORDER", false);
this.backButtonAction();
},
getInvoiceNumber() {
@ -271,8 +366,6 @@ export default {
return store.getters.order.workOrderNumber.replace("-", "");
}
// no work order, set pia error and return
this.dispatchStoreAction(storeActions.SAVE_PIA_ERROR_CODE, "NO WORK ORDER", false);
this.backButtonAction();
},
getAddress1() {
@ -351,6 +444,28 @@ export default {
window.location = applicationConfig.PIA_CANCEL_URL;
//this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
switchPIAMethod(payMethod) {
this.paymentType = payMethod;
this.submitHopForm();
},
submitHopForm() {
this.$nextTick(() => {
this.$refs.hopForm.submit();
const iframe = this.$refs.paymentFrame;
if (iframe) {
iframe.onload = () => {
if (iframe.contentWindow) {
if (this.paymentType == "cc") {
iframe.contentWindow.postMessage("CreditCardChosen", "*");
} else if (this.paymentType == "ap") {
iframe.contentWindow.postMessage("afterpay", "*");
}
}
};
}
});
},
async fetchSignatureInfo(signatureInfo) {
if (this.isPaypal()) {
this.$refs.loadingModal.showModal();
@ -360,20 +475,7 @@ export default {
this.authSignature = signatureInfo.signature;
this.authSignatureStart = signatureInfo.startDate;
this.$nextTick(() => {
this.$refs.myForm.submit();
if (this.paymentType == "ap") {
const iframe = this.$refs.paymentFrame;
if (iframe) {
iframe.onload = () => {
if (iframe.contentWindow) {
iframe.contentWindow.postMessage("afterpay", "*");
}
};
}
}
});
this.submitHopForm();
},
},
components: {
@ -387,8 +489,11 @@ export default {
<style lang="scss">
.hop-iframe {
position: relative;
min-height: 1600px;
min-height: 1320px;
width: 100%;
border: 0 none;
}
#pageLoadingModal {
background-color: red;
}
</style>

View file

@ -1,5 +1,6 @@
const routerParams = {
DISPLAY_VEHICLE_CHANGE_ALERT: "displayVehicleChangeAlert",
DISPLAY_PIA_ALERT: "displayPiaAlert",
};
export { routerParams };

View file

@ -28,7 +28,6 @@ import { getDateDifferenceInDays } from "@/helpers/date-helper";
// Export State
const getDefaultState = () => {
return {
submitAfterSave: false,
order: {
vehicle: {
year: null,
@ -97,7 +96,6 @@ const getDefaultState = () => {
parentAccountNumber: 0,
isPia: null,
piaType: null,
piaErrorCode: null,
inactivePromos: null,
paypalToken: null,
ccToken: {
@ -128,7 +126,6 @@ const getDefaultState = () => {
referralCorrelationId: null,
eon: null,
workOrderNumber: null,
createDeleteStatusWorkOrderForPia: false,
},
applicationUser: {
eventBus: [],
@ -148,10 +145,6 @@ export const state = getDefaultState();
// Export Mutations
export const mutations = {
updateWorkOrderFlag(state, submitAfterSave) {
state.submitAfterSave = submitAfterSave;
},
// VEHICLE MUTATIONS
updateYear(state, year) {
state.order.vehicle.year = year;
@ -249,12 +242,6 @@ export const mutations = {
updatePiaType(state, piaType) {
state.order.payment.piaType = piaType;
},
updatePiaErrorCode(state, piaErrorCode) {
state.order.payment.piaErrorCode = piaErrorCode;
},
updatePiaWorkOrder(state, piaWorkOrder) {
state.order.createDeleteStatusWorkOrderForPia = piaWorkOrder;
},
updateWorkOrderNumber(state, workOrderNumber) {
state.order.workOrderNumber = workOrderNumber;
},
@ -570,7 +557,6 @@ export const mutations = {
// Export Getters
export const getters = {
submitAfterSave: (state) => state.submitAfterSave,
vehicle: (state) => state.order.vehicle,
eventBusItem: (state) => (eventCategory, eventSubCategory) => {
const matchedEvent = state.applicationUser.eventBus.find(
@ -697,6 +683,7 @@ export const getters = {
},
experimentSettings: (state) =>
state.applicationUser.experiments
.filter((x) => !!x.isActive)
.map((x) => x.settings)
.reduce((r, c) => Object.assign(r, c), {}) ?? {},
};
@ -1581,13 +1568,17 @@ export const actions = {
},
// Session API Actions
saveSession(context, { pageNameToLog }) {
saveSession(context, { pageNameToLog, payload }) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
const applicationUser = context.getters.applicationUser;
const lineItems = context.state.order.lineItems;
const submitAfterSave = payload?.submitAfterSave === "true";
const createDeleteStatusWorkOrderForPia =
payload?.createDeleteStatusWorkOrderForPia === "true";
// create a new array to avoid mutating state
const newGlassToReplace = convertGlassPieceNamingForApi(damage.glassToReplace);
@ -1595,7 +1586,7 @@ export const actions = {
method: endpoints.SaveSession.method,
endpoint: endpoints.SaveSession.url,
payload: {
submitAfterSave: context.state.submitAfterSave,
submitAfterSave: submitAfterSave,
userAgent: navigator.userAgent,
applicationUser: {
crmCustomerId: applicationUser.crmCustomerId,
@ -1706,7 +1697,7 @@ export const actions = {
referralNumber: order.referralNumber?.toString(),
referralSequenceNumber: order.referralSequenceNumber,
eon: order.eon,
createDeleteStatusWorkOrderForPia: order.createDeleteStatusWorkOrderForPia,
createDeleteStatusWorkOrderForPia: createDeleteStatusWorkOrderForPia,
},
},
logApiCall: true,
@ -1777,15 +1768,6 @@ export const actions = {
);
},
saveWorkOrderFlag(context, submitAfterSave) {
context.commit(storeMutations.UPDATE_WORK_ORDER_FLAG, submitAfterSave);
},
savePiaErrorCode(context, piaErrorCode) {
context.commit(storeMutations.UPDATE_PIA_ERROR_CODE, piaErrorCode);
},
savePiaWorkOrder(context, piaWorkOrder) {
context.commit(storeMutations.UPDATE_PIA_WORK_ORDER, piaWorkOrder);
},
saveCCToken(context, ccToken) {
context.commit(storeMutations.UPDATE_CCTOKEN, ccToken);
},

View file

@ -2791,17 +2791,6 @@ describe("Actions", () => {
});
describe("Getters", () => {
it("submitAfterSave, should return true", () => {
// Arrange
const storeState = state;
// Act
mutations.updateWorkOrderFlag(storeState, true);
// Assert
expect(getters.submitAfterSave(storeState)).toEqual(true);
});
it("Vehicle getter, should return vehicle data", () => {
// Arrange
const storeState = state;