650 lines
23 KiB
Vue
650 lines
23 KiB
Vue
<template>
|
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
|
<div class="container">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div>
|
|
<alert
|
|
ref="piaErrorAlert"
|
|
class="my-4"
|
|
v-if="hasPaymentFailureError"
|
|
cmsWidgetName="PIAErrorAlertWidget"
|
|
alertClass="alert-danger"
|
|
@textLinkClicked="paymentFailedPayLater"
|
|
v-bind:isDismissible="false" />
|
|
</div>
|
|
<div id="card-container">
|
|
<div id="payment-container">
|
|
<funnelSubHeader class="py-5" cmsWidgetName="FunnelSubHeaderWidget" />
|
|
<div id="adyen-container"></div>
|
|
</div>
|
|
|
|
<div id="cart-container">
|
|
<cart
|
|
ref="cart"
|
|
:damage="damageInfo"
|
|
:availableVaps="availableVaps"
|
|
:allowItemRemoval="false"
|
|
v-model="lineItems"
|
|
servicePackageOptionsCmsName="ServicePackageTitle"
|
|
recyclingModalCmsWidgetName="RecycleModal"
|
|
msrModalCmsWidgetName="MSRModal"
|
|
:isInsurance="isInsurance"
|
|
:insuranceDeductible="currentDeductible"
|
|
:insuranceCompanyName="insuranceCompanyName"
|
|
:showInsuranceCoverageAs="showInsuranceCoverageAs"
|
|
:isMSRFeeApplicable="isMSRFeeApplicable"
|
|
:IsMSRFeeCoveredByInsurance="isMSRFeeCoveredByInsurance"
|
|
:isItac="isItac"
|
|
:isNoComp="isNoComp"
|
|
:isCollapsible="false" />
|
|
<textBlock
|
|
class="payment-disclaimer"
|
|
marginTopSizeOverride="0"
|
|
cmsWidgetName="PaymentDisclaimerWidget" />
|
|
</div>
|
|
</div>
|
|
|
|
<navbar
|
|
cmsWidgetName="FunnelFooterWidget"
|
|
ref="navbar"
|
|
isForwardActionDisabled="true"
|
|
isSubmitHidden="true"
|
|
@back-clicked="backButtonAction" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Form>
|
|
</template>
|
|
<script>
|
|
import Form from "vee-validate";
|
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
|
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
|
import alert from "@/ux-components/alert/alert";
|
|
import textBlock from "@/digital-components/text-block/text-block";
|
|
|
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
|
import globalMethods from "@/global-methods";
|
|
import { endpoints } from "../../constants/endpoints";
|
|
import { AppointmentTypeStrings } from "../../constants/schedule-constants";
|
|
import baseMixin from "@/mixins/base-mixin.js";
|
|
import { storeActions } from "@/constants/store-actions";
|
|
import { getAmountDue } from "@/helpers/pricing-helper.js";
|
|
import { submitWorkOrder } from "@/helpers/heritage-integration/order-helper.js";
|
|
import { createAdyenCheckout } from "@/helpers/adyen-helper";
|
|
import { Dropin } from "@adyen/adyen-web/auto";
|
|
import { applicationConfig } from "@/constants/application-config";
|
|
import { paymentMethods } from "@/constants/payment-method-constants";
|
|
import analyticsMixin from "@/mixins/analytics-mixin";
|
|
|
|
import "@adyen/adyen-web/styles/adyen.css";
|
|
import { mapAdyenToFmgPaymentMethod, mapFmgToAdyenPaymentMethod } from "../../helpers/adyen-helper";
|
|
import { showFmgLoadingModal } from "../../helpers/loading-modal-helper";
|
|
import cart from "@/fmg-components/cart/cart";
|
|
import { coverageStatus } from "@/constants/insurance";
|
|
import { deepClone } from "@/helpers/object-helper";
|
|
import store from "@/store";
|
|
import {
|
|
flushPagePrereqsLogs,
|
|
hasServiceLocationInfo,
|
|
hasInsuranceInfo,
|
|
hasSchedulingInfo,
|
|
hasCustomerInfo,
|
|
hasPaymentMethodInfo,
|
|
hasGlassPartsOrRepairInfo,
|
|
} from "@/helpers/page-prerequisites-helper.js";
|
|
|
|
export default {
|
|
name: "payment-adyen",
|
|
mixins: [],
|
|
data() {
|
|
return {
|
|
sessionId: String,
|
|
dropinComponent: null,
|
|
hasPaymentFailureError: false,
|
|
};
|
|
},
|
|
|
|
async beforeRouteEnter(to, from, next) {
|
|
// Call APIs
|
|
const cmsContentPromise = fetchCmsContentForPage(to.name);
|
|
|
|
// Settle API calls in parallel before handling results.
|
|
const promiseResultMap = [
|
|
{
|
|
resultKey: "cmsContent",
|
|
promise: cmsContentPromise,
|
|
},
|
|
];
|
|
|
|
const resultMap = await settleAllPromises(promiseResultMap);
|
|
|
|
const hasExistingPiaError = to.query?.piaFailure;
|
|
|
|
// Hydrate page with results
|
|
next(async (vm) => {
|
|
vm.setCmsContent(resultMap.cmsContent);
|
|
|
|
if (hasExistingPiaError) {
|
|
vm.hasPaymentFailureError = true;
|
|
}
|
|
});
|
|
},
|
|
|
|
methods: {
|
|
async initializeAdyenWithErrorHandling() {
|
|
try {
|
|
await this.initializeAdyen();
|
|
} catch (error) {
|
|
console.error("Failed to initialize Adyen payment:", error);
|
|
|
|
const errorJson = JSON.stringify(error, Object.getOwnPropertyNames(Object(error)));
|
|
analyticsMixin.methods.pushFmgSessionData(errorJson);
|
|
await global.$logger.logError(errorJson);
|
|
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.PIA_ERROR,
|
|
this.pageName
|
|
);
|
|
|
|
return;
|
|
}
|
|
},
|
|
|
|
async backButtonAction() {
|
|
// Stub, for navigating back via nav-bar.
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.CLICKED_BACK,
|
|
this.pageName
|
|
);
|
|
},
|
|
|
|
async forwardButtonAction() {
|
|
// Stub, for navigating forward via nav-bar
|
|
},
|
|
|
|
arePagePrerequisitesValid() {
|
|
const order = store.getters.order;
|
|
const logQueue = [];
|
|
const results = [
|
|
hasServiceLocationInfo(order, logQueue),
|
|
hasInsuranceInfo(order, logQueue),
|
|
hasSchedulingInfo(order, logQueue),
|
|
hasCustomerInfo(order, logQueue),
|
|
hasPaymentMethodInfo(order, logQueue),
|
|
hasGlassPartsOrRepairInfo(order, logQueue),
|
|
];
|
|
const result = results.every(Boolean);
|
|
flushPagePrereqsLogs("payment-adyen.vue", result, logQueue);
|
|
return result;
|
|
},
|
|
|
|
async initializeAdyen() {
|
|
console.log(`Price = ${this.amountDue}`);
|
|
console.log(`Adyen Price = ${this.adyenPriceTotal}`);
|
|
|
|
const requestBody = await this.getAdyenInitRequestInfo();
|
|
|
|
console.log(`Calling with:`);
|
|
console.log(requestBody);
|
|
|
|
const response = await globalMethods.callHttpClient({
|
|
method: endpoints.InitializeAdyenPayment.method,
|
|
endpoint: endpoints.InitializeAdyenPayment.url,
|
|
payload: requestBody,
|
|
logApiCall: true,
|
|
pageNameToLog: "payment-adyen",
|
|
});
|
|
|
|
const responseJson = response?.data;
|
|
|
|
console.log(`Got data:`);
|
|
console.log(responseJson);
|
|
|
|
const session = {
|
|
id: responseJson.sessionId,
|
|
sessionData: responseJson.sessionData,
|
|
};
|
|
|
|
this.sessionId = responseJson.sessionId;
|
|
|
|
const checkout = await createAdyenCheckout({
|
|
session: session,
|
|
handlers: {
|
|
onPaymentCompleted: (result, component) => {
|
|
console.log(`Payment completed from Adyen.`);
|
|
this.handleCompletedPayment(result);
|
|
},
|
|
onPaymentFailed: (result, component) => {
|
|
console.log(`Payment failed from Adyen`);
|
|
this.handleFailedPayment(result);
|
|
},
|
|
onError: (error, component) => {
|
|
console.log(`Error from Adyen`);
|
|
const errorJson = JSON.stringify(
|
|
error,
|
|
Object.getOwnPropertyNames(Object(error))
|
|
);
|
|
analyticsMixin.methods.pushFmgSessionData(errorJson);
|
|
|
|
this.handleError(error);
|
|
},
|
|
},
|
|
options: {
|
|
amount: this.adyenPriceTotal,
|
|
},
|
|
});
|
|
|
|
console.log(checkout);
|
|
|
|
const expiryTime = new Date(checkout.options.expiresAt);
|
|
const expiryInterval = expiryTime.getTime() - new Date().getTime();
|
|
|
|
const handleTimeout = () => {
|
|
this.resetAdyenDropin();
|
|
};
|
|
|
|
const timeout = setTimeout(handleTimeout, expiryInterval);
|
|
|
|
const configuration = {
|
|
paymentMethodsConfiguration: {
|
|
ideal: {
|
|
showImage: true,
|
|
},
|
|
paypal: {
|
|
amount: {
|
|
value: this.adyenPriceTotal,
|
|
currency: "USD",
|
|
},
|
|
environment: applicationConfig.ADYEN_ENVIRONMENT,
|
|
countryCode: "US", // Only needed for test. This will be automatically retrieved when you are in production.
|
|
blockPayPalVenmoButton: true,
|
|
blockPayPalPayLaterButton: true,
|
|
},
|
|
card: {
|
|
hasHolderName: true,
|
|
holderNameRequired: true,
|
|
billingAddressRequired: true,
|
|
name: "Credit or debit card",
|
|
},
|
|
},
|
|
};
|
|
|
|
const adyenPaymentType = mapFmgToAdyenPaymentMethod(this.piaType);
|
|
|
|
if (adyenPaymentType) {
|
|
configuration.openPaymentMethod = {
|
|
type: adyenPaymentType,
|
|
};
|
|
}
|
|
|
|
const dropin = new Dropin(checkout, configuration);
|
|
|
|
this.dropinComponent = dropin;
|
|
|
|
dropin.mount("#adyen-container");
|
|
await this.dispatchStoreAction(storeActions.CORRECT_IDEMPOTENCY_KEY_EXPIRY, expiryTime);
|
|
},
|
|
|
|
async handleCompletedPayment(result) {
|
|
console.log(`Result =`);
|
|
console.log(result);
|
|
// Fetch session info
|
|
const paymentSessionRequest = {
|
|
zipCodeCtu: this.locationInfo.zipCodeCtu,
|
|
workOrderNumber: this.workOrderNumberLastSixDigits,
|
|
sourceSystem: this.sourceSystem,
|
|
sessionId: this.sessionId,
|
|
sessionResult: result?.sessionResult,
|
|
};
|
|
|
|
console.log(`Get session payload =`);
|
|
console.log(paymentSessionRequest);
|
|
|
|
showFmgLoadingModal(true);
|
|
|
|
const response = await globalMethods.callHttpClient({
|
|
method: endpoints.GetAdyenSessionResult.method,
|
|
endpoint: endpoints.GetAdyenSessionResult.url,
|
|
payload: paymentSessionRequest,
|
|
logApiCall: true,
|
|
pageNameToLog: "payment-adyen",
|
|
});
|
|
|
|
const responseJson = response?.data;
|
|
|
|
console.log(`Session response =`);
|
|
console.log(responseJson);
|
|
|
|
const ccToken = {
|
|
subscriptionId: responseJson?.storedToken,
|
|
expMonth: responseJson?.cardExpiryMonth,
|
|
expYear: responseJson?.cardExpiryYear,
|
|
cardType: responseJson?.cardType,
|
|
billToPostalCode: this.locationInfo.zipCode, // TODO
|
|
billToFirstName: this.$store.getters.order.customer.firstName, // TODO
|
|
billToLastName: this.$store.getters.order.customer.lastName, // TODO
|
|
referenceNumber: responseJson?.workOrderNumber,
|
|
authCode: responseJson?.authCode,
|
|
transactionId: responseJson?.transactionReference, // TODO
|
|
transReferenceNumber: responseJson?.transactionReference,
|
|
lastFour: responseJson?.last4DigitsOfCard,
|
|
};
|
|
|
|
console.log(`CC Token generated =`);
|
|
console.log(ccToken);
|
|
|
|
const paymentMethodFromSession = responseJson?.paymentMethod;
|
|
|
|
const paymentMethod = mapAdyenToFmgPaymentMethod(paymentMethodFromSession);
|
|
|
|
// Save payment type.
|
|
// For now, CC.
|
|
// Need to determine payment type from Adyen response
|
|
await this.dispatchStoreAction(
|
|
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
|
|
paymentMethod,
|
|
false
|
|
);
|
|
|
|
if (paymentMethod === paymentMethods.PAYPAL) {
|
|
await baseMixin.methods.dispatchStoreAction(
|
|
storeActions.SAVE_PAYPAL_TOKEN,
|
|
responseJson?.storedToken,
|
|
false
|
|
);
|
|
} else {
|
|
await baseMixin.methods.dispatchStoreAction(
|
|
storeActions.SAVE_CCTOKEN,
|
|
ccToken,
|
|
false
|
|
);
|
|
}
|
|
|
|
await baseMixin.methods.dispatchStoreAction(
|
|
storeActions.SAVE_NEXTGEN_SETTLED_AMOUNT,
|
|
this.amountDue,
|
|
false
|
|
);
|
|
|
|
await this.saveAndSubmitWorkOrder();
|
|
},
|
|
async handleFailedPayment(result) {
|
|
console.log(`Result =`);
|
|
console.log(result);
|
|
|
|
const wasPaymentCancelled = result?.resultCode === "Cancelled";
|
|
|
|
if (!wasPaymentCancelled) {
|
|
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${this.sessionId}`;
|
|
|
|
await global.$logger.logError(stringToLog);
|
|
|
|
this.hasPaymentFailureError = true;
|
|
}
|
|
|
|
this.dropinComponent?.update();
|
|
},
|
|
async handleError(error) {
|
|
console.log(error);
|
|
|
|
const wasPaymentCancelled = error?.name === "CANCEL";
|
|
|
|
if (!wasPaymentCancelled) {
|
|
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
|
|
|
|
await global.$logger.logError(stringToLog);
|
|
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.PIA_ERROR,
|
|
this.pageName
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
this.dropinComponent?.update();
|
|
},
|
|
|
|
async saveAndSubmitWorkOrder() {
|
|
// Work order submission after successful payment.
|
|
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
|
|
try {
|
|
await submitWorkOrder({
|
|
pageNameToLog: this.pageName,
|
|
submitAfterSave: true,
|
|
});
|
|
} catch (error) {
|
|
console.log(`error: response from submit work order: ${error.message}`);
|
|
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.PIA_ERROR,
|
|
this.pageName
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// clear order
|
|
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE);
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.CLICKED_FORWARD,
|
|
this.pageName
|
|
);
|
|
},
|
|
|
|
async paymentFailedPayLater() {
|
|
console.log(`Doing final work order submission...`);
|
|
showFmgLoadingModal(true);
|
|
|
|
await this.dispatchStoreAction(
|
|
storeActions.SAVE_PAYMENT_METHOD_CHOICE,
|
|
paymentMethods.PAY_LATER,
|
|
false
|
|
);
|
|
|
|
await this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE);
|
|
|
|
try {
|
|
await submitWorkOrder({
|
|
pageNameToLog: this.pageName,
|
|
submitAfterSave: true,
|
|
});
|
|
|
|
await baseMixin.methods.dispatchStoreAction(storeActions.CREATE_SUBMITTED_STATE);
|
|
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.CLICKED_PAY_LATER,
|
|
this.pageName
|
|
);
|
|
} catch (error) {
|
|
console.log(`Final submission failed after failed payment.`);
|
|
this.$router.handleSoftError();
|
|
return;
|
|
}
|
|
},
|
|
|
|
async resetAdyenDropin() {
|
|
this?.dropinComponent?.unmount();
|
|
await this.initializeAdyen();
|
|
},
|
|
|
|
// Now a Method so it is always freshly called and not cached.
|
|
async getIdempotencyKey() {
|
|
return await this.dispatchStoreAction(storeActions.GET_VALID_IDEMPOTENCY_KEY);
|
|
},
|
|
|
|
// Now a Method so it is always freshly called and not cached.
|
|
async getAdyenInitRequestInfo() {
|
|
const key = await this.getIdempotencyKey();
|
|
return {
|
|
sourceSystem: this.sourceSystem,
|
|
referralSequenceNumber: this.$store.getters.order.referralSequenceNumber,
|
|
workOrderNumber: this.workOrderNumberLastSixDigits,
|
|
zipCodeCtu: this.locationInfo.zipCodeCtu,
|
|
amount: this.adyenPriceTotal,
|
|
city: this.locationInfo.city,
|
|
street: this.locationInfo.address ?? "",
|
|
houseNumberOrName: this.locationInfo.address2 ?? "",
|
|
zipCode: this.locationInfo.zipCode,
|
|
stateOrProvince: this.locationInfo.state,
|
|
returnUrl: applicationConfig.PIA_ADYEN_RETURN_URL,
|
|
email: this.$store.getters.order.customer.emailAddress,
|
|
firstName: this.$store.getters.order.customer.firstName,
|
|
lastName: this.$store.getters.order.customer.lastName,
|
|
idempotencyKey: key,
|
|
};
|
|
},
|
|
},
|
|
|
|
computed: {
|
|
piaType() {
|
|
return this.$store.getters.order.payment.piaType;
|
|
},
|
|
|
|
workOrderNumberLastSixDigits() {
|
|
const workOrderNumber = this.$store.getters.order.workOrderNumber ?? "";
|
|
const tokens = workOrderNumber.split("-");
|
|
const lastSegment = tokens[tokens.length - 1] ?? "";
|
|
const numDigits = lastSegment.length;
|
|
const numZeroes = 6 - numDigits;
|
|
|
|
let result = "";
|
|
for (let i = 0; i < numZeroes; i++) {
|
|
result += "0";
|
|
}
|
|
|
|
result += lastSegment;
|
|
|
|
return result;
|
|
},
|
|
|
|
locationInfo() {
|
|
const serviceLocation = this.$store.getters.order.serviceLocation;
|
|
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
|
if (isMobile) {
|
|
return {
|
|
address: serviceLocation.address,
|
|
address2: serviceLocation.address2,
|
|
city: serviceLocation.city,
|
|
state: serviceLocation.state,
|
|
zipCode: serviceLocation.zipCode,
|
|
zipCodeCtu: serviceLocation.zipCodeCtu,
|
|
};
|
|
} else {
|
|
const providerInfo = serviceLocation.provider.address;
|
|
return {
|
|
address: providerInfo.streetAddress,
|
|
address2: "",
|
|
city: providerInfo.city,
|
|
state: providerInfo.state,
|
|
zipCode: providerInfo.zipCode,
|
|
zipCodeCtu: providerInfo.zipCodeCtu,
|
|
};
|
|
}
|
|
},
|
|
|
|
sourceSystem() {
|
|
return "FMG-2.0";
|
|
},
|
|
|
|
amountDue() {
|
|
return getAmountDue(this.$store.getters.order.lineItems);
|
|
},
|
|
|
|
adyenPriceTotal() {
|
|
return Math.round(this.amountDue * 100);
|
|
},
|
|
|
|
// Cart info
|
|
damageInfo() {
|
|
return this.$store.getters.damage;
|
|
},
|
|
isInsurance() {
|
|
return this.$store.getters.payment.isInsurance;
|
|
},
|
|
isNoComp() {
|
|
return this.$store.getters.policy.isNoComp;
|
|
},
|
|
isItac() {
|
|
return this.$store.getters.policy.isItac;
|
|
},
|
|
showInsuranceCoverageAs() {
|
|
if (!this.isInsurance) {
|
|
return null;
|
|
}
|
|
|
|
if (this.isNoComp || this.isItac) {
|
|
return coverageStatus.NOCOMP;
|
|
} else {
|
|
return this.$store.getters.payment.insuranceCoverage.coverageStatus;
|
|
}
|
|
},
|
|
currentDeductible() {
|
|
return this.$store.getters.policy.currentDeductible;
|
|
},
|
|
insuranceCompanyName() {
|
|
return this.$store.getters.policy.insuranceCompanyName;
|
|
},
|
|
isMSRFeeApplicable() {
|
|
return this.$store.getters.order.isMSRFeeApplicable;
|
|
},
|
|
isMSRFeeCoveredByInsurance() {
|
|
return this.$store.getters.order.isMSRFeeCoveredByInsurance;
|
|
},
|
|
lineItems() {
|
|
return deepClone(this.$store.getters.lineItems);
|
|
},
|
|
availableVaps() {
|
|
return this.$store.getters.order.lineItems?.vaps ?? [];
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
this.initializeAdyenWithErrorHandling();
|
|
},
|
|
|
|
components: {
|
|
Form,
|
|
funnelHeader,
|
|
funnelSubHeader,
|
|
navbar,
|
|
cart,
|
|
alert,
|
|
textBlock,
|
|
},
|
|
};
|
|
</script>
|
|
<style lang="scss">
|
|
#card-container {
|
|
display: flex;
|
|
& > #payment-container {
|
|
flex-basis: 55%;
|
|
margin-right: 9%;
|
|
}
|
|
& > #cart-container {
|
|
flex-basis: 36%;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
@include media-breakpoint-down(lg) {
|
|
flex-direction: column;
|
|
& > #payment-container {
|
|
flex-basis: auto;
|
|
margin-right: 0;
|
|
}
|
|
& > #cart-container {
|
|
flex-basis: auto;
|
|
margin: 0 0.5rem;
|
|
}
|
|
}
|
|
|
|
.payment-disclaimer p {
|
|
font-size: 0.875rem;
|
|
line-height: 1.2;
|
|
}
|
|
}
|
|
</style>
|