diff --git a/package-lock.json b/package-lock.json index 26792b8c..0ab28ced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,10 @@ "axios-retry": "^3.5.0", "bootstrap": "^5.3", "iframe-resizer": "^4.3.9", - "maska": "^1.5.0", + "maska": "^2.1.1", "pinia": "^2.1.4", "pinia-plugin-persistedstate": "^2.2.0", + "uuid": "^9.0.1", "vee-validate": "^4.5.7", "vue": "^3.3.4", "vue-plugin-load-script": "^2.1.0", @@ -11599,6 +11600,15 @@ "node": ">=10" } }, + "node_modules/jest-junit/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest-leak-detector": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", @@ -13937,9 +13947,9 @@ } }, "node_modules/maska": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/maska/-/maska-1.5.0.tgz", - "integrity": "sha512-BwZXzs5gHeu6wtn3iWFqrKRtcsM3sTpkHvfAngVNVNlN7tl9ZyQUeHTz11s9Sy7Bq1MoQ+xyR/+IzghY8nR84Q==" + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/maska/-/maska-2.1.11.tgz", + "integrity": "sha512-IGqWjBnKxMYcVa06pb4mPfag9sJjnR2T15CdGfQ2llR3gajiSd4AxXCvNqHMEq9W3UBhjjTazgWumsP3sWrUSg==" }, "node_modules/mdn-data": { "version": "2.0.14", @@ -16662,6 +16672,15 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17730,10 +17749,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index 9c287dc0..82ca9450 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ "axios-retry": "^3.5.0", "bootstrap": "^5.3", "iframe-resizer": "^4.3.9", - "maska": "^1.5.0", + "maska": "^2.1.1", "pinia": "^2.1.4", "pinia-plugin-persistedstate": "^2.2.0", + "uuid": "^9.0.1", "vee-validate": "^4.5.7", "vue": "^3.3.4", "vue-plugin-load-script": "^2.1.0", @@ -60,4 +61,4 @@ "vitest": "^0.33.0", "volar-service-vetur": "latest" } -} +} \ No newline at end of file diff --git a/src/assets/img/icons/Google.svg b/src/assets/img/icons/Google.svg new file mode 100644 index 00000000..606351ec --- /dev/null +++ b/src/assets/img/icons/Google.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/assets/img/icons/Outlook.com.svg b/src/assets/img/icons/Outlook.com.svg new file mode 100644 index 00000000..467b585a --- /dev/null +++ b/src/assets/img/icons/Outlook.com.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/img/icons/Outlook.svg b/src/assets/img/icons/Outlook.svg new file mode 100644 index 00000000..f974fb13 --- /dev/null +++ b/src/assets/img/icons/Outlook.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/assets/img/icons/Yahoo.svg b/src/assets/img/icons/Yahoo.svg new file mode 100644 index 00000000..0206eccb --- /dev/null +++ b/src/assets/img/icons/Yahoo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/assets/img/icons/add-to-calendar.svg b/src/assets/img/icons/add-to-calendar.svg new file mode 100644 index 00000000..fd621c5c --- /dev/null +++ b/src/assets/img/icons/add-to-calendar.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/img/icons/iCal.svg b/src/assets/img/icons/iCal.svg new file mode 100644 index 00000000..399bcdea --- /dev/null +++ b/src/assets/img/icons/iCal.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/constants/application-config.js b/src/constants/application-config.js index ce39577b..7230f485 100644 --- a/src/constants/application-config.js +++ b/src/constants/application-config.js @@ -19,7 +19,11 @@ const applicationConfig = Object.freeze({ ISS_DEV_CMS_DOMAIN: 'https://digitalisscms.dev.safelite.io', CASH_PARENT_ACCOUNT_NUMBER: 167132, ITAC_FAIR_AND_REASONABLE_BILLTO: '214616', // Lib Mutal FnR BillTo - ITAC_CASH_BILLTO: '283393' // Lib Mutal ITAC Cash BillTo + ITAC_CASH_BILLTO: '283393', // Lib Mutal ITAC Cash BillTo + GOOGLE_CALENDAR: 'https://www.google.com/calendar/render?action=TEMPLATE', + YAHOO_CALENDAR: 'https://calendar.yahoo.com/?v=60', + OUTLOOK_CALENDAR: + 'https://outlook.office.com/calendar/deeplink/compose?path=/calendar/action/compose&rru=addevent' }); export default applicationConfig; diff --git a/src/constants/calendar-options.js b/src/constants/calendar-options.js new file mode 100644 index 00000000..22e9d5ef --- /dev/null +++ b/src/constants/calendar-options.js @@ -0,0 +1,8 @@ +const calendarOptions = Object.freeze({ + ICAL: 'iCal', + GOOGLE: 'Google', + OUTLOOK: 'Outlook', + OUTLOOKCOM: 'Outlook.com', + YAHOO: 'Yahoo' +}); +export default calendarOptions; diff --git a/src/constants/calendar-status.js b/src/constants/calendar-status.js new file mode 100644 index 00000000..5174f33a --- /dev/null +++ b/src/constants/calendar-status.js @@ -0,0 +1,7 @@ +const calendarStatus = Object.freeze({ + FREE: 'Free', + BUSY: 'Busy', + TENTATIVE: 'Tentative', + OUT_OF_THE_OFFICE: 'OutOfTheOffice' +}); +export default calendarStatus; diff --git a/src/constants/maska-masks.js b/src/constants/maska-masks.js new file mode 100644 index 00000000..b7d321c0 --- /dev/null +++ b/src/constants/maska-masks.js @@ -0,0 +1,15 @@ +const MaskaFormattedMasks = { + PHONE_NUMBER: { + mask: 'x##-###-####', + tokens: { + token: 'x', + pattern: '[2-9]' + } + }, + ZIPCODE: { + mask: '#####', + tokens: null + } +}; + +export default MaskaFormattedMasks; diff --git a/src/constants/query-strings.js b/src/constants/query-strings.js index 576d1e5b..81d5ba7d 100644 --- a/src/constants/query-strings.js +++ b/src/constants/query-strings.js @@ -9,6 +9,7 @@ const queryStrings = Object.freeze({ CARD_TYPE: 'sgcardtype', DISPLAY_PAY_IN_ADVANCE_ALERT: 'displayPayInAdvanceAlert', ERROR: 'error', + TOKEN: 'token', LAST_FOUR: 'last_four', REFERENCE_NUMBER: 'req_reference_number', REFERRAL_SEQ_NUM: 'referralseqnum', diff --git a/src/constants/service-type.js b/src/constants/service-type.js new file mode 100644 index 00000000..ebbec62d --- /dev/null +++ b/src/constants/service-type.js @@ -0,0 +1,6 @@ +const serviceType = Object.freeze({ + REPLACEMENT: 'replacement', + REPAIR: 'repair', + REPLACEMENT_AND_RECALIBRATION: 'replacement and recalibration' +}); +export default serviceType; diff --git a/src/digital-components/textbox-question/textbox-question.vue b/src/digital-components/textbox-question/textbox-question.vue index 2ba8d5b9..c97a6b4a 100644 --- a/src/digital-components/textbox-question/textbox-question.vue +++ b/src/digital-components/textbox-question/textbox-question.vue @@ -19,7 +19,9 @@ :id="inputId" :ref="inputId" v-model.trim="value" - v-maska="mask" + v-maska + :data-maska="getMaskFromProp" + :data-maska-tokens="getTokensFromProp" class="form-control" :type="type" :name="inputId" @@ -97,7 +99,7 @@ export default { iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used hasError: Boolean, mask: { - type: String, + type: [String, Object], default: '' }, validationRules: String, @@ -183,6 +185,21 @@ export default { return questionText; } + }, + getMaskFromProp() { + if (this.mask) { + if (this.mask.mask) { + return this.mask.mask; + } + return this.mask; + } + return null; + }, + getTokensFromProp() { + if (this.mask?.tokens) { + return `{ '${this.mask.tokens.token}': { 'pattern': '${this.mask.tokens.pattern}' }}`; + } + return null; } }, watch: { diff --git a/src/helpers/add-to-calendar-helper.js b/src/helpers/add-to-calendar-helper.js new file mode 100644 index 00000000..b00f891b --- /dev/null +++ b/src/helpers/add-to-calendar-helper.js @@ -0,0 +1,75 @@ +import calendarStatus from '@/constants/calendar-status'; +import { getDateFormat } from '@/helpers/date-helper'; + +function ToCalendarFileString(str, replacementArg = '

