Merge pull request #1514 from Safelite/feature/CSR-1393.1
Feature/csr 1393.1
This commit is contained in:
commit
eaab096705
7 changed files with 379 additions and 7 deletions
|
|
@ -33,6 +33,7 @@ const errorMessages = {
|
||||||
MAKE_REQUIRED: "Please select your vehicle make",
|
MAKE_REQUIRED: "Please select your vehicle make",
|
||||||
MODEL_REQUIRED: "Please select your vehicle model",
|
MODEL_REQUIRED: "Please select your vehicle model",
|
||||||
STYLE_REQUIRED: "Please select your vehicle style",
|
STYLE_REQUIRED: "Please select your vehicle style",
|
||||||
|
PROMO_REQUIRED: "Please enter a promo code",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { errorMessages };
|
export { errorMessages };
|
||||||
|
|
|
||||||
|
|
@ -221,15 +221,15 @@ export default {
|
||||||
let cartItem = null;
|
let cartItem = null;
|
||||||
|
|
||||||
cartItem = {
|
cartItem = {
|
||||||
name: promoLineItem.promoCode,
|
name: promoLineItem?.promoCode, // get from CMS?
|
||||||
category: cartItemCategories.PROMOS,
|
category: cartItemCategories.PROMOS,
|
||||||
cartItemType: promoLineItem.partType,
|
cartItemType: promoLineItem?.partType,
|
||||||
isDisplayed: true,
|
isDisplayed: true,
|
||||||
subTotal:
|
subTotal:
|
||||||
(promoLineItem.kitPrice ?? 0) +
|
(promoLineItem?.kitPrice ?? 0) +
|
||||||
(promoLineItem.laborAmount ?? 0) +
|
(promoLineItem?.laborAmount ?? 0) +
|
||||||
(promoLineItem.sellingPrice ?? 0),
|
(promoLineItem?.sellingPrice ?? 0),
|
||||||
salesTax: promoLineItem.salesTax,
|
salesTax: promoLineItem?.salesTax,
|
||||||
lineItems: [],
|
lineItems: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,13 @@
|
||||||
recyclingModalCmsWidgetName="RecycleModal" />
|
recyclingModalCmsWidgetName="RecycleModal" />
|
||||||
|
|
||||||
<hr class="my-5" />
|
<hr class="my-5" />
|
||||||
|
<promoModalQuestion
|
||||||
|
class="pb-3"
|
||||||
|
v-model="lineItems"
|
||||||
|
:availableVaps="availableVaps"
|
||||||
|
@added-to-cart="showAlert"
|
||||||
|
linkWidgetName="PromoLinkWidget"
|
||||||
|
modalWidgetName="PromoModalWidget" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<alert
|
<alert
|
||||||
|
|
@ -73,6 +80,7 @@ import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-heade
|
||||||
import paymentMethodQuestion from "@/layouts/payment-method/payment-method-question/payment-method-question";
|
import paymentMethodQuestion from "@/layouts/payment-method/payment-method-question/payment-method-question";
|
||||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||||
import cart from "@/fmg-components/cart/cart";
|
import cart from "@/fmg-components/cart/cart";
|
||||||
|
import promoModalQuestion from "@/layouts/payment-method/promo-modal-question/promo-modal-question";
|
||||||
import addVapsModalButtons from "./add-vaps-modal-buttons/add-vaps-modal-buttons";
|
import addVapsModalButtons from "./add-vaps-modal-buttons/add-vaps-modal-buttons";
|
||||||
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
||||||
import alert from "@/ux-components/alert/alert";
|
import alert from "@/ux-components/alert/alert";
|
||||||
|
|
@ -487,6 +495,7 @@ export default {
|
||||||
alert,
|
alert,
|
||||||
addVapsModalButtons,
|
addVapsModalButtons,
|
||||||
paymentMethodQuestion,
|
paymentMethodQuestion,
|
||||||
|
promoModalQuestion,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<textLink
|
||||||
|
id="promoLinkPromptId"
|
||||||
|
linkType="text"
|
||||||
|
:text="promoLinkText"
|
||||||
|
href="#!"
|
||||||
|
@click-event="openModal" />
|
||||||
|
<modal
|
||||||
|
:ref="modalName"
|
||||||
|
:headerText="modalHeaderText"
|
||||||
|
:footerButtonText="modalFooterText"
|
||||||
|
@footer-button-event="addPromoCode">
|
||||||
|
<promoQuestion
|
||||||
|
ref="promoQuestion"
|
||||||
|
customInputId="promoCode"
|
||||||
|
v-model="promoCode"
|
||||||
|
cmsWidgetName="PromoQuestionWidget" />
|
||||||
|
<alert
|
||||||
|
ref="alertInvalidPromo"
|
||||||
|
v-if="displayInvalidPromoAlert"
|
||||||
|
v-html="InvalidPromoText"
|
||||||
|
class="my-4 alert"
|
||||||
|
cmsWidgetName="AlertInvalidPromoWidget"
|
||||||
|
alertClass="alert-danger"
|
||||||
|
v-bind:isDismissible="true" />
|
||||||
|
<div v-for="(promo, i) in this.getPromoList()" :key="i">
|
||||||
|
<span class="applied-promo">Promo code "{{ promo.promoCode }}" applied </span>
|
||||||
|
<textLink
|
||||||
|
ref="removeLink"
|
||||||
|
linkType="text"
|
||||||
|
:text="removeLinkText"
|
||||||
|
href="#!"
|
||||||
|
@click-event="removeItem(promo.promoCode)" />
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
|
import promoQuestion from "@/layouts/payment-method/promo-modal-question/promo-question/promo-question";
|
||||||
|
import modal from "@/digital-components/modal/modal";
|
||||||
|
import alert from "@/ux-components/alert/alert";
|
||||||
|
import { storeActions } from "@/constants/store-actions.js";
|
||||||
|
import { getVapsThatNeedToBeAddedToSatisfyPromos } from "@/helpers/promotions-helper";
|
||||||
|
import { deepClone } from "@/helpers/object-helper";
|
||||||
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
|
import { cartItemCategories } from "@/constants/cart-item-categories";
|
||||||
|
import store from "@/store";
|
||||||
|
import { mapTaxedLineItemsToStoreFormat } from "@/store";
|
||||||
|
export default {
|
||||||
|
name: "promo-modal-question",
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
promoCode: "",
|
||||||
|
promoTextInputId: "",
|
||||||
|
displayInvalidPromoAlert: false,
|
||||||
|
displayInvalidOnOrderPromoAlert: false,
|
||||||
|
lineItems: deepClone(this.modelValue),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
linkWidgetName: String,
|
||||||
|
modalWidgetName: String,
|
||||||
|
availableVaps: Object,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
promoLinkText() {
|
||||||
|
return this.getCmsContent(this.linkWidgetName, "BodyText");
|
||||||
|
},
|
||||||
|
modalHeaderText() {
|
||||||
|
return this.getCmsContent("PromoQuestionWidget", "QuestionText");
|
||||||
|
},
|
||||||
|
modalFooterText() {
|
||||||
|
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||||
|
},
|
||||||
|
modalName() {
|
||||||
|
return this.modalWidgetName;
|
||||||
|
},
|
||||||
|
modal() {
|
||||||
|
return this.$refs[this.modalName];
|
||||||
|
},
|
||||||
|
PromoOnOrderText() {
|
||||||
|
return this.getCmsContent("AlertInvalidPromoOnOrderWidget", "HeadlineText");
|
||||||
|
},
|
||||||
|
InvalidPromoOnOrderText() {
|
||||||
|
return this.PromoText?.replaceAll("{custom:NEWPROMOCODE}", this.promoCode).replaceAll(
|
||||||
|
"{custom:OLDPROMOCODE}",
|
||||||
|
this.promoCode
|
||||||
|
);
|
||||||
|
},
|
||||||
|
PromoText() {
|
||||||
|
return this.getCmsContent("AlertInvalidPromoWidget", "HeadlineText");
|
||||||
|
},
|
||||||
|
InvalidPromoText() {
|
||||||
|
return this.PromoText?.replaceAll("{custom:PROMOCODE}", this.promoCode);
|
||||||
|
},
|
||||||
|
AddPromoSuccessMessageFromCms() {
|
||||||
|
return this.getCmsContent("PromoSuccessAlertWidget", "Text");
|
||||||
|
},
|
||||||
|
removeLinkText() {
|
||||||
|
return this.getCmsContent("RemoveCartItemTextWidget", "Text");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetAlerts() {
|
||||||
|
this.displayInvalidPromoAlert = false;
|
||||||
|
},
|
||||||
|
resetsOnPromoInput() {
|
||||||
|
this.resetAlerts();
|
||||||
|
},
|
||||||
|
getPromoList() {
|
||||||
|
return this.lineItems.promos;
|
||||||
|
},
|
||||||
|
openModal() {
|
||||||
|
this.modal.openModal();
|
||||||
|
},
|
||||||
|
closeModal() {
|
||||||
|
this.modal.closeModal();
|
||||||
|
},
|
||||||
|
focusOnPromoInput() {
|
||||||
|
const input = document.getElementById(this.promoTextInputId);
|
||||||
|
input?.focus();
|
||||||
|
},
|
||||||
|
resetModalButtonStyle() {
|
||||||
|
this.modal.resetButtonStyle();
|
||||||
|
},
|
||||||
|
getPromoAddedMessage() {
|
||||||
|
return this.AddPromoSuccessMessageFromCms?.replaceAll(
|
||||||
|
"{custom:PROMOCODE}",
|
||||||
|
this.promoCode
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async getPromoCodeData(promoCode, lineItems, availableVaps, pageNameToLog = null) {
|
||||||
|
const pageName = pageNameToLog ?? this.$options?.name;
|
||||||
|
//const addableVaps = getAddableVapsFromAvailableLineItems(availableLineItems);
|
||||||
|
//const lineItemsToUse = lineItems.Length > 0 ? lineItems : null;
|
||||||
|
const promoValidationResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||||
|
storeActions.VALIDATE_ORDER_PROMO_AND_SAVE_SERVER_DATA,
|
||||||
|
{
|
||||||
|
promoCode: promoCode,
|
||||||
|
lineItemsToUse: lineItems,
|
||||||
|
addableVaps: availableVaps,
|
||||||
|
},
|
||||||
|
pageName,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: !("errorCode" in promoValidationResponse),
|
||||||
|
promoCode: promoValidationResponse?.orderPromos,
|
||||||
|
errorCode: promoValidationResponse?.errorCode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
removeItem(promo) {
|
||||||
|
//let lineItems = this.modelValue;
|
||||||
|
this.lineItems[cartItemCategories.PROMOS] = this.lineItems[
|
||||||
|
cartItemCategories.PROMOS
|
||||||
|
].filter((lineItemsToKeep) => lineItemsToKeep.promoCode != promo);
|
||||||
|
|
||||||
|
this.$emit("update:modelValue", this.lineItems);
|
||||||
|
},
|
||||||
|
async addPromoCode() {
|
||||||
|
if (this.promoCode) {
|
||||||
|
this.resetAlerts();
|
||||||
|
const promoCodeData = await this.getPromoCodeData(
|
||||||
|
this.promoCode,
|
||||||
|
this.lineItems,
|
||||||
|
this.availableVaps,
|
||||||
|
"payment-method"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (promoCodeData.isValid) {
|
||||||
|
const addedPromo = this.lineItems?.promos.find(
|
||||||
|
(promoItem) =>
|
||||||
|
promoItem?.promoCode == promoCodeData.promoCode?.[0]?.promoCode
|
||||||
|
);
|
||||||
|
if (addedPromo) {
|
||||||
|
this.displayInvalidPromoAlert = true;
|
||||||
|
this.focusOnPromoInput();
|
||||||
|
this.resetModalButtonStyle();
|
||||||
|
} else {
|
||||||
|
const pricedLineItemsToTax = [];
|
||||||
|
pricedLineItemsToTax.push(...promoCodeData.promoCode);
|
||||||
|
const taxedLineItems =
|
||||||
|
await baseMixin.methods.dispatchStoreActionWithLogging(
|
||||||
|
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||||
|
{
|
||||||
|
billToAccountNumber: "87291",
|
||||||
|
providerNumber:
|
||||||
|
store.getters.order.serviceLocation.provider.providerNumber,
|
||||||
|
appointmentType:
|
||||||
|
store.getters.order.serviceLocation.appointmentType,
|
||||||
|
serviceLocationCity: store.getters.order.serviceLocation.city,
|
||||||
|
serviceLocationState: store.getters.order.serviceLocation.state,
|
||||||
|
serviceLocationZipCode:
|
||||||
|
store.getters.order.serviceLocation.zipCode,
|
||||||
|
pricedLineItems: pricedLineItemsToTax,
|
||||||
|
},
|
||||||
|
"payment-method",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
// Match all line items to the line items as they are in the store
|
||||||
|
// and rebuild the original structure.
|
||||||
|
this.lineItems = mapTaxedLineItemsToStoreFormat(
|
||||||
|
taxedLineItems,
|
||||||
|
this.lineItems
|
||||||
|
);
|
||||||
|
const taxedVaps = mapTaxedLineItemsToStoreFormat(
|
||||||
|
taxedLineItems,
|
||||||
|
this.availableVaps
|
||||||
|
);
|
||||||
|
|
||||||
|
const getVaps = getVapsThatNeedToBeAddedToSatisfyPromos(
|
||||||
|
promoCodeData.promoCode,
|
||||||
|
taxedVaps,
|
||||||
|
this.lineItems
|
||||||
|
);
|
||||||
|
this.lineItems?.promos.push(...promoCodeData.promoCode);
|
||||||
|
this.lineItems.vaps?.push(...getVaps);
|
||||||
|
this.closeModal();
|
||||||
|
const message = this.getPromoAddedMessage();
|
||||||
|
this.$emit("update:modelValue", this.lineItems);
|
||||||
|
this.$emit("added-to-cart", {
|
||||||
|
messageHeadline: "Promo Applied!",
|
||||||
|
messageCopy: message,
|
||||||
|
type: "alert-success",
|
||||||
|
isDismissible: true,
|
||||||
|
});
|
||||||
|
this.promoCode = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.displayInvalidPromoAlert = true;
|
||||||
|
this.focusOnPromoInput();
|
||||||
|
this.resetModalButtonStyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
promoCode() {
|
||||||
|
this.displayInvalidPromoAlert = false;
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
handler(newValue) {
|
||||||
|
this.lineItems = deepClone(newValue);
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
textLink,
|
||||||
|
promoQuestion,
|
||||||
|
modal,
|
||||||
|
alert,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.applied-promo {
|
||||||
|
padding-right: 5rem;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #0c7e47;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
color: #d4281c;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import promoQuestion from "./promo-question";
|
||||||
|
|
||||||
|
describe("promo-question.vue", () => {
|
||||||
|
it("Should get the modelValue", async () => {
|
||||||
|
// Arrange
|
||||||
|
const text = "test";
|
||||||
|
const wrapper = shallowMount(promoQuestion, {
|
||||||
|
props: {
|
||||||
|
modelValue: text,
|
||||||
|
},
|
||||||
|
attachTo: document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const modelValueText = wrapper.vm.value;
|
||||||
|
wrapper.vm.value = "test also";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(modelValueText).toEqual("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should emit to set value", async () => {
|
||||||
|
// Arrange
|
||||||
|
const text = "test";
|
||||||
|
const wrapper = shallowMount(promoQuestion, {
|
||||||
|
props: {
|
||||||
|
modelValue: text,
|
||||||
|
},
|
||||||
|
attachTo: document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const modelValueText = wrapper.vm.value;
|
||||||
|
wrapper.vm.value = "test also";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted("update:modelValue")).toEqual([["test also"]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<textboxQuestion
|
||||||
|
ref="promoInputTextQuestion"
|
||||||
|
:cmsWidgetName="cmsWidgetName"
|
||||||
|
v-model="value"
|
||||||
|
inputId="promoCode"
|
||||||
|
questionAlignment="center"
|
||||||
|
cornerStyle="rounded"
|
||||||
|
:displayQuestionText="false"
|
||||||
|
isRequired
|
||||||
|
validationRules="promo-required" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||||
|
|
||||||
|
//Supporting Files
|
||||||
|
import { defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
|
||||||
|
// Define Validation Rules
|
||||||
|
defineRule("promo-required", required(errorMessages.PROMO_REQUIRED));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "promo-question",
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
textboxQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -2122,6 +2122,7 @@ export const actions = {
|
||||||
},
|
},
|
||||||
// COMBINE THESE
|
// COMBINE THESE
|
||||||
savePromos(context, promos) {
|
savePromos(context, promos) {
|
||||||
|
addGuidToLineItemsIfNotAlreadyThere(promos);
|
||||||
context.commit(storeMutations.UPDATE_PROMOS, promos);
|
context.commit(storeMutations.UPDATE_PROMOS, promos);
|
||||||
},
|
},
|
||||||
saveInactivePromos(context, inactivePromos) {
|
saveInactivePromos(context, inactivePromos) {
|
||||||
|
|
@ -2805,7 +2806,7 @@ function renameGlassToReplaceAttributes(glassToReplace) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGuidToLineItemsIfNotAlreadyThere(lineItems) {
|
function addGuidToLineItemsIfNotAlreadyThere(lineItems) {
|
||||||
lineItems.forEach((lineItem) => {
|
lineItems?.forEach((lineItem) => {
|
||||||
if (!lineItem.id) {
|
if (!lineItem.id) {
|
||||||
lineItem.id = crypto.randomUUID();
|
lineItem.id = crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue