Merge branch 'develop' into feature/SSR-1118

This commit is contained in:
Katie Kroell 2024-03-15 15:20:10 -04:00
commit 8076a7b59a
11 changed files with 234 additions and 92 deletions

View file

@ -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',

View file

@ -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);

View file

@ -5,5 +5,12 @@ Object {
"baseServiceLineItems": Array [],
"deductible": null,
"isExpanded": false,
"widget": Object {
"amountDue": "AmountDueTextWidget",
"basePrice": "BasePriceWidget",
"deductible": "DeductibleWidget",
"salesTax": "SalesTaxWidget",
"subtotal": "SubtotalWidget",
},
}
`;

View file

@ -121,6 +121,58 @@ describe('cart-dropdown component', () => {
// Assert
expect(cartBasePrice.exists()).toBeTruthy();
});
test('cart footer', () => {
// Arrange
const reference = '#cart-footer';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
// Act
const cartFooter = wrapper.find(reference);
// Assert
expect(cartFooter.exists()).toBeTruthy();
});
test('subtotal', () => {
// Arrange
const reference = '#cart-subtotal';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
// Act
const subtotal = wrapper.find(reference);
// Assert
expect(subtotal.exists()).toBeTruthy();
});
test('tax', () => {
// Arrange
const reference = '#cart-sales-tax';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
// Act
const salesTax = wrapper.find(reference);
// Assert
expect(salesTax.exists()).toBeTruthy();
});
test('bottom amount due', () => {
// Arrange
const reference = '#bottom-amount-due';
const isExpanded = true;
const initialData = { isExpanded };
const { wrapper } = getMountedComponent({}, {}, initialData);
// Act
const bottomAmountDue = wrapper.find(reference);
// Assert
expect(bottomAmountDue.exists()).toBeTruthy();
});
});
describe('does not display', () => {
test('cart deductible when showDeductibleLineItem is false', () => {

View file

@ -1,5 +1,5 @@
<template>
<div :class="[isExpanded ? 'pb-3' : 'pb-4']">
<div :class="[isExpanded ? 'pb-5' : 'pb-4']">
<div
id="cart-dropdown-head"
class="row cart-toggle flex align-items-center pt-4"
@ -9,50 +9,73 @@
aria-label="expand cart details"
href="javascript:void(0)"
class="col d-flex justify-content-between py-0">
<span class="cart-label color-black">{{ amountDueLabel }}</span>
<span class="cart-label amount-due">{{ getDisplayed(amountDue) }}</span>
<span class="color-black">{{ amountDueLabel }}</span>
<span class="color-green">{{ getDisplayed(amountDue) }}</span>
</a>
</div>
<div
id="cart-table"
class="cart-table">
<!-- Deductible Line Item -->
<div
id="cart-deductible-or-base-price"
class="mt-4 cart-line-item color-gray-100 px-5 py-1">
id="cart-line-items"
class="color-darker-gray">
<div
v-if="showDeductibleLineItem"
id="cart-deductible"
class="d-flex justify-content-between align-items-center">
<span
id="deductible-label"
class="cart-label">{{ deductibleLabel }}</span>
<span id="deductible-value">{{ getDisplayed(deductible) }}</span>
id="cart-deductible-or-base-price"
class="cart-deductible-or-base-price">
<div
v-if="showDeductibleLineItem"
id="cart-deductible"
class="d-flex justify-content-between align-items-center">
<span id="deductible-label">{{ deductibleLabel }}</span>
<span id="deductible-value">{{ getDisplayed(deductible) }}</span>
</div>
<div
v-else
id="cart-base-price"
class="d-flex justify-content-between align-items-center">
<span id="base-price-label">{{ basePriceLabel }}</span>
<span id="base-price-value">{{ getDisplayed(baseServicePrice) }}</span>
</div>
</div>
<div
v-else
id="cart-base-price"
class="d-flex justify-content-between align-items-center">
<span
id="base-price-label"
class="cart-label">{{ basePriceLabel }}</span>
<span id="base-price-value">{{ getDisplayed(baseServicePrice) }}</span>
<!-- Packaged Cart Items -->
<!-- Nonpackaged Cart Items -->
<!-- This will be a loop when this is implemented -->
<div class="non-packaged-cart-item">
<textLink
linkType="text"
text="Recycling"
href="javascript:void(0)"
@clickEvent="openModal(recyclingModalCmsWidgetName)" />
</div>
</div>
<!-- Packaged Cart Items -->
<!-- Nonpackaged Cart Items -->
<!-- This will be a loop when this is implemented -->
<div class="non-packaged-cart-item">
<textLink
linkType="text"
text="Recycling"
href="javascript:void(0)"
@clickEvent="openModal(recyclingModalCmsWidgetName)" />
<div
id="cart-footer"
class="cart-footer">
<div
id="cart-subtotal-and-tax"
class="subtotal-and-tax">
<div
id="cart-subtotal"
class="col d-flex justify-content-between">
<span id="subtotal-label">{{ subtotalLabel }}</span>
<span id="subtotal-value">{{ getDisplayed(subTotal) }}</span>
</div>
<div
id="cart-sales-tax"
class="pt-1 col d-flex justify-content-between">
<span id="sales-tax-label">{{ salesTaxLabel }}</span>
<span id="sales-tax-value">{{ getDisplayed(salesTax) }}</span>
</div>
</div>
<div
id="bottom-amount-due"
class="bottom-amount-due col d-flex justify-content-between">
<span id="bottom-amount-due-label">{{ amountDueLabel }}</span>
<span id="bottom-amount-due-value">{{ getDisplayed(amountDue) }}</span>
</div>
</div>
<!-- Sub total, sales tax, total columns -->
</div>
<contentGroupModal
ref="RecycleModal"
@ -69,6 +92,7 @@ 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 widgetFields from '@/constants/cms-widget-fields.js';
const VERIFYING_COVERAGE = 'Verifying coverage';
@ -78,9 +102,6 @@ export default {
textBlock,
textLink },
props: {
amountDueLabel: String,
basePriceLabel: String,
deductibleLabel: String,
recyclingModalCmsWidgetName: String,
showAsPaid: Boolean
},
@ -95,7 +116,14 @@ export default {
return {
deductible: currentDeductible,
baseServiceLineItems,
isExpanded: false
isExpanded: false,
widget: {
amountDue: 'AmountDueTextWidget',
deductible: 'DeductibleWidget',
basePrice: 'BasePriceWidget',
subtotal: 'SubtotalWidget',
salesTax: 'SalesTaxWidget'
}
};
},
computed: {
@ -120,6 +148,21 @@ export default {
return this.showAsPaid
? 0
: this.subTotal + this.salesTax;
},
amountDueLabel() {
return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
deductibleLabel() {
return this.getCmsContent(this.widget.deductible, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
basePriceLabel() {
return this.getCmsContent(this.widget.basePrice, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
subtotalLabel() {
return this.getCmsContent(this.widget.subtotal, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
salesTaxLabel() {
return this.getCmsContent(this.widget.salesTax, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}
},
methods: {
@ -136,7 +179,6 @@ export default {
}
}
};
</script>
<style lang="scss" scoped>
@ -145,11 +187,12 @@ export default {
.color-black {
color: $black;
}
.color-gray-100 {
background-color: $gray-100;
.color-green {
color: $green;
}
.cart-line-item {
.color-darker-gray {
color: $darker-gray;
}
@ -158,6 +201,29 @@ export default {
transition: all 350ms ease-in;
overflow: hidden;
visibility: hidden;
.cart-deductible-or-base-price {
background-color: $gray-100;
margin-top: 1rem;
padding: 0.25rem 1.5rem;
}
.cart-footer {
border-top: 1px solid $green;
}
.subtotal-and-tax {
background-color: $green-150;
font-weight: $font-weight-bold;
color: $black;
padding: 0.25rem 1.5rem;
}
.bottom-amount-due {
color: $white;
background-color: $green;
padding: 0.25rem 1.5rem;
}
}
:deep(.modal-content a.external-text) {
@ -175,6 +241,8 @@ export default {
}
.cart-toggle {
font-weight: $font-weight-bold;
&:after {
content: "";
transition: all 0.5s ease;
@ -201,13 +269,6 @@ export default {
a {
text-decoration: none;
}
.cart-label {
font-weight: $font-weight-bold;
line-height: 1.625;
}
.amount-due {
color: $green;
}
}
</style>

View file

@ -21,9 +21,6 @@
<hr class="my-0" />
<cartDropdown
:showAsPaid="false"
:amountDueLabel="amountDueText"
:deductibleLabel="deductibleLabel"
:basePriceLabel="basePriceLabel"
recyclingModalCmsWidgetName="RecycleModal" />
<hr class="mt-0 mb-5" />
<div>Pia Alert Placeholder</div>
@ -61,7 +58,6 @@ import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { paymentMethods } from '@/constants/payment-method-constants';
import globalRules from '@/constants/global-rules';
import { Form } from 'vee-validate';
import widgetFields from '@/constants/cms-widget-fields.js';
import { AppointmentTypeStrings } from '@/constants/schedule-constants';
@ -109,11 +105,6 @@ export default {
paymentMethodInternalModel: this.getPaymentMethodFromStore(),
rules: {
optionRequired: globalRules.OPTION_REQUIRED
},
widget: {
amountDue: 'AmountDueTextWidget',
deductible: 'DeductibleWidget',
basePrice: 'BasePriceWidget'
}
};
},
@ -132,15 +123,6 @@ export default {
},
paymentMethod() {
return this.paymentMethodInternalModel;
},
amountDueText() {
return this.getCmsContent(this.widget.amountDue, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
deductibleLabel() {
return this.getCmsContent(this.widget.deductible, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
basePriceLabel() {
return this.getCmsContent(this.widget.basePrice, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
}
},
watch: {

View file

@ -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}`);

View file

@ -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(

View file

@ -1145,7 +1145,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 +1233,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 +1279,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'}`

View file

@ -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);
});
});
});

View file

@ -28,6 +28,7 @@ $red-900: #330300;
// Greens
$green-100: #e3f2ea;
$green-150: #e6f2ec;
$green-200: #bcedd4;
$green-300: #94d7b6;
$green-400: #5abc8c; // Used in theme