') { + return str.replace('\r\n', replacementArg); +} + +export function getCalendarFile(calFile) { + const dateFormat = 'yyyyMMddTHHmmss'; + const calEvent = []; + calEvent.push('BEGIN:VCALENDAR'); + calEvent.push('VERSION:2.0'); + calEvent.push('BEGIN:VEVENT'); + calEvent.push(`DTSTAMP:${calFile.TimeStamp}`); + calEvent.push(`UID:${calFile.UniqueId}@safelite.com`); + calEvent.push('PRODID:noreply@safelite.com'); + switch (calFile.Status) { + case calendarStatus.BUSY: + calEvent.push('X-MICROSOFT-CDO-BUSYSTATUS:BUSY'); + break; + case calendarStatus.FREE: + calEvent.push('TRANSP:TRANSPARENT'); + break; + case calendarStatus.TENTATIVE: + calEvent.push('STATUS:TENTATIVE'); + break; + case calendarStatus.OUT_OF_THE_OFFICE: + calEvent.push('X-MICROSOFT-CDO-BUSYSTATUS:OOF'); + break; + default: + // throw new Exception("Invalid CalendarStatus"); + } + if (calFile.AllDayEvent) { + calEvent.push(`DTSTART;VALUE=DATE:${getDateFormat(calFile.StartDate, dateFormat)}`); + calEvent.push(`DTEND;;VALUE=DATE:${getDateFormat(calFile.EndDate, dateFormat)}`); + } else { + calEvent.push(`DTSTART:${getDateFormat(calFile.StartDate, dateFormat)}`); + calEvent.push(`DTEND:${getDateFormat(calFile.EndDate, dateFormat)}`); + } + + calEvent.push(`SUMMARY:${ToCalendarFileString(calFile.Subject, ' ')}`); + if (calFile.Location != null && calFile.Location !== '') { + calEvent.push(`LOCATION:${ToCalendarFileString(calFile.Location, ' ')}`); + } + if (calFile.Body != null && calFile.Body !== '') { + if (calFile.IsHTML) { + calEvent.push(`DESCRIPTION:${ToCalendarFileString(calFile.Body)}`); + calEvent.push('X-ALT-DESC;FMTTYPE=text/html:' + + '' + + '' + + '

