Merge pull request #1142 from Safelite/feature/humphries/INSR-7995

INSR-7995: Adyen integration
This commit is contained in:
AHumphriesSL 2026-03-19 10:47:26 -04:00 committed by GitHub
commit 2c488803bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1099 additions and 5 deletions

41
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "digitalconsumer.iss",
"version": "0.1.0",
"dependencies": {
"@adyen/adyen-web": "^6.31.1",
"axios": "^1.13.5",
"axios-retry": "^3.5.0",
"bootstrap": "^5.3",
@ -93,6 +94,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/@adyen/adyen-web": {
"version": "6.31.1",
"resolved": "https://registry.npmjs.org/@adyen/adyen-web/-/adyen-web-6.31.1.tgz",
"integrity": "sha512-vxeJW1bTvjXxYf6CkPQV8gbYIhm/hupcjjeoKkkBdxk7geW3qy18Jlco4/MuSeb88ctTRM3gebwFXpqF2NjBLQ==",
"license": "MIT",
"dependencies": {
"@types/applepayjs": "14.0.9",
"@types/googlepay": "0.7.10",
"classnames": "2.5.1",
"preact": "10.28.2"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"dev": true,
@ -4125,6 +4138,12 @@
"node": ">= 10"
}
},
"node_modules/@types/applepayjs": {
"version": "14.0.9",
"resolved": "https://registry.npmjs.org/@types/applepayjs/-/applepayjs-14.0.9.tgz",
"integrity": "sha512-xEprYbb0TEP/XIiDPbVnTYpDai8fTFpsQfVSfTd81Is2GOMUy7ie019eyX6Mz2ECxfjoUVKaiGSL577roIeHCg==",
"license": "MIT"
},
"node_modules/@types/aria-query": {
"version": "5.0.1",
"dev": true,
@ -4283,6 +4302,12 @@
"@types/node": "*"
}
},
"node_modules/@types/googlepay": {
"version": "0.7.10",
"resolved": "https://registry.npmjs.org/@types/googlepay/-/googlepay-0.7.10.tgz",
"integrity": "sha512-ByXjDfxEp87zsjw5cVsNKEeA+AIJYcWwJvRqPu85jO0Lp/FeG67csnf4mqbPaIxmdI6+Khp3v5I0yyjq9MrGhw==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.5",
"dev": true,
@ -7371,6 +7396,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/clean-css": {
"version": "5.3.1",
"dev": true,
@ -17407,6 +17438,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/preact": {
"version": "10.28.2",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"dev": true,

View file

@ -20,6 +20,7 @@
"lint:inspect": "eslint --inspect-config"
},
"dependencies": {
"@adyen/adyen-web": "^6.31.1",
"axios": "^1.13.5",
"axios-retry": "^3.5.0",
"bootstrap": "^5.3",

View file

@ -26,7 +26,9 @@ const applicationConfig = Object.freeze({
FRONTEND_LOGGER_PATH: "/analytics/api/v1/logging",
BAILOUT_ON_APPLICATION_ERROR: true,
BAILOUT_ON_API_ERROR: true,
BAILOUT_ON_ROUTER_ERROR: true
BAILOUT_ON_ROUTER_ERROR: true,
ADYEN_CLIENT_KEY: process.env.VUE_APP_ADYEN_CLIENT_KEY,
ADYEN_ENVIRONMENT: process.env.VUE_APP_ADYEN_ENVIRONMENT
});
export default applicationConfig;

View file

@ -10,6 +10,7 @@ const PARTS_BASE_URL = '/parts/api/v1/parts';
const PRICE_BASE_URL = '/price/api/v1/price';
const SCHEDULE_BASE_URL = '/schedule/api/v1/schedule';
const VEHICLE_BASE_URL = '/vehicle/api/v1/vehicle';
const PAYMENT_BASE_URL = '/payment/api/v1/payment';
const endpoints = Object.freeze({
GetRouteInfo: {
@ -239,6 +240,14 @@ const endpoints = Object.freeze({
GetPaymentSignature: {
url: `${ORDER_BASE_URL}/sign`,
method: 'POST'
},
InitializeAdyenPayment: {
url: `${PAYMENT_BASE_URL}/payment/session/initialize-session`,
method: "POST",
},
GetAdyenSessionResult: {
url: `${PAYMENT_BASE_URL}/payment/session/session-result`,
method: "POST",
}
});

View file

@ -1,7 +1,8 @@
const experimentUniverses = Object.freeze({
ISS_FUNNEL: 'ISSFunnel',
ISS_FEATURETOGGLE_AREFEES_HIDDEN: 'NextGenISS_FeatureToggle_AreFeesHidden',
ISS_FEATURETOGGLE_AREFEES_OVERRIDDEN: 'NextGenISS_FeatureToggle_AreFeesOverridden'
ISS_FEATURETOGGLE_AREFEES_OVERRIDDEN: 'NextGenISS_FeatureToggle_AreFeesOverridden',
ADYEN_PAYMENT_TEST: 'NextGenAdyenPaymentTest'
});
const experimentSettings = Object.freeze({
@ -10,7 +11,8 @@ const experimentSettings = Object.freeze({
ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_HIDDEN: 'HideMobileFee',
ISS_FEATURE_TOGGLE_IS_MOBILE_FEE_OVERRIDDEN: 'OverrideMobileFee',
ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_HIDDEN: 'HideRecycleFee',
ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_OVERRIDDEN: 'OverrideRecycleFee'
ISS_FEATURE_TOGGLE_IS_RECYCLE_FEE_OVERRIDDEN: 'OverrideRecycleFee',
ISS_ENABLE_ADYEN_V1: 'ISS_Enable_Adyen_V1'
});
const experimentTriggers = Object.freeze({

View file

@ -66,6 +66,14 @@ export const pageProgressMapper = {
// No progress bar on this page
percent: 0
},
'payment-page-adyen': {
// No progress bar on this page
percent: 0
},
'payment-return-adyen': {
// No progress bar on this page
percent: 0
},
'tpa-search': {
percent: 80
},

164
src/helpers/adyen-helper.js Normal file
View file

@ -0,0 +1,164 @@
import { AdyenCheckout } from "@adyen/adyen-web/auto";
import { useMainStore } from "@/store";
import globalMethods from "@/global-methods";
import endpoints from "@/constants/endpoints";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { paymentMethods } from "@/constants/payment-method-constants";
import applicationConfig from "@/constants/application-config";
export async function getSessionInfo(sessionId, encryptedSessionResult) {
const order = useMainStore().order;
const location = getLocationInfo(order);
const workOrder = getWorkOrderLastSixDigits(order);
const system = getSourceSystem();
const request = {
sessionId: sessionId,
sessionResult: encryptedSessionResult,
zipCodeCtu: location.zipCodeCtu,
workOrderNumber: workOrder,
sourceSystem: system,
};
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
payload: request,
logApiCall: true
});
return response?.data;
}
export async function createAdyenCheckout({
session: { id, sessionData },
handlers: { onPaymentCompleted, onPaymentFailed, onError },
options: { showPayButton = true, amount },
}) {
const config = {
session: {
id: id,
sessionData: sessionData,
},
clientKey: applicationConfig.ADYEN_CLIENT_KEY,
environment: applicationConfig.ADYEN_ENVIRONMENT,
locale: "en_US",
countryCode: "US",
showPayButton: showPayButton,
translations: {
en_US: {
"creditCard.securityCode.label": "CVV/CVC",
},
},
onPaymentCompleted: onPaymentCompleted,
onPaymentFailed: onPaymentFailed,
onError: onError,
};
if (amount !== undefined && amount !== null) {
config.amount = {
value: amount,
currency: "USD",
};
}
return await AdyenCheckout(config);
}
export function mapAdyenToIssPaymentMethod(adyenMethod) {
const paymentMethodMap = {
["scheme"]: paymentMethods.CREDIT_CARD,
["afterpaytouch_US"]: paymentMethods.AFTERPAY,
["paypal"]: paymentMethods.PAYPAL,
["applepay"]: paymentMethods.APPLEPAY,
};
const match = paymentMethodMap[adyenMethod];
return match ?? paymentMethods.CREDIT_CARD;
}
export function mapIssToAdyenPaymentMethod(issMethod) {
const paymentMethodMap = {
[paymentMethods.CREDIT_CARD]: "scheme",
[paymentMethods.AFTERPAY]: "afterpaytouch_US",
[paymentMethods.PAYPAL]: "paypal",
[paymentMethods.APPLEPAY]: "applepay",
[paymentMethods.PayNow]: null,
[paymentMethods.PAY_AT_TIME_OF_SERVICE]: null,
};
const match = paymentMethodMap[issMethod];
return match;
}
export function generateCcToken(adyenSessionInfo) {
const zipCode = useMainStore().order.customer.address.zipCode
?? useMainStore().order.serviceLocation.zipCode
?? useMainStore().order.serviceLocation.provider.address.zipCode
const ccToken = {
subscriptionId: adyenSessionInfo?.storedToken,
expMonth: adyenSessionInfo?.cardExpiryMonth,
expYear: adyenSessionInfo?.cardExpiryYear,
cardType: adyenSessionInfo?.cardType,
billToPostalCode: zipCode,
billToFirstName: useMainStore().contactInfo.firstName,
billToLastName: useMainStore().contactInfo.lastName,
referenceNumber: adyenSessionInfo?.workOrderNumber,
authCode: adyenSessionInfo?.authCode,
transactionId: adyenSessionInfo?.transactionReference,
transReferenceNumber: adyenSessionInfo?.transactionReference,
lastFour: adyenSessionInfo?.last4DigitsOfCard,
};
return ccToken;
}
function getLocationInfo(order) {
const serviceLocation = order.serviceLocation;
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE ||
serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
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,
};
}
}
function getWorkOrderLastSixDigits(order) {
const workOrderNumber = 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;
}
function getSourceSystem() {
return "ISS-NextGen";
}

View file

@ -239,8 +239,23 @@ export default {
return !isEnabled || useMainStore().isUnverified || (useMainStore().isDeductible && isDeductibleTotalEqualToZero);
},
isAdyenEnabled() {
const adyenEnabled = this.getSettingValue(experimentSettings.ISS_ENABLE_ADYEN_V1);
return adyenEnabled === 'true';
},
paymentMethodWidgetName() {
return supportsApplePay() ? this.widget.paymentMethodApplePay : this.widget.paymentMethod;
let showApplePay = false;
if (this.isAdyenEnabled) {
// Apple Pay is Available in Adyen if the protocol is HTTPS
if (window.location.protocol === 'https:') {
showApplePay = true;
}
}
else {
showApplePay = supportsApplePay();
}
return showApplePay ? this.widget.paymentMethodApplePay : this.widget.paymentMethod;
},
showCartTotal() {
return this.mainStore.isVerified && (this.mainStore.isITAC || this.mainStore.isNoComp
@ -418,8 +433,12 @@ export default {
bailoutOnError: true
});
const scenario = this.isAdyenEnabled
? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN
: this.navigationScenarios.CLICKED_PAY_NOW;
this.$router.navigate(
this.navigationScenarios.CLICKED_PAY_NOW,
scenario,
this.$route,
{},
{ [routerParams.SKIP_SAVE_SESSION]: true }

View file

@ -0,0 +1,623 @@
<template>
<Form>
<div class="fade-on-route-transition">
<div class="justify-content-center">
<siteHeader cmsWidgetName="SiteHeaderWidget" />
</div>
<div class="iss-heritage-container-width payment-page-container">
<div id="card-container" class="row">
<div class="col-lg-8">
<alert
v-if="hasPaymentFailureError"
name="payInAdvanceErrorAlert"
class="pay-in-advance-alert"
cmsWidgetName="PayInAdvanceErrorAlertWidget"
alertClass="alert-danger"
:isDismissible="false"
:isCollapsible="true"
:preventDefaultOnClick="true"
@textLinkClicked="paymentFailedPayLater" />
<div id="adyen-container"></div>
</div>
<div class="col-lg-4 col-md-10 col-xs-12">
<div class="order-summary-panel">
<div class="cart-title">{{ cartTitle }}</div>
<cartDropdown
ref="cart"
class="cart-dropdown-component"
:readOnly="true"
:showAsPaid="false"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle" />
</div>
</div>
</div>
<div class="col-xs-12">
<siteFooter
class="footer-component"
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="true"
:isForwardButtonHidden="true"
@ForwardClicked="forwardButtonAction"
@backClicked="backButtonAction" />
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import alert from '@/ux-components/alert/alert.vue';
import cartDropdown from '@/iss-components/cart-dropdown/cart-dropdown.vue';
// Supporting files
import issPageValues from '@/router/router-constants/issPage-values.js';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import settleAllPromises from '@/helpers/layout-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { getCartTotal } from '@/helpers/cart-helper';
import { useMainStore } from '@/store';
import widgetFields from '@/constants/cms-widget-fields.js';
import { paymentMethods } from '@/constants/payment-method-constants.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants.js';
import queryStrings from '@/constants/query-strings';
import { formatAmountInDollars } from '@/helpers/text-helper';
import showIssLoadingModal from '@/helpers/loading-modal-helper';
import { submitWorkOrder } from '@/helpers/order-helper';
import submitType from '@/constants/submit-type';
import "@adyen/adyen-web/styles/adyen.css";
import { mapAdyenToIssPaymentMethod, mapIssToAdyenPaymentMethod } from '@/helpers/adyen-helper';
import { createAdyenCheckout } from "@/helpers/adyen-helper";
import { Dropin } from "@adyen/adyen-web/auto";
import applicationConfig from '@/constants/application-config';
export default {
name: 'payment-page-adyen',
components: {
cartDropdown,
siteHeader,
siteFooter,
Form,
alert
},
mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) {
const alertToDisplay = to.query[queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT];
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise
}
];
// use resultMap to populate layout content.
const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$nextTick(() => {
if (vm.$refs.cart) {
const cartItems = vm.$refs.cart.cartItems;
vm.getPayInAdvanceLineItems(cartItems);
}
if (alertToDisplay) {
vm.hasPaymentFailureError = true;
}
});
});
},
mounted() {
showIssLoadingModal(true);
this.initializeAdyen().finally(() => {
showIssLoadingModal(false);
});
},
data() {
return {
referralSequenceNumber: useMainStore().order.referralSequenceNumber,
emailAddress: useMainStore().contactInfo.emailAddress,
address1: this.getAddress1(),
address2: this.getAddress2(),
city: this.getCity(),
state: this.getState(),
zipCode: this.getZipCode(),
firstName: useMainStore().contactInfo.firstName,
lastName: useMainStore().contactInfo.lastName,
phoneNumber: useMainStore().contactInfo.servicePhone,
ctu: useMainStore().order.serviceLocation.provider?.address?.zipCodeCtu ?? useMainStore().order.serviceLocation.zipCodeCtu,
referralCorrelationId: useMainStore().order.referralCorrelationId,
workOrderNumber: this.getWorkOrderNumber(),
invoiceNumber: this.getInvoiceNumber(),
totalAmount: this.getAmountDue(),
displayAmount: this.getDisplayAmountDue(),
payInAdvanceLineItems: '',
sessionId: '',
dropinComponent: null,
sourceSystem: 'ISS-NextGen',
hasPaymentFailureError: false,
adyenTimeout: null
};
},
unmounted() {
if (this.adyenTimeout) {
clearTimeout(this.adyenTimeout);
}
},
computed: {
piaType() {
return this.mainStore.payment.paymentMethod;
},
parentDomainName() {
return window.location.hostname;
},
payInAdvanceResponseUrl() {
const { protocol, host } = window.location;
return `${protocol}//${host}/?issPage=${issPageValues.PAYMENT_RETURN_ADYEN}&src=iss-nextgen`;
},
payInAdvanceCancelUrl() {
const { protocol, host } = window.location;
return `${protocol}//${host}/?issPage=${issPageValues.PAYMENT_METHOD}&src=iss-nextgen`;
},
cartTitle() {
return this.getCmsContent('OrderSummaryTextWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
switchPaymentText() {
return this.getCmsContent('SwitchPaymentTextWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
submitPaymentText() {
return this.getCmsContent('SiteFooterWidget', widgetFields.FOOTER_WIDGET.FORWARD_BUTTON_TEXT);
},
adyenPriceTotal() {
return Math.round(this.totalAmount * 100);
},
workOrderNumberLastSixDigits() {
const workOrderNumber = this.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;
},
splitStreetAddress() {
const address = this.address1;
const tokens = address.split(" ") ?? [""];
const number = tokens[0];
const street = tokens.slice(1).join(" ") ?? "";
return {
number: number ?? "",
street: street ?? "",
};
}
},
methods: {
arePagePrerequisitesValid() {
// Line Items
const packageReqs =
!!useMainStore().order.lineItems.supportingItems;
// Service Location
const { serviceLocation } = useMainStore().order;
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
|| serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP;
const serviceLocationReqs =
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
// Insurance
const isInsuranceSet =
useMainStore().order.payment.isInsurance !== null;
// Schedule
const { schedule } = useMainStore().order;
const scheduleReqs = !!(
schedule.date
&& schedule.startTime
&& schedule.endTime
&& schedule.jobMaxMinutes
&& schedule.jobMinMinutes
);
// Contact Info
const { contactInfo } = useMainStore();
const contactInfoReqs = !!(
contactInfo.firstName
&& contactInfo.lastName
&& contactInfo.servicePhone
&& contactInfo.emailAddress
);
const paymentMethodReqs = useMainStore().isPayInAdvance;
return (
packageReqs
&& serviceLocationReqs
&& isInsuranceSet
&& scheduleReqs
&& contactInfoReqs
&& paymentMethodReqs
);
},
getWorkOrderNumber() {
const { workOrderNumber } = useMainStore().order;
if (workOrderNumber) {
const items = workOrderNumber.split('-');
if (items.length > 1) {
return items[1];
}
}
return undefined;
},
getInvoiceNumber() {
const { workOrderNumber } = useMainStore().order;
if (workOrderNumber) {
return workOrderNumber.replace('-', '');
}
return undefined;
},
getAddress1() {
return (
useMainStore().order.serviceLocation.address
?? useMainStore().order.serviceLocation.provider.address
.streetAddress
);
},
getAddress2() {
return useMainStore().order.serviceLocation.address2;
},
getCity() {
return (
useMainStore().order.serviceLocation.city
?? useMainStore().order.serviceLocation.provider.address.city
);
},
getState() {
return (
useMainStore().order.serviceLocation.state
?? useMainStore().order.serviceLocation.provider.address.state
);
},
getZipCode() {
return (
useMainStore().order.customer.address.zipCode
?? useMainStore().order.serviceLocation.zipCode
?? useMainStore().order.serviceLocation.provider.address.zipCode
);
},
getPayInAdvanceLineItems(cartItems) {
const { glassParts } = useMainStore().order.lineItems;
const lineItems =
glassParts === null
? [
this.getPayInAdvanceFormattedLineItem('Labor', 0, 1),
this.getPayInAdvanceFormattedLineItem('Repair supplies', 0, 1)
]
: [
this.getPayInAdvanceFormattedLineItem('Parts and labor', 0, 1)
];
cartItems.forEach((item) => {
if (item.name !== null && item.category !== 'promos') {
lineItems.push(this.getPayInAdvanceFormattedLineItem(
item.name,
(item.salesTax + item.subTotal).toFixed(2),
1
));
}
});
this.payInAdvanceLineItems = lineItems.join('||');
},
getPayInAdvanceFormattedLineItem(itemName, price, quantity) {
return `${itemName}|${price}|${quantity}`;
},
getAmountDue() {
return getCartTotal(useMainStore().order);
},
getDisplayAmountDue() {
return formatAmountInDollars(this.getAmountDue());
},
async paymentFailedPayLater() {
this.showIssLoadingModal(true);
this.mainStore.savePaymentMethodChoice(paymentMethods.PAY_AT_TIME_OF_SERVICE);
await submitWorkOrder({ submitType: submitType.SAFELITE });
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
backButtonAction() {
// page will not render with normal navigation due to navigating from the iframe
window.location = this.payInAdvanceCancelUrl;
},
forwardButtonAction() {
this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD,
this.$route
);
},
async initializeAdyen() {
const requestBody = this.getAdyenInitRequestInfo();
const adyenResponse = await this.mainStore.initializeAdyenPayment(requestBody);
const session = {
id: adyenResponse.sessionId,
sessionData: adyenResponse.sessionData,
};
this.sessionId = adyenResponse.sessionId;
const checkout = await createAdyenCheckout({
session: session,
handlers: {
onPaymentCompleted: (result, component) => {
this.handleCompletedPayment(result);
},
onPaymentFailed: (result, component) => {
this.handleFailedPayment(result);
},
onError: (error, component) => {
this.handleError(error);
},
},
options: {
amount: this.adyenPriceTotal,
},
});
const expiryTime = new Date(checkout.options.expiresAt);
const expiryInterval = expiryTime.getTime() - new Date().getTime();
const handleTimeout = () => {
this.resetAdyenDropin();
};
if (this.adyenTimeout) {
clearTimeout(this.adyenTimeout);
}
this.adyenTimeout = 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",
data: {
holderName: `${this.mainStore.contactInfo.firstName} ${this.mainStore.contactInfo.lastName}`,
billingAddress: {
postalCode: this.zipCode,
country: "US"
}
}
},
},
};
const adyenPaymentType = mapIssToAdyenPaymentMethod(this.piaType);
if (adyenPaymentType) {
configuration.openPaymentMethod = {
type: adyenPaymentType,
};
}
const dropin = new Dropin(checkout, configuration);
this.dropinComponent = dropin;
dropin.mount("#adyen-container");
},
getAdyenInitRequestInfo() {
const request = {
sourceSystem: this.sourceSystem,
referralSequenceNumber: this.referralSequenceNumber,
workOrderNumber: this.workOrderNumberLastSixDigits,
zipCodeCtu: this.ctu,
amount: this.adyenPriceTotal,
city: this.city,
houseNumberOrName: this.splitStreetAddress.number,
street: this.splitStreetAddress.street,
zipCode: this.zipCode,
stateOrProvince: this.state,
returnUrl: this.payInAdvanceResponseUrl,
email: this.mainStore.contactInfo.emailAddress,
firstName: this.mainStore.contactInfo.firstName,
lastName: this.mainStore.contactInfo.lastName,
idempotencyKey: this.getIdempotencyKey(),
};
return request;
},
async resetAdyenDropin() {
this?.dropinComponent?.unmount();
await this.initializeAdyen();
},
getIdempotencyKey() {
const currentDateTime = new Date();
const currentHour = currentDateTime.getUTCHours();
const currentDate = currentDateTime.getUTCDate();
const id = this?.mainStore?.order?.referralCorrelationId;
const system = this.sourceSystem;
const total = this.adyenPriceTotal;
return `${id}-${system}-${currentDate}-${currentHour}-${total}`;
},
async handleCompletedPayment(result) {
// Fetch session info
const paymentSessionRequest = {
zipCodeCtu: this.ctu,
workOrderNumber: this.workOrderNumberLastSixDigits,
sourceSystem: this.sourceSystem,
sessionId: this.sessionId,
sessionResult: result?.sessionResult,
};
showIssLoadingModal(true);
const adyenResponse = await this.mainStore.getAdyenSessionResult(paymentSessionRequest);
const ccToken = {
subscriptionId: adyenResponse?.storedToken,
expMonth: adyenResponse?.cardExpiryMonth,
expYear: adyenResponse?.cardExpiryYear,
cardType: adyenResponse?.cardType,
billToPostalCode: this.zipCode,
billToFirstName: this.mainStore.contactInfo.firstName,
billToLastName: this.mainStore.contactInfo.lastName,
referenceNumber: adyenResponse?.workOrderNumber,
authCode: adyenResponse?.authCode,
transactionId: adyenResponse?.transactionReference,
transReferenceNumber: adyenResponse?.transactionReference,
lastFour: adyenResponse?.last4DigitsOfCard,
};
const paymentMethodFromSession = adyenResponse?.paymentMethod;
const paymentMethod = mapAdyenToIssPaymentMethod(paymentMethodFromSession);
this.mainStore.savePaymentMethodChoice(paymentMethod);
if (paymentMethod === paymentMethods.PAYPAL) {
this.mainStore.updatePaypalToken(adyenResponse?.storedToken);
} else {
this.mainStore.updateCreditCardToken(ccToken);
}
this.mainStore.updateNextGenSettledAmount(this.totalAmount);
await this.saveAndSubmitWorkOrder();
},
async handleFailedPayment(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) {
const wasPaymentCancelled = error?.name === "CANCEL";
if (!wasPaymentCancelled) {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
await global.$logger.logError(stringToLog);
this.hasPaymentFailureError = true;
}
this.dropinComponent?.update();
},
async saveAndSubmitWorkOrder() {
// Final work order submit after returning from pay in advance.
await submitWorkOrder({ submitType: submitType.SAFELITE });
this.$router.navigate(
this.navigationScenarios.PAY_IN_ADVANCE_SUCCESS,
this.$route
);
},
showIssLoadingModal
}
};
</script>
<style lang="scss" scoped>
.iss-heritage-container-width.payment-page-container {
padding: 0 .9375rem;
#card-container {
padding-top: 1.375rem;
margin: 0;
> * {
padding: 0 .9375rem;
}
}
.order-summary-panel {
margin-bottom: 1.375rem;
}
.cart-title {
font-size: 1rem;
font-weight: 500;
margin-top: 1.25rem;
margin-bottom: .625rem;
}
.cart-dropdown-component.cart-table {
font-size: .8125rem;
}
.footer-component:deep(.footer) {
margin: 1.25rem 0 0 0;
}
.pay-in-advance-alert {
:deep(a) {
font-weight: $font-weight-bold;
}
}
}
.fade-on-route-transition {
height: unset;
}
.payment p {
margin-bottom: 0.75rem;
}
</style>

View file

@ -0,0 +1,160 @@
<template>
<Form
ref="theForm"
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit">
</Form>
</template>
<script>
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store/index.js';
import queryStrings from '@/constants/query-strings';
import { getCartTotal } from '@/helpers/cart-helper';
import { submitWorkOrder } from '@/helpers/order-helper.js';
import { createAdyenCheckout, generateCcToken, getSessionInfo, mapAdyenToIssPaymentMethod } from '@/helpers/adyen-helper';
import navigationScenarios from '@/router/router-constants/navigation-scenarios';
import submitType from '@/constants/submit-type';
import showIssLoadingModal from '@/helpers/loading-modal-helper';
import { paymentMethods } from '@/constants/payment-method-constants';
export default {
name: 'payment-return-adyen',
components: {
Form
},
mixins: [BaseFormMixin],
async mounted() {
showIssLoadingModal(true);
const store = useMainStore();
const sessionId = this.$route?.query?.sessionId;
const redirectResult = this.$route?.query?.redirectResult;
if (sessionId && redirectResult) {
let result = null;
try {
result = await this.finalizeAdyenPayment(sessionId, redirectResult);
} catch (unexpectedResult) {
// If user cancelled payment, handle without error message
if (unexpectedResult.code === "Cancelled" || unexpectedResult.code === "CANCEL") {
this.$router.navigate(navigationScenarios.PAY_IN_ADVANCE_CANCEL, this.$route);
return;
}
// Otherwise, failure scenario.
// Payment fails, so return user to payment-page-adyen screen to try again or pay later.
this.$router.navigate(
navigationScenarios.PAY_IN_ADVANCE_ERROR,
this.$route,
{
[queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT]: true
}
);
return;
}
const sessionInfo = await getSessionInfo(sessionId, result.sessionResult);
const paymentMethod = mapAdyenToIssPaymentMethod(sessionInfo?.paymentMethod);
const amountDue = getCartTotal(store.order);
const ccToken = generateCcToken(sessionInfo);
if (paymentMethod === paymentMethods.AFTERPAY) {
ccToken.authCode = "831001";
ccToken.cardType = "VS";
ccToken.lastFour = "1111";
ccToken.expMonth = "03";
ccToken.expYear = "2030";
}
store.savePaymentMethodChoice(paymentMethod);
store.updateCreditCardToken(ccToken);
store.updateNextGenSettledAmount(amountDue);
store.clearSaveSessionPromise();
try {
await this.saveAndSubmitWorkOrder();
return;
} catch (error) {
this.$router.navigate(
navigationScenarios.PAY_IN_ADVANCE_ERROR,
this.$route,
{
[queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT]: true
}
);
}
}
this.$router.navigate(
navigationScenarios.PAY_IN_ADVANCE_ERROR,
this.$route,
{
[queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT]: true
}
);
},
methods: {
finalizeAdyenPayment(sessionId, redirectResult) {
return new Promise((resolve, reject) => {
createAdyenCheckout({
session: {
id: sessionId,
},
handlers: {
onPaymentCompleted: (result, component) => {
resolve(result);
},
onPaymentFailed: (result, component) => {
if (result?.resultCode !== "Cancelled") {
const stringToLog = `ADYEN PAYMENT NOT AUTHORIZED. Code = ${result?.resultCode}. Id = ${sessionId}`;
global.$logger.logError(stringToLog);
}
reject({
code: result?.resultCode,
message: "Payment failed from Adyen",
});
},
onError: (error, component) => {
if (error?.name !== "CANCEL") {
const stringToLog = `ADYEN ERROR. Name = ${error?.name}. Details = ${error?.message}.`;
global.$logger.logError(stringToLog);
}
reject({
code: error?.name,
message: "Error from Adyen",
});
},
},
options: {},
}).then(
(checkout) => {
checkout.submitDetails({
details: {
redirectResult: redirectResult,
},
});
},
(error) => {
reject(error);
}
);
});
},
async saveAndSubmitWorkOrder() {
// Final work order submit after returning from pay in advance.
await submitWorkOrder({ submitType: submitType.SAFELITE });
this.$router.navigate(
this.navigationScenarios.PAY_IN_ADVANCE_SUCCESS,
this.$route
);
}
}
};
</script>

View file

@ -19,6 +19,8 @@ const issPageValues = Object.freeze({
PAYMENT_METHOD: 'payment-method',
PAYMENT_PAGE: 'payment-page',
PAYMENT_RETURN: 'payment-return',
PAYMENT_PAGE_ADYEN: 'payment-page-adyen',
PAYMENT_RETURN_ADYEN: 'payment-return-adyen',
PART_QUESTIONS: 'part-questions',
POLICY_HOLDER_DETAILS: 'policy-holder-details',
PROVIDER_PREFERENCE: 'provider-preference',

View file

@ -102,6 +102,8 @@ const navigationScenarios = Object.freeze({
CLICKED_BACK_INSHOP: 'CLICKED_BACK_INSHOP',
CLICKED_BACK_MOBILE: 'CLICKED_BACK_MOBILE',
CLICKED_PAY_NOW: 'CLICKED_PAY_NOW',
CLICKED_PAY_NOW_ADYEN: 'CLICKED_PAY_NOW_ADYEN',
PAY_IN_ADVANCE_CANCEL: 'PAY_IN_ADVANCE_CANCEL',
PAY_IN_ADVANCE_ERROR: 'PAY_IN_ADVANCE_ERROR',
PAY_IN_ADVANCE_CREDIT_CARD_ERROR: 'PAY_IN_ADVANCE_CREDIT_CARD_ERROR',
PAY_IN_ADVANCE_SUCCESS: 'PAY_IN_ADVANCE_SUCCESS',

View file

@ -641,6 +641,10 @@ const routingTable = () => [
scenario: navigationScenarios.CLICKED_PAY_NOW,
destinationIssPageValue: issPageValues.PAYMENT_PAGE
},
{
scenario: navigationScenarios.CLICKED_PAY_NOW_ADYEN,
destinationIssPageValue: issPageValues.PAYMENT_PAGE_ADYEN
},
{
scenario: navigationScenarios.EDIT_SERVICE_LOCATION_INSHOP,
destinationIssPageValue: issPageValues.SCHEDULE_PAGE
@ -676,6 +680,23 @@ const routingTable = () => [
}
]
},
{
issPageValue: issPageValues.PAYMENT_PAGE_ADYEN,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.PAYMENT_METHOD
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.ORDER_CONFIRMATION
},
{
scenario: navigationScenarios.PAY_IN_ADVANCE_SUCCESS,
destinationIssPageValue: issPageValues.ORDER_CONFIRMATION
}
]
},
{
issPageValue: issPageValues.PAYMENT_RETURN,
maps: [
@ -693,6 +714,23 @@ const routingTable = () => [
}
]
},
{
issPageValue: issPageValues.PAYMENT_RETURN_ADYEN,
maps: [
{
scenario: navigationScenarios.PAY_IN_ADVANCE_CANCEL,
destinationIssPageValue: issPageValues.PAYMENT_PAGE_ADYEN
},
{
scenario: navigationScenarios.PAY_IN_ADVANCE_ERROR,
destinationIssPageValue: issPageValues.PAYMENT_PAGE_ADYEN
},
{
scenario: navigationScenarios.PAY_IN_ADVANCE_SUCCESS,
destinationIssPageValue: issPageValues.ORDER_CONFIRMATION
}
]
},
{
issPageValue: issPageValues.TPA_CONFIRMATION,
maps: [

View file

@ -1778,6 +1778,9 @@ export const useMainStore = defineStore({
updatePaypalToken(token) {
this.order.payment.paypalToken = token;
},
updateNextGenSettledAmount(amount) {
this.order.payment.nextGenSettledAmount = amount;
},
setSaveSessionPromise(promise) {
this.applicationUser.saveSessionPromise = promise;
},
@ -2896,6 +2899,24 @@ export const useMainStore = defineStore({
endpoint: endpoints.GetPaymentSignature.url
});
},
async initializeAdyenPayment(requestBody) {
const response = await globalMethods.callHttpClient({
method: endpoints.InitializeAdyenPayment.method,
endpoint: endpoints.InitializeAdyenPayment.url,
payload: requestBody,
logApiCall: true
});
return response.data;
},
async getAdyenSessionResult(paymentSessionRequest) {
const response = await globalMethods.callHttpClient({
method: endpoints.GetAdyenSessionResult.method,
endpoint: endpoints.GetAdyenSessionResult.url,
payload: paymentSessionRequest,
logApiCall: true
});
return response.data;
},
hasSubmittedOrder() {
return window.sessionStorage.getItem(webStorageConstants.SUBMITTED_ORDER) !== null;

View file

@ -3,6 +3,8 @@ process.env.VUE_APP_CURRENT_ENVIRONMENT = 'Localhost';
process.env.VUE_APP_CUSTOMER_PORTAL_URL = 'https://myaccountdev.safelite.com/';
process.env.VUE_APP_GOOGLE_PLACES_API_KEY = 'AIzaSyCuLhQcDdZTTb4JzpUFms1OCch2dk5lHF0';
process.env.VUE_APP_SAFELITE_HOP = 'https://iss-hop-dev.glassclaim.com/fmgCheckoutShared.aspx';
process.env.VUE_APP_ADYEN_ENVIRONMENT = 'test';
process.env.VUE_APP_ADYEN_CLIENT_KEY = 'test_WYGT4F5ZT5BRRL5MPWHK6QLYOIXZXROD';
// GA & GTM
// NOTE: Using the old ISS site GTM Cotnainer ID for now, will create a new one soon.