DigitalConsumer.FixMyGlass/src/layouts/payment/payment.vue
CarlNation 4e1caf8559 CSR-1072
CSR-1072 insurance orders will remove line items depending on the company.  mobile fee, recycle fee, etc.  So we can return from heritage with nothing in supporting items causing exceptions when lineItems is no longer required.
2024-02-19 07:40:07 -05:00

703 lines
28 KiB
Vue

<template>
<Form>
<loadingModal notFullScreen ref="loadingModal" />
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 p-0">
<div>
<alert
ref="piaCCErrorAlert"
class="my-4"
v-if="shouldDisplayPiaCCAlert"
cmsWidgetName="PIACCErrorAlertWidget"
alertClass="alert-danger"
@textLinkClicked="paymentFailedPayLater"
v-bind:isDismissible="false" />
</div>
<div>
<alert
ref="piaAPErrorAlert"
class="my-4"
v-if="shouldDisplayPiaAPAlert"
cmsWidgetName="PIAAPErrorAlertWidget"
alertClass="alert-danger"
@textLinkClicked="paymentFailedPayLater"
v-bind:isDismissible="false" />
</div>
<div id="card-container">
<iframe
v-resize
ref="paymentFrame"
class="hop-iframe payment-iframe"
name="card-frame"
id="card-frame"
seamless
scrolling="no">
</iframe>
</div>
<hr />
<cart
ref="cart"
:damage="damageInfo"
:availableVaps="availableVaps"
:allowItemRemoval="false"
v-model="lineItems"
servicePackageOptionsCmsName="ServicePackageTitle"
recyclingModalCmsWidgetName="RecycleModal" />
<hr class="mb-5" />
<navbar
cmsWidgetName="FunnelFooterWidget"
isForwardActionDisabled="true"
:isBackButtonHidden="shouldHideBackButton"
isSubmitHidden="true"
@back-clicked="backButtonAction" />
</div>
</div>
<div v-if="shouldBlockInteraction" class="ui-block" @click="supressClick"></div>
</div>
</Form>
<form
ref="hopForm"
id="card-data"
:target="isPaypal ? '_top' : 'card-frame'"
method="POST"
:action="checkoutUrl">
<input type="hidden" name="paymentType" :value="paymentType" />
<input type="hidden" name="sgSessionId" :value="authToken" />
<input type="hidden" name="sgAuthToken" :value="authToken" />
<input type="hidden" name="sgSignaturePublic" :value="authSignature" />
<input type="hidden" name="sgSignatureStartDate" :value="authSignatureStart" />
<input type="hidden" name="referralSeqNum" :value="referralSequenceNumber" />
<input type="hidden" name="sge_commerce_indicator_isinternet" value="true" />
<input type="hidden" name="sghopsource" value="Safelite.com" />
<input type="hidden" name="sgHtmlStyle" value="ResourceSafeliteHtml" />
<input type="hidden" name="sgErrorMessagesEmbedded" value="true" />
<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" />
<input type="hidden" name="sgDeclineResponseURL" :value="piaResponseUrl" />
<input type="hidden" name="sgErrorResponseURL" :value="piaResponseUrl" />
<input type="hidden" name="sgheaderline1" :value="getHeaderLine1" />
<input type="hidden" name="sgHeader1subtitle" value="" />
<input type="hidden" name="sgheaderline2" :value="getHeaderLine2" />
<input type="hidden" name="sgheaderline3" value="" />
<input type="hidden" name="sgheaderline4" value="" />
<input type="hidden" name="sgheaderline5" value="*Required information" />
<input type="hidden" name="billTo_firstName" :value="firstName" />
<input type="hidden" name="billTo_firstNameShow" value="true" />
<input type="hidden" name="sghopmiddleInitialShow" value="false" />
<input type="hidden" name="billTo_lastName" :value="lastName" />
<input type="hidden" name="billTo_lastNameShow" value="true" />
<input type="hidden" name="billTo_street1" value="" />
<input type="hidden" name="billTo_street2" value="" />
<input type="hidden" name="billTo_city" value="" />
<input type="hidden" name="billTo_state" value="" />
<input type="hidden" name="billTo_postalCode" value="" />
<input type="hidden" name="billTo_postalCodeShow" value="true" />
<input type="hidden" name="billTo_postalCodeEnable" value="true" />
<input type="hidden" name="sgLabelPostalCode" value="Billing ZIP" />
<input type="hidden" name="sgtotalamount" :value="displayAmount" />
<input type="hidden" name="totalAmountDecimal" :value="totalAmount" />
<input type="hidden" name="lineItems" :value="piaLineItems" />
<input type="hidden" name="sgdiscountstrikethruamount" value="" />
<input type="hidden" name="callerDisplayText" value="" />
<input
type="hidden"
name="sgtermsofusedisplaytext"
value="By selecting submit, I agree to Safelite's" />
<input type="hidden" name="sgtermsofuseurl" value="http://www.safelite.com/terms-of-use/" />
<input type="hidden" name="sgtermsofuselinkdisplaytext" value="terms of use" />
<input type="hidden" name="sgtermsofcancelrefunddisplaytext" value="and" />
<input
type="hidden"
name="sgtermsofcancelrefundlinkdisplaytext"
value="cancellation and refund policy" />
<input
type="hidden"
name="sgtermsofcancelrefundurl"
value="http://www.safelite.com/cancellation-refund-policy" />
<input
type="hidden"
name="sgErrorMessageCardVerification"
value="Credit card could not be processed" />
<input
type="hidden"
name="sgErrorMessageTimeOut"
value="For your security, this transaction has been timed out." />
<input type="hidden" name="sgLabelCardNumber" value="" />
<input type="hidden" name="sgLabelAddressLine1" :value="address1" />
<input type="hidden" name="sgLabelAddressLine2" :value="address2" />
<input type="hidden" name="sgLabelCity" :value="city" />
<input type="hidden" name="sgLabelState" :value="state" />
<input type="hidden" name="sgCtu" :value="ctu" />
<input type="hidden" name="sgWorkOrder" :value="workOrderNumber" />
<input type="hidden" name="sgEmailAddress" :value="emailAddress" />
<input type="hidden" name="paypalInvoiceNumber" :value="invoiceNumber" />
<input type="hidden" name="paypalSuccessUrl" :value="piaResponseUrl" />
<input type="hidden" name="paypalCancelUrl" :value="piaCancelUrl" />
<input type="hidden" name="sgCCDeclineURL" :value="piaCancelUrl" />
<input type="hidden" name="sgCCTimeoutURL" :value="piaCancelUrl" />
<input type="hidden" name="sgTransactionType" value="authorization" />
<input type="hidden" name="amount" :value="totalAmount" />
<input type="hidden" name="ctu" :value="ctu" />
<input type="hidden" name="orderNumber" :value="workOrderNumber" />
<input type="hidden" name="billTo_email" :value="emailAddress" />
<input type="hidden" name="useDecisionManager" value="True" />
<input type="hidden" name="correlationId" :value="referralCorrelationId" />
<input type="hidden" name="sgErrorMessageCard_CVN" value="Please enter a valid CVV" />
<input type="hidden" name="ship_to_address_line1" :value="address1" />
<input type="hidden" name="ship_to_address_line2" :value="address2" />
<input type="hidden" name="ship_to_address_city" :value="city" />
<input type="hidden" name="ship_to_address_state" :value="state" />
<input type="hidden" name="ship_to_address_country" value="US" />
<input type="hidden" name="ship_to_address_postal_code" :value="zipCode" />
<input type="hidden" name="ship_to_phone" :value="phoneNumber" />
<input type="hidden" name="calling_application" value="NextGen" />
</form>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import cart from "@/fmg-components/cart/cart";
import alert from "@/ux-components/alert/alert";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import { externalUrls } from "@/router/router-constants/externalUrl-values";
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";
import { mapTaxedLineItemsToStoreFormat } from "../../store";
import {
revalidatePromosAndValidateQueryStringPromo,
getAddableVapsFromAvailableLineItems,
getVapsThatNeedToBeAddedToSatisfyPromos,
} from "@/helpers/promotions-helper";
import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { deepClone } from "@/helpers/object-helper";
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
import iframeResize from "../../../node_modules/iframe-resizer/js/iframeResizer.js";
import { routerParams } from "@/router/router-constants/router-params";
export default {
name: "payment",
data() {
return {
checkoutUrl: externalUrls.SAFELITE_HOP,
piaResponseUrl: applicationConfig.PIA_RESPONSE_URL,
piaCancelUrl: applicationConfig.PIA_CANCEL_URL,
paymentType: this.getPaymentType(),
authToken: "",
authSignature: "",
authSignatureStart: "",
referralSequenceNumber: store.getters.order.referralSequenceNumber,
emailAddress: store.getters.order.customer.emailAddress,
address1: this.getAddress1(),
address2: this.getAddress2(),
city: this.getCity(),
state: this.getState(),
zipCode: this.getZipCode(),
firstName: store.getters.order.customer.firstName,
lastName: store.getters.order.customer.lastName,
phoneNumber: store.getters.order.customer.phoneNumber,
ctu: store.getters.order.serviceLocation.zipCodeCtu,
referralCorrelationId: store.getters.order.referralCorrelationId,
workOrderNumber: this.getWOrkOrderNumber(),
invoiceNumber: this.getInvoiceNumber(),
totalAmount: this.getAmountDue(),
displayAmount: this.getDisplayAmountDue(),
piaLineItems: "",
lineItems: [],
availableVaps: [],
shouldBlockInteraction: false,
};
},
directives: {
resize: {
mounted: function (el) {
el.addEventListener("load", () => {
iframeResize({ checkOrigin: false }, el);
});
},
unmounted: function (el) {
el.iFrameResizer.removeListeners();
},
},
},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const signaturePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SIGNATURE,
null,
"payment"
);
const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_WIPERS,
{
serviceZipCode: store.getters.order.serviceLocation.zipCode,
carId: store.getters.vehicle.carId,
},
"payment"
);
const rainDefensePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_DEFENSE,
null,
"payment"
);
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "signature",
promise: signaturePromise,
},
{
resultKey: "wipers",
promise: wipersPromise,
},
{
resultKey: "rainDefense",
promise: rainDefensePromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const lineItemsFromStore = deepClone(store.getters.order.lineItems);
const glassParts = lineItemsFromStore.glassParts ?? [];
const supportingItems = lineItemsFromStore.supportingItems ?? [];
const lineItemsToTax = [
resultMap.rainDefense,
...supportingItems,
...resultMap.wipers,
...glassParts,
];
const availableVaps = [resultMap.rainDefense, ...resultMap.wipers];
const pricedLineItemsToTax = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
availableLineItems: lineItemsToTax,
},
"payment",
false
);
// Promo logic
const promoCodeFromQueryString = getQuerystringParameter(queryStrings.PROMO);
const { validatePromoResponse, revalidatePromoResponse } =
await revalidatePromosAndValidateQueryStringPromo(
promoCodeFromQueryString,
pricedLineItemsToTax,
"payment"
);
const newValidatedPromos = validatePromoResponse?.orderPromos ?? [];
newValidatedPromos.push(
...(revalidatePromoResponse ? revalidatePromoResponse.promoLineItems : [])
);
pricedLineItemsToTax.push(...newValidatedPromos);
// End of promo logic
const taxedLineItems = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.TAX_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{
billToAccountNumber: store.getters.payment.billToAccountNumber,
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",
false
);
// Match all line items to the line items as they are in the store
// and rebuild the original structure.
const lineItems = mapTaxedLineItemsToStoreFormat(taxedLineItems, lineItemsFromStore);
const taxedVaps = mapTaxedLineItemsToStoreFormat(taxedLineItems, availableVaps);
// Add items that were not in the store yet but added via query string promo validation
//newValidatedPromos contains both validated and revalidated promos.
lineItems.promos = Array.from(newValidatedPromos);
const vapsToAddToCart = getVapsThatNeedToBeAddedToSatisfyPromos(
newValidatedPromos,
taxedVaps,
lineItems
);
lineItems.vaps = lineItems.vaps ?? [];
lineItems.vaps.push(...vapsToAddToCart);
// Call the "next" function to complete the transition to this page.
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.availableVaps = taxedVaps;
vm.lineItems = lineItems;
vm.$nextTick(() => {
if (vm.$refs.cart) {
const cartItems = vm.$refs.cart.cartItems;
vm.getPiaLineItems(cartItems);
}
vm.fetchSignatureInfo(resultMap.signature);
vm.setIFrameListener();
});
});
},
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`;
},
getHeaderLine1() {
if (!this.isPaypal) {
return this.getCmsContent("PIAHeaderLine1", "Text");
}
return "";
},
getHeaderLine2() {
if (!this.isPaypal) {
return this.getCmsContent("PIAHeaderLine2", "Text");
}
return "";
},
damageInfo() {
return this.$store.getters.damage;
},
isPaypal() {
if (this.paymentType == "pp") {
return true;
}
return false;
},
shouldDisplayPiaCCAlert() {
return this.shouldDisplayPiaAlert(paymentMethods.CREDIT_CARD);
},
shouldDisplayPiaAPAlert() {
return this.shouldDisplayPiaAlert(paymentMethods.AFTERPAY);
},
},
methods: {
arePagePrerequisitesValid() {
// 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 &&
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 (
serviceLocationReqs &&
isInsuranceSet &&
scheduleReqs &&
customerReqs &&
paymentMethodReqs
);
},
getWOrkOrderNumber() {
if (store.getters.order.workOrderNumber) {
const items = store.getters.order.workOrderNumber.split("-");
return items[1];
}
this.backButtonAction();
},
getInvoiceNumber() {
if (store.getters.order.workOrderNumber) {
return store.getters.order.workOrderNumber.replace("-", "");
}
this.backButtonAction();
},
getAddress1() {
return store.getters.order.serviceLocation.address
? store.getters.order.serviceLocation.address
: store.getters.order.serviceLocation.provider.address.streetAddress;
},
getAddress2() {
return store.getters.order.serviceLocation.address2
? store.getters.order.serviceLocation.address2
: null;
},
getCity() {
return store.getters.order.serviceLocation.city
? store.getters.order.serviceLocation.city
: store.getters.order.serviceLocation.provider.address.city;
},
getState() {
return store.getters.order.serviceLocation.state
? store.getters.order.serviceLocation.state
: store.getters.order.serviceLocation.provider.address.state;
},
getZipCode() {
return store.getters.order.serviceLocation.zipCode
? store.getters.order.serviceLocation.zipCode
: store.getters.order.serviceLocation.provider.address.zipCode;
},
getPaymentType() {
switch (store.getters.order.payment.piaType) {
case paymentMethods.AFTERPAY:
return "ap";
case paymentMethods.CREDIT_CARD:
return "cc";
case paymentMethods.PAYPAL:
return "pp";
default:
return store.getters.payment.piaType;
}
},
getPiaLineItems(cartItems) {
const glassParts = store.getters.order.lineItems.glassParts;
let lineItems = [];
if (glassParts === null) {
lineItems = ["Labor|0|1", "Repair supplies|0|1"];
} else {
lineItems = ["Parts and labor|0|1"];
}
cartItems.forEach((item) => {
if (item.name !== null && item.category != "promos") {
lineItems.push(`${item.name}|${(item.salesTax + item.subTotal).toFixed(2)}|1`);
}
});
this.piaLineItems = lineItems.join("||");
},
getAmountDue() {
return baseMixin.methods.getAmountDue(store.getters.order.lineItems);
},
getDisplayAmountDue() {
return baseMixin.methods.getDisplayAmountDue(store.getters.order.lineItems);
},
async paymentFailedPayLater() {
this.$refs.loadingModal.isModalVisible = true;
await this.dispatchStoreAction(
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
paymentMethods.LATER,
false
);
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
try {
await submitWorkOrder({
pageNameToLog: "payment",
submitAfterSave: true,
});
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_ORDER);
window.location = applicationConfig.CONFIRMATION_URL;
} catch (error) {
console.log("error: response from submit work order(payment pg):" + error.message);
this.$router.navigateWithoutSaving(
this.navigationScenarios.PIA_ERROR,
this.$route,
{},
{ [routerParams.DISPLAY_PIA_ALERT]: true }
);
this.$refs.loadingModal.isModalVisible = false;
return;
}
},
backButtonAction() {
// The only way I could figure out how to get navigation to work on a page with the iframe.
// This is also how a cancel from Paypal would work. Just a redirect to the payment-method page.
// The normal navigation sort of works but the page does not render. The "next" callback function does not get
// invoked in payment-method's beforeroute enter. I also spotted an error in vue-router.esm-bundler.js when this
// happens but was unable to ignore it with a try catch. The navigation guard error is:
// Error: Redirected from "/?fmgPage=payment-method" to "{ params: {isSavingNation: "false"}, query: "fmgPage: "payment-method"}"
// I don't know if the navigation guard thinks the to and from are the same page.
// Also tried overriding with a $router.push
window.location = applicationConfig.PIA_CANCEL_URL;
//this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
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();
}
this.authToken = signatureInfo.token;
this.authSignature = signatureInfo.signature;
this.authSignatureStart = signatureInfo.startDate;
this.submitHopForm();
},
handleIFrameContentWindwMessage(event) {
if (event.data.indexOf("afterpayClosed") > -1) {
this.backButtonAction();
}
if (event.data.indexOf("creditCardSubmit") > -1) {
this.setUIBlock(true);
}
},
setIFrameListener() {
window.addEventListener("message", (event) =>
this.handleIFrameContentWindwMessage(event)
);
},
setUIBlock(val) {
this.shouldBlockInteraction = val;
},
supressClick(event) {
if (this.shouldBlockInteraction) {
event.stopPropagation();
}
},
shouldDisplayPiaAlert(payMethod) {
return (
this.$route.query[queryStrings.DISPLAY_PIA_ALERT] === payMethod ||
this.$route.params[routerParams.DISPLAY_PIA_ALERT] === payMethod
);
},
},
components: {
funnelHeader,
navbar,
loadingModal,
cart,
alert,
},
};
</script>
<style lang="scss">
.hop-iframe {
position: relative;
min-height: 650px;
width: 100%;
border: 0 none;
}
#pageLoadingModal {
background-color: red;
}
.ui-block {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
}
</style>