' + + `${ + ToCalendarFileString(calFile.Body) + }`); + } else { + calEvent.push(`DESCRIPTION:${ToCalendarFileString(calFile.Body)}`); + } + } + calEvent.push('END:VEVENT'); + calEvent.push('END:VCALENDAR'); + return calEvent.join('\r\n'); +} + +export function download(filename, fileBody) { + const element = document.createElement('a'); + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(fileBody)}`); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} diff --git a/src/helpers/date-helper.js b/src/helpers/date-helper.js index be8161e8..70f598a6 100644 --- a/src/helpers/date-helper.js +++ b/src/helpers/date-helper.js @@ -104,3 +104,72 @@ export function get12HourTimeFormat(time) { } return timeString.join(''); // return adjusted time or original string } + +export function getDateFormat(date, format) { + if (date instanceof Date !== true) return; + const year = date.getFullYear(); + const month = (`0${date.getMonth() + 1}`).slice(-2); + const day = (`0${date.getDate()}`).slice(-2); + const hour = (`0${date.getHours()}`).slice(-2); + const minute = (`0${date.getMinutes()}`).slice(-2); + const second = (`0${date.getSeconds()}`).slice(-2); + // eslint-disable-next-line consistent-return + return format + .replace('yyyy', year) + .replace('MM', month) + .replace('dd', day) + .replace('hh', hour) + .replace('HH', hour) + .replace('mm', minute) + .replace('ss', second); +} + +export function padTo2Digits(time) { + // Use the built-in method toString() with a radix of 10 to convert the time value to a decimal string + // eslint-disable-next-line no-param-reassign + time = time.toString(10); + // Use the conditional operator to check if the length of the string is less than 2 + return time.length < 2 + // If yes, prepend a '0' to the string and return it + // If no, return the original string + ? `0${time}` : time; +} + +export function convertMsToTime(milliseconds) { + let seconds = Math.floor(milliseconds / 1000); + let minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + seconds %= 60; + minutes %= 60; + // commenting to get 24 time format + // hours = hours % 24; + + return `${padTo2Digits(hours)}${padTo2Digits(minutes)}`; +} + +export function calculateDuration(startDate, endDate) { + if (startDate instanceof Date !== true) return; + if (endDate instanceof Date !== true) return; + // eslint-disable-next-line consistent-return + return convertMsToTime(endDate - startDate); +} + +export function combineDateAndTime(date, time) { + // Use the Date.parse() method to convert the date and time strings to a numeric value + const timestamp = Date.parse(`${date}T${time}`); + // Use the new Date() constructor to create a new date object from the numeric value + const newDate = new Date(timestamp); + // Return the new date object + return newDate; +} +export function addMinutes(date, minutes) { + return new Date(date.getTime() + minutes * 60000); +} +export function shortTimeString(date) { + // Use a ternary operator to check if the input is a valid date object + return date instanceof Date + // Use the built-in method toLocaleTimeString() to get the short time string in the current locale + // Return undefined if the input is not a valid date object + ? date.toLocaleTimeString('en-us', { hour: 'numeric', minute: 'numeric', hour12: true }) : undefined; +} diff --git a/src/helpers/order-helper.js b/src/helpers/order-helper.js index 33e7d3c4..97b685cd 100644 --- a/src/helpers/order-helper.js +++ b/src/helpers/order-helper.js @@ -4,8 +4,8 @@ import { updateOrCreateISSCookie } from '@/helpers/cookie-helper'; /* Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing */ -async function saveSessionHelper(store) { - const savedSessionInfo = await store.saveSession(); +async function saveSessionHelper(store, { submitAfterSave }) { + const savedSessionInfo = await store.saveSession({ submitAfterSave }); if (savedSessionInfo) { store.setSaveSessionInfo(savedSessionInfo.data); } @@ -17,11 +17,11 @@ async function saveSessionHelper(store) { 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({ shouldAwaitSaveSessionQueue = false }) { +export async function saveSession({ shouldAwaitSaveSessionQueue = false, submitAfterSave = false }) { const store = useMainStore(); const saveSessionPromise = store.applicationUser.saveSessionPromise - ? store.applicationUser.saveSessionPromise.then(() => saveSessionHelper(store)) - : saveSessionHelper(store); + ? store.applicationUser.saveSessionPromise.then(() => saveSessionHelper(store, { submitAfterSave })) + : saveSessionHelper(store, { submitAfterSave }); store.setSaveSessionPromise(saveSessionPromise); diff --git a/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap b/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap index f635c1f9..e6e66827 100644 --- a/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap +++ b/src/iss-components/cart-dropdown/__snapshots__/cart-dropdown.spec.js.snap @@ -9,6 +9,8 @@ Object { "amountDue": "AmountDueTextWidget", "basePrice": "BasePriceWidget", "deductible": "DeductibleWidget", + "mobileFee": "MobileServiceWidget", + "recycleFee": "RecycleFeeWidget", "salesTax": "SalesTaxWidget", "subtotal": "SubtotalWidget", }, diff --git a/src/iss-components/cart-dropdown/cart-dropdown.spec.js b/src/iss-components/cart-dropdown/cart-dropdown.spec.js index 8ef2cd50..fc09a48e 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.spec.js +++ b/src/iss-components/cart-dropdown/cart-dropdown.spec.js @@ -7,6 +7,7 @@ import { getMountOptions } from '@/helpers/unit-test-helper.js'; import { useMainStore } from '@/store'; import coverageStatuses from '@/constants/coverage-statuses'; import { formatAmountInDollars } from '@/helpers/text-helper.js'; +import partTypeStrings from '@/constants/part-type-strings'; const VERIFYING_COVERAGE = 'Verifying coverage'; @@ -41,7 +42,13 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, propsData describe('cart-dropdown component', () => { test('initial data rendered as expected', () => { // Arrange - const { wrapper } = getMountedComponent(); + const { wrapper } = getMountedComponent({ + order: { + lineItems: { + supportingItems: [] + } + } + }); // Assert expect(wrapper.vm.$data).toMatchSnapshot(); @@ -568,5 +575,85 @@ describe('cart-dropdown component', () => { expect(result).toBe(dollarAmount); }); }); + describe('Recyle fee', () => { + test('Shows recycle fee when preset in supported items', () => { + // Arrange + const reference = '#recycle-fee-item'; + const {wrapper} = getMountedComponent({ + order: { + lineItems: { + supportingItems: [{ + partType: partTypeStrings.REPLACE_FEE + }] + } + } + }); + + // Act + const cartDeductible = wrapper.find(reference); + + // Assert + expect(wrapper.vm.recycleFeeCartItem).not.toBeNull(); + expect(cartDeductible.exists()).toBeTruthy(); + }); + test('Hides recycle fee when not in supported items', () => { + // Arrange + const reference = '#recycle-fee-item'; + const {wrapper} = getMountedComponent({ + order: { + lineItems: { + supportingItems: [] + } + } + }); + + // Act + const cartDeductible = wrapper.find(reference); + + // Assert + expect(wrapper.vm.recycleFeeCartItem).toBeNull(); + expect(cartDeductible.exists()).toBeFalsy(); + }); + }); + describe('Mobile Fee', () => { + test('Shows mobile fee when preset', () => { + // Arrange + const reference = '#mobile-fee-item'; + const { wrapper } = getMountedComponent({ + order: { + lineItems: { + supportingItems: [], + mobileFee: {} + } + } + }); + + // Act + const cartDeductible = wrapper.find(reference); + + // Assert + expect(wrapper.vm.mobileFeeCartItem).not.toBeNull(); + expect(cartDeductible.exists()).toBeTruthy(); + }); + test('Hides recycle fee when not in supported items', () => { + // Arrange + const reference = '#mobile-fee-item'; + const { wrapper } = getMountedComponent({ + order: { + lineItems: { + supportingItems: [], + mobileFee: null + } + } + }); + + // Act + const cartDeductible = wrapper.find(reference); + + // Assert + expect(wrapper.vm.mobileFeeCartItem).toBeNull(); + expect(cartDeductible.exists()).toBeFalsy(); + }); + }); }); }); diff --git a/src/iss-components/cart-dropdown/cart-dropdown.vue b/src/iss-components/cart-dropdown/cart-dropdown.vue index 2df3782f..590a77ae 100644 --- a/src/iss-components/cart-dropdown/cart-dropdown.vue +++ b/src/iss-components/cart-dropdown/cart-dropdown.vue @@ -21,7 +21,7 @@ class="color-darker-gray">

+ class="cart-item cart-gray">
- -
-
- + + +
- - - @@ -92,6 +116,8 @@ import textBlock from '@/digital-components/text-block/text-block.vue'; import textLink from '@/ux-components/text-link/text-link.vue'; import getPriceOfLineItems from '@/helpers/price-calculator.js'; import { formatAmountInDollars } from '@/helpers/text-helper.js'; +import baseMixin from '@/mixins/base-mixin'; +import partTypeStrings from '@/constants/part-type-strings'; import widgetFields from '@/constants/cms-widget-fields.js'; const VERIFYING_COVERAGE = 'Verifying coverage'; @@ -105,6 +131,10 @@ export default { recyclingModalCmsWidgetName: String, showAsPaid: Boolean }, + setup() { + const mainStore = useMainStore(); + return { mainStore }; + }, data() { const { currentDeductible } = useMainStore().order; const { supportingItems, glassParts, otherParts } = useMainStore().lineItems; @@ -122,7 +152,9 @@ export default { deductible: 'DeductibleWidget', basePrice: 'BasePriceWidget', subtotal: 'SubtotalWidget', - salesTax: 'SalesTaxWidget' + salesTax: 'SalesTaxWidget', + recycleFee: 'RecycleFeeWidget', + mobileFee: 'MobileServiceWidget' } }; }, @@ -163,6 +195,18 @@ export default { }, salesTaxLabel() { return this.getCmsContent(this.widget.salesTax, widgetFields.TEXT_BLOCK_WIDGET.TEXT); + }, + recycleFeeCartItem() { + return this.getCartItem( + this.getCmsContent(this.widget.recycleFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT), + this.mainStore.order.lineItems.supportingItems.find((lineItem) => lineItem.partType === partTypeStrings.REPLACE_FEE) + ); + }, + mobileFeeCartItem() { + return this.getCartItem( + this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT), + this.mainStore.order.lineItems.mobileFee + ); } }, methods: { @@ -176,6 +220,19 @@ export default { return this.isUnverified && this.showDeductibleLineItem ? VERIFYING_COVERAGE : formatAmountInDollars(amount); + }, + getCartItem(label, lineItem) { + let cartItem = null; + + if (lineItem) { + cartItem = { + name: label, + subTotal: baseMixin.methods.getTotalLineItemPrice(lineItem, false), + salesTax: lineItem.salesTax ?? 0 + }; + } + + return cartItem; } } }; @@ -201,13 +258,16 @@ export default { transition: all 350ms ease-in; overflow: hidden; visibility: hidden; + margin-top: 1rem; - .cart-deductible-or-base-price { - background-color: $gray-100; - margin-top: 1rem; + .cart-item { padding: 0.25rem 1.5rem; } + .cart-gray { + background-color: $gray-100; + } + .cart-footer { border-top: 1px solid $green; } @@ -269,6 +329,9 @@ export default { a { text-decoration: none; } +} +:deep(#recycle-fee-label a){ + line-height: initial; } diff --git a/src/layouts/bailout-page/bailout-page.vue b/src/layouts/bailout-page/bailout-page.vue index 4a60c8e9..131665fe 100644 --- a/src/layouts/bailout-page/bailout-page.vue +++ b/src/layouts/bailout-page/bailout-page.vue @@ -39,7 +39,7 @@ inputId="phoneNumberField" cmsWidgetName="PhoneNumberQuestion" isRequired - :mask="mask" + :mask="phoneMask" disableAutoFill :validationRules="rules.phoneNumber" /> appointmentText) + } + } + ]; + const wrapper = shallowMount(addToCalendar, mountOptions); + wrapper.vm.$refs.calendarModalQuestion.openModal = jest.fn(); + return { wrapper }; +} + +beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + useMainStore().submittedOrder = { + schedule: { + date: '2019-01-01', + startTime: '09:00', + endTime: '10:00', + routeCode: '000' + }, + lineItems: { + glassParts: [ + { + partNumber: 'ABC123' + } + ], + supportingItems: [] + }, + serviceLocation: { + address: '', + address2: null, + city: '', + state: 'AZ', + appointmentType: 'Inshop', + zipCode: '12345', + zipCodeCtu: '01234', + provider: { + providerNumber: '123', + address: { + streetAddress: 'test1', + city: 'test', + state: 'AZ', + zipCode: '12345', + zipCodeCtu: '01234' + } + } + }, + damage: { + isRepair: false + }, + referralNumber: '1234567', + payment: { + isInsurance: true + } + }; +}); +afterEach(() => { + useMainStore().submittedOrder = {}; + jest.restoreAllMocks(); + jest.clearAllMocks(); +}); + +describe('Add-to-calendar methods...', () => { + test('Add-to-calendar should trigger openModal method', () => { + // Arrange + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + appointment: { + RefrrlSeqNum: '123456', + StartDate: new Date(2023, 9, 11, 12, 0, 0), + EndDate: new Date(2023, 9, 11, 13, 0, 0), + Subject: 'Meeting with John', + Location: 'Conference Room', + Body: 'Discuss the project progress', + IsHTML: true, + Duration: '1220' + } + } + } + }); + + // Act + wrapper.vm.openCalendarModal(); + + // Assert + expect(wrapper.vm.$refs.calendarModalQuestion.openModal).toBeCalled(); + }); + + test('getCalendarData should return expected model value.', () => { + // Arrange + const type = calendarOptions.OUTLOOKCOM; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + appointment: { + RefrrlSeqNum: '123456', + StartDate: new Date(2023, 9, 11, 12, 0, 0), + EndDate: new Date(2023, 9, 11, 13, 0, 0), + Subject: 'Meeting with John', + Location: 'Conference Room', + Body: 'Discuss the project progress', + IsHTML: true, + Duration: '1220' + } + } + } + }); + + // Act + const testValue = wrapper.vm.getCalendarData(type); + + // Assert + expect(testValue.name).toEqual(calendarOptions.OUTLOOKCOM); + }); + + test('setCalendarAppointment should call getAppointment method for iCAL and Outlook.', () => { + // Arrange + const newValue = { name: calendarOptions.OUTLOOK }; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + appointment: { + RefrrlSeqNum: '123456', + StartDate: new Date(2023, 9, 11, 12, 0, 0), + EndDate: new Date(2023, 9, 11, 13, 0, 0), + Subject: 'Meeting with John', + Location: 'Conference Room', + Body: 'Discuss the project progress', + IsHTML: true, + Duration: '1220' + } + } + } + }); + wrapper.vm.getAppointment = jest.fn(); + // Act + wrapper.vm.setCalendarAppointment(newValue); + + // Assert + expect(wrapper.vm.getAppointment).toHaveBeenCalled(); + }); + test('setCalendarAppointment should call window.open for Google, yahoo and Outlook.com.', () => { + // Arrange + const newValue = { name: calendarOptions.GOOGLE }; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + appointment: { + RefrrlSeqNum: '123456', + StartDate: new Date(2023, 9, 11, 12, 0, 0), + EndDate: new Date(2023, 9, 11, 13, 0, 0), + Subject: 'Meeting with John', + Location: 'Conference Room', + Body: 'Discuss the project progress', + IsHTML: true, + Duration: '1220' + } + } + } + }); + window.open = jest.fn(); + + // Act + wrapper.vm.setCalendarAppointment(newValue); + + // Assert + expect(window.open).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue b/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue new file mode 100644 index 00000000..ebd7eb47 --- /dev/null +++ b/src/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue @@ -0,0 +1,325 @@ + + + diff --git a/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-list-button/calendar-modal-list-button.vue b/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-list-button/calendar-modal-list-button.vue new file mode 100644 index 00000000..814d10fe --- /dev/null +++ b/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-list-button/calendar-modal-list-button.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.spec.js b/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.spec.js new file mode 100644 index 00000000..51b43508 --- /dev/null +++ b/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.spec.js @@ -0,0 +1,41 @@ +import { shallowMount } from '@vue/test-utils'; +import calendarModalQuestion from '@/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.vue'; + +describe('calendar-modal-question.vue', () => { + test('When selected calendar option is changed, a correctly selectedCalendarOption should be emitted', async () => { + // Arrange + const url = 'iCal url'; + const name = 'iCal'; + const icon = 'iCal.svg'; + const expectedEmit = [ + [ + { + url, + name, + icon + } + ] + ]; + + const wrapper = shallowMount(calendarModalQuestion, { + propsData: { + calendarOptionsData: [ + { + url, + name, + icon + } + ] + } + }); + + wrapper.vm.$refs.calendarOptions.closeModal = jest.fn(); + wrapper.vm.selectedCalendarOption = name; + + // Act + wrapper.vm.setSelectedCalendarOption(); + + // Assert + expect(wrapper.emitted()['update:modelValue']).toEqual(expectedEmit); + }); +}); diff --git a/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.vue b/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.vue new file mode 100644 index 00000000..702abf1b --- /dev/null +++ b/src/layouts/order-confirmation/add-to-calendar/calendar-modal-question/calendar-modal-question.vue @@ -0,0 +1,180 @@ + + + diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index 99ba8fa7..47e0f137 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -39,6 +39,10 @@ const vehicleBannerStub = { render: () => {} }; +const addToCalendarStub = { + render: () => {} +}; + const initialStore = { order: { schedule: { @@ -127,7 +131,8 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRu mountOptions.global.stubs = { siteFooter: footerStub, siteHeader: headerStub, - vehicleBanner: vehicleBannerStub + vehicleBanner: vehicleBannerStub, + addToCalendar: addToCalendarStub }; const testingPinia = createTestingPinia({ diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index fa7a4c14..7ee31d95 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -25,6 +25,19 @@

{{ appointmentDateFormatted }}

{{ appointmentTimeFormatted }}

+
@@ -52,6 +65,7 @@ import siteHeader from '@/iss-components/site-header/site-header.vue'; import vehicleBanner from '@/iss-components/vehicle-banner/vehicle-banner.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue'; +import addToCalendar from '@/layouts/order-confirmation/add-to-calendar/add-to-calendar.vue'; // Supporting files import { fetchCmsContentForPage, processIfStatements } from '@/helpers/cms-content-helper'; import settleAllPromises from '@/helpers/layout-helper'; @@ -72,7 +86,8 @@ export default { Form, siteHeader, vehicleBanner, - siteFooter + siteFooter, + addToCalendar }, mixins: [BaseFormMixin], async beforeRouteEnter(to, from, next) { @@ -180,7 +195,7 @@ export default { }, providerFullAddress() { // eslint-disable-next-line max-len - return `${this.providerAddress},
${this.providerCity}, ${this.providerState} ${this.providerZipCode}`; + return this.submittedOrder.serviceLocation?.provider?.address ? `${this.providerAddress},
${this.providerCity}, ${this.providerState} ${this.providerZipCode}` : ''; }, appointmentWordingText() { switch (this.appointmentType) { @@ -297,15 +312,15 @@ $page-side-padding: 1.5rem; .appointment-details { margin-bottom: 1.5rem; padding: 1rem 1.5rem 1.5rem; - box-shadow: 0 3px 10px rgb(0 0 0 / 0.2); - border-radius: 5px; + box-shadow: 0 0.188rem 0.625rem rgb(0 0 0 / 0.2); + border-radius: 0.313rem; } .appointment-date-time P { color: $black; font-size: $h5-font-size; line-height: map-get($spacers, 6); - margin-bottom: 0.5rem; + margin-bottom: 0; + p { font-size: map-get($spacers, 4); diff --git a/src/layouts/payment-return/payment-return.vue b/src/layouts/payment-return/payment-return.vue index 8e5d3de6..4a29f2ea 100644 --- a/src/layouts/payment-return/payment-return.vue +++ b/src/layouts/payment-return/payment-return.vue @@ -74,6 +74,7 @@ export default { await this.processCreditCardResponse(); break; case paymentMethods.PAYPAL: + await this.processPaypalResponse(); break; default: console.log(`Unknown pay in advance type: ${payInAdvanceType}`); diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 5fbb91c6..2f503879 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -340,8 +340,11 @@ export default { await this.$refs.shopQuestion.reloadShopData(zipCode); }, async forwardButtonAction() { - const providerToUse = this.isMobileLocationDisplayed - ? { + let provider = this.selectedProvider; + this.mainStore.updateMobileFee(null); + if (this.isMobileLocationDisplayed) { + this.mainStore.updateMobileFee(this.mobileFeePart); + provider = { providerNumber: this.mobileProviderNumber, address: { streetAddress: null, @@ -350,10 +353,10 @@ export default { zipCode: null, zipCodeCtu: null } - } - : this.selectedProvider; + }; + } - useMainStore().saveServiceLocation({ + this.mainStore.saveServiceLocation({ address: this.streetAddress, address2: this.streetAddress2, city: this.city, @@ -362,16 +365,7 @@ export default { zipCodeCtu: this.zipCodeCtu, appointmentType: this.selectedAppointmentType, isVehicleProtected: this.isVehicleProtected, - provider: { - providerNumber: providerToUse?.providerNumber, - address: { - streetAddress: providerToUse?.address?.streetAddress, - city: providerToUse?.address?.city, - state: providerToUse?.address?.state, - zipCode: providerToUse?.address?.zipCode, - zipCodeCtu: providerToUse?.address?.zipCodeCtu - } - } + provider }); this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD, this.$route); diff --git a/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue b/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue index fd5ab900..5f1b0b6f 100644 --- a/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue +++ b/src/layouts/tpa-submit/contact-details-drawer/contact-details-drawer.vue @@ -38,7 +38,7 @@ inputId="phoneNumber" :cmsWidgetName="widget.phoneNumberQuestion" isRequired - :mask="mask" + :mask="phoneMask" disableAutoFill :validationRules="rules.phoneNumber" /> @@ -53,6 +53,7 @@ import textboxQuestion from '@/digital-components/textbox-question/textbox-quest // Supporting Files import { useMainStore } from '@/store/index.js'; import globalRules from '@/constants/global-rules.js'; +import MaskaFormattedMasks from '@/constants/maska-masks'; import widgetFields from '@/constants/cms-widget-fields.js'; export default { @@ -102,15 +103,8 @@ export default { footerButtonText() { return this.getCmsContent(this.widget.drawerFooter, widgetFields.FOOTER_WIDGET.FORWARD_BUTTON_TEXT); }, - mask() { - return { - mask: 'x##-###-####', - tokens: { - x: { - pattern: /[2-9]/ - } - } - }; + phoneMask() { + return MaskaFormattedMasks.PHONE_NUMBER; } }, methods: { diff --git a/src/layouts/welcome-page/welcome-page.vue b/src/layouts/welcome-page/welcome-page.vue index df58619e..30b388f9 100644 --- a/src/layouts/welcome-page/welcome-page.vue +++ b/src/layouts/welcome-page/welcome-page.vue @@ -86,7 +86,7 @@ cmsWidgetName="PhoneNumberQuestion" :validationRules="rules.phoneNumber" isRequired - :mask="mask" + :mask="phoneMask" disableAutoFill />
@@ -197,6 +197,7 @@ import { useMainStore } from '@/store'; import states from '@/constants/states'; import globalRules from '@/constants/global-rules'; import routerParams from '@/router/router-constants/router-params'; +import MaskaFormattedMasks from '@/constants/maska-masks'; // define validation rules defineRule('damage-option-required', required(errorMessages.DAMAGE_OPTION_REQUIRED)); @@ -313,15 +314,8 @@ export default { maxCoverageLookupAttemptsReached() { return this.mainStore.applicationUser.coverageLookupAttempts >= 11; }, - mask() { - return { - mask: 'x##-###-####', - tokens: { - x: { - pattern: /[2-9]/ - } - } - }; + phoneMask() { + return MaskaFormattedMasks.PHONE_NUMBER; } }, methods: { diff --git a/src/main.js b/src/main.js index 68987d2e..fa8cc769 100644 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import { useMainStore } from '@/store'; import baseMixin from '@/mixins/base-mixin'; import { createPinia } from 'pinia'; -import Maska from 'maska'; +import { vMaska } from 'maska'; import LoadScript from 'vue-plugin-load-script'; import analyticsMixin from '@/mixins/analytics-mixin'; @@ -29,8 +29,8 @@ useMainStore().populateInitialState(); // Additional Vue items to setup vueApp.use(router); -vueApp.use(Maska); vueApp.use(LoadScript); +vueApp.directive('maska', vMaska); vueApp.mixin(baseMixin); vueApp.mixin(analyticsMixin); vueApp.mixin(experimentMixin); diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js index 0cbeec5b..3816d5c2 100644 --- a/src/mixins/base-mixin.js +++ b/src/mixins/base-mixin.js @@ -67,6 +67,10 @@ export default { }); }, getAmountDue(lineItems) { + if (!useMainStore().isNoComp && !useMainStore().policy.isITAC) { + return useMainStore().order.currentDeductible; + } + let amountDue = 0; if (lineItems.glassParts) { amountDue += this.getTotalPriceOfAllLineItemsAndChildParts( diff --git a/src/store/index.js b/src/store/index.js index 09c18a81..5b9e2bd9 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -147,7 +147,8 @@ const getDefaultState = () => ({ glassParts: null, otherParts: null, supportingItems: null, - vaps: null + vaps: null, + mobileFee: null }, payment: { insuranceCoverage: { @@ -1145,7 +1146,7 @@ export const useMainStore = defineStore({ this.applicationUser.crmCustomerId = response.crmCustomerId.toString(); }, - saveSession() { + saveSession({ submitAfterSave }) { const { vehicle, damage, policy, customer, contactInfo, payment, lineItems, serviceLocation, schedule } = this.order; @@ -1233,7 +1234,12 @@ export const useMainStore = defineStore({ coverageStatus: payment.insuranceCoverage?.coverageStatus, claimNumber: payment.insuranceCoverage?.claimNumber }, - parentAccountNumber: this.issConfig.parentAccountNumber + parentAccountNumber: this.issConfig.parentAccountNumber, + paypalToken: payment.paypalToken, + isPaypal: payment.payInAdvanceType === paymentMethods.PAYPAL, + isCreditCard: payment.payInAdvanceType === paymentMethods.CREDIT_CARD, + isAfterpay: payment.payInAdvanceType === paymentMethods.AFTERPAY, + CCToken: payment.creditCardToken }, serviceLocation: { address: { @@ -1274,7 +1280,8 @@ export const useMainStore = defineStore({ referralSequenceNumber: this.order.referralSequenceNumber, eon: this.order.eon, submitToMainframe: !!this.order.referralNumber, - loadedFromDupeCheck + loadedFromDupeCheck, + submitAfterSave: !!submitAfterSave }, additionalSuccessEventDataHandler: () => `Email provided: ${customer.emailAddress ? 'true' : 'false'}` @@ -1531,6 +1538,10 @@ export const useMainStore = defineStore({ this.order.lineItems.vaps = partsData; }, + updateMobileFee(mobileFee) { + this.order.lineItems.mobileFee = mobileFee; + }, + updateVehicle(vehicle) { // Assuming that the method caller pass all the properties. // otherwise need to check for undefined for every property. @@ -2243,6 +2254,7 @@ export const useMainStore = defineStore({ resetPartsAndDependencies() { this.resetGlassPartsState(); this.updateVaps(null); + this.updateMobileFee(null); this.resetServiceLocationAndDependencies(); }, diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 3a44c37e..63e55894 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -503,7 +503,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Asserts expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -517,7 +517,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(response)); // Act - const result = store.saveSession(); + const result = store.saveSession({}); // Asserts await expect(result).resolves.toBe(response); @@ -537,7 +537,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Asserts expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -570,7 +570,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Asserts expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -612,7 +612,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -659,7 +659,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Asserts expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -708,7 +708,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -742,7 +742,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -769,7 +769,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -802,7 +802,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -838,7 +838,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -861,7 +861,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -875,7 +875,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Assert expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -888,7 +888,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Asserts expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -904,7 +904,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Asserts expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -927,7 +927,7 @@ describe('Store', () => { globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); // Act - await store.saveSession(); + await store.saveSession({}); // Asserts expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ @@ -941,13 +941,27 @@ describe('Store', () => { }) })); }); + it('calls api with expected submitAfterSave', async () => { + // Arrange + globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); + + // Act + await store.saveSession({ submitAfterSave: true }); + + // Asserts + expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ + payload: expect.objectContaining({ + submitAfterSave: true + }) + })); + }); it('api call throws exception', async () => { expect.assertions(2); const error = 'save session error'; globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.reject(error)); // Act - await store.saveSession().catch((e) => { + await store.saveSession({}).catch((e) => { expect(e).toEqual(error); }); @@ -1887,4 +1901,17 @@ describe('Store', () => { expect(store.order.payment.payInAdvanceType).toEqual(paymentMethod); }); }); + + describe('updatePaypalToken method', () => { + it('paypalToken is valid when set', () => { + // Arrange + const paypalToken = getRandomString(6, 6); + + // Act + store.updatePaypalToken(paypalToken); + + // Assert + expect(store.order.payment.paypalToken).toEqual(paypalToken); + }); + }); });