diff --git a/src/constants/cart-item-types.js b/src/constants/cart-item-types.js
index d8ab9f66f..2f84d22c2 100644
--- a/src/constants/cart-item-types.js
+++ b/src/constants/cart-item-types.js
@@ -11,6 +11,7 @@ const cartItemTypes = {
EARLY_BIRD: "EARLY BIRD",
SUPPLIES_REPAIR: "SUPPLIES-REPAIR",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
+ DONATION: "DONATION",
};
export { cartItemTypes };
diff --git a/src/constants/experiments.js b/src/constants/experiments.js
index 09dfc4140..d64849466 100644
--- a/src/constants/experiments.js
+++ b/src/constants/experiments.js
@@ -2,6 +2,7 @@ const experimentUniverses = {
CONCEPT_FUNNEL: "ConceptFunnel",
RECAL_PRICE_REMOVAL: "NextGen_RecalPriceRemoval",
MSR: "MSR",
+ IGQ_SkipQuote: "NextGen_IGQSkipToInsurance",
};
const experimentSettings = {
@@ -22,6 +23,7 @@ const experimentSettings = {
WAITLIST_THRESHOLD_DAYS: "Waitlist_Threshold_Days",
PRICING_BY_DAY: "DisplayPricingByDay",
FOSTER_LOVE: "DisplayFosterLove",
+ SKIP_TO_INSURANCE: "SkipToInsurance",
};
const experimentTriggers = {
diff --git a/src/constants/package-names.js b/src/constants/package-names.js
index 221a75a49..0115daa61 100644
--- a/src/constants/package-names.js
+++ b/src/constants/package-names.js
@@ -3,3 +3,9 @@ export const packageNames = {
TIER_TWO: "TierTwo",
TIER_THREE: "TierThree",
};
+
+export const externalParamPackageLabels = {
+ GLASS_ONLY: "glassonly",
+ STANDARD: "standard",
+ PREMIUM: "premium",
+};
diff --git a/src/constants/part-number-strings.js b/src/constants/part-number-strings.js
index d373ba016..488bf0742 100644
--- a/src/constants/part-number-strings.js
+++ b/src/constants/part-number-strings.js
@@ -5,6 +5,7 @@ const partNumberStrings = {
DONATION: "DONATION",
// Fees
RECYCLE_FEE: "RECYCLE FEE",
+ PRICING_BY_DAY_UPCHARGE: "PREMAPPT GEN",
};
export { partNumberStrings };
diff --git a/src/constants/part-type-strings.js b/src/constants/part-type-strings.js
index 4681e2776..94e8598bd 100644
--- a/src/constants/part-type-strings.js
+++ b/src/constants/part-type-strings.js
@@ -10,6 +10,7 @@ const partTypeStrings = {
REPAIR_FEE: "REPAIR FEE",
EARLY_BIRD: "EARLY BIRD",
SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT",
+ DONATION: "DONATION",
};
export { partTypeStrings };
diff --git a/src/constants/schedule-constants.js b/src/constants/schedule-constants.js
index a0aaf8316..fbd165ad1 100644
--- a/src/constants/schedule-constants.js
+++ b/src/constants/schedule-constants.js
@@ -8,8 +8,7 @@ const PREMIUM_TIME_SLOT_ID_FLAG = "-PREMIUM";
const PREMIUM_FEE_PART_TYPE = "EARLY BIRD";
-// TODO; This will need updating with real PricingByDay part type
-const PRICING_BY_DAY_PART_TYPE = "DISC CASHSAVE20";
+const PRICING_BY_DAY_PART_TYPE = "PREMAPPT GEN";
const RouteCodeFlags = {
ALL_DAY_DROP_OFF: "ALL DAY DROP OFF",
diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js
index baa219aa1..e7cb38953 100644
--- a/src/constants/store-actions.js
+++ b/src/constants/store-actions.js
@@ -27,6 +27,7 @@ const storeActions = {
GET_RAIN_DEFENSE: "getRainDefense",
GET_SUPPORTING_ITEMS: "getSupportingItems",
GET_CAPABILITY_QUESTIONS: "getCapabilityQuestions",
+ SUBMIT_DONATION_PART_TO_SV2: "submitDonationPartToSV2",
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart",
diff --git a/src/digital-components/date-picker/mixins/constants.js b/src/digital-components/date-picker/mixins/constants.js
index b9092b5eb..52b973d3f 100644
--- a/src/digital-components/date-picker/mixins/constants.js
+++ b/src/digital-components/date-picker/mixins/constants.js
@@ -26,43 +26,36 @@ const DAYS_OF_WEEK = [
{
label: "Sunday",
cssClass: "sunday",
- index: 0,
- isPricingByDayUpchargeDay: false,
+ isPricingByDayUpchargeDay: true,
},
{
label: "Monday",
cssClass: "monday",
- index: 1,
isPricingByDayUpchargeDay: true,
},
{
label: "Tuesday",
cssClass: "tuesday",
- index: 2,
isPricingByDayUpchargeDay: false,
},
{
label: "Wednesday",
cssClass: "wednesday",
- index: 3,
isPricingByDayUpchargeDay: false,
},
{
label: "Thursday",
cssClass: "thursday",
- index: 4,
isPricingByDayUpchargeDay: false,
},
{
label: "Friday",
cssClass: "friday",
- index: 5,
isPricingByDayUpchargeDay: true,
},
{
label: "Saturday",
cssClass: "saturday",
- index: 6,
isPricingByDayUpchargeDay: true,
},
];
diff --git a/src/experiment-components/date-picker-for-pricing-by-day.vue b/src/experiment-components/date-picker-for-pricing-by-day.vue
index 5bdcec336..cad976cf7 100644
--- a/src/experiment-components/date-picker-for-pricing-by-day.vue
+++ b/src/experiment-components/date-picker-for-pricing-by-day.vue
@@ -1,5 +1,7 @@
-
@@ -108,7 +108,7 @@ export default {
DonationBodySubText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
},
- DonationAddButtonWidget() {
+ DonationAddButtonCopy() {
return this.getCmsContent("DonationAddButtonWidget", "Text");
},
DonationThanksAlertWidget() {
@@ -126,6 +126,7 @@ export default {
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
+ this.$emit("DonationAddedEvent");
},
},
},
diff --git a/src/fmg-components/cart/cart.vue b/src/fmg-components/cart/cart.vue
index f569c11d4..1cc1bb484 100644
--- a/src/fmg-components/cart/cart.vue
+++ b/src/fmg-components/cart/cart.vue
@@ -403,7 +403,9 @@ export default {
}
return [
...(this.lineItems.glassParts ?? []),
- ...(this.lineItems.supportingItems ?? []),
+ ...(this.lineItems.supportingItems.filter(
+ (item) => item.partType !== partTypeStrings.DONATION
+ ) ?? []),
...(this.availableVaps ?? []),
];
},
@@ -466,6 +468,10 @@ export default {
});
}
+ if (this.donationCartItem) {
+ cartItems.push(this.donationCartItem);
+ }
+
return cartItems;
},
},
@@ -1064,6 +1070,46 @@ export default {
return promoCartItems;
},
+ showDonationCartItem() {
+ const donationLineItem = this.lineItems.supportingItems.find(
+ (item) => item.partType === partTypeStrings.DONATION
+ );
+ if (donationLineItem?.sellingPrice !== 0) {
+ return true;
+ } else {
+ return false;
+ }
+ },
+ donationCartItemName() {
+ return this.getCmsContent("DonationCartTextWidget", "Text");
+ },
+ donationCartItem() {
+ if (!this.lineItems || !this.lineItems.supportingItems) {
+ return null;
+ }
+
+ const donationLineItem = this.lineItems.supportingItems.find(
+ (item) => item.partType === partTypeStrings.DONATION
+ );
+
+ if (donationLineItem?.sellingPrice > 0) {
+ return {
+ name: this.donationCartItemName,
+ category: cartItemCategories.SUPPORTING_ITEMS,
+ cartItemType: cartItemTypes.DONATION,
+ isDisplayed: true,
+ isRemovable: false,
+ subTotal: donationLineItem.sellingPrice,
+ salesTax: 0,
+ lineItems: [],
+ isCoveredByInsurance: cartItemTypesCoveredByInsurance.includes(
+ cartItemTypes.DONATION
+ ),
+ };
+ }
+
+ return null;
+ },
removeLinkText() {
return this.getCmsContent("RemoveCartItemTextWidget", "Text");
},
diff --git a/src/fmg-components/funnel-header/menu-modal/menu-modal.vue b/src/fmg-components/funnel-header/menu-modal/menu-modal.vue
index 825e5e0fb..4dc07f3a5 100644
--- a/src/fmg-components/funnel-header/menu-modal/menu-modal.vue
+++ b/src/fmg-components/funnel-header/menu-modal/menu-modal.vue
@@ -57,6 +57,10 @@
alt="Your privacy choices" />
+
+ :isMSRFeeApplicable="isMSRFeeApplicable"
+ :donationAmount="donationAmount"
+ @update-donation-amount="updateDonationAmount" />
+ :showDonationError="showDonationError"
+ @DonationAddedEvent="addDonationToOrder" />
@@ -522,49 +525,29 @@ export default {
forwardButtonAction() {
window.location.assign(location.protocol + "//" + location.host);
},
- async addDonation(amount, plateTemp) {
- // try to call API to add donation to order
-
- // TEMP DUMMY CALL TO SIMULATE SUCCESS OR ERROR FROM API CALL
- // TO BE REPLACED BY REAL CALL LATER
- const donationResponse = await this.dispatchStoreActionWithLogging(
- storeActions.LOOKUP_VIN_BY_PLATE,
- { licensePlate: plateTemp, licenseState: "OH" },
- "test",
+ async addDonationToOrder() {
+ const donationResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.SUBMIT_DONATION_PART_TO_SV2,
+ {
+ donationAmount: parseInt(this.donationAmount),
+ submittedOrder: this.submittedOrder,
+ },
false
- ).catch((error) => {
- return error;
- });
+ );
- if (donationResponse?.status == "200") {
+ if (donationResponse?.wasSuccessful) {
this.showDonationSuccess = true;
this.showDonationError = false;
- const donationResponse = await baseMixin.methods.dispatchStoreAction(
+ await baseMixin.methods.dispatchStoreAction(
storeActions.ADD_DONATION_TO_SUBMITTED_ORDER,
- amount,
+ parseInt(this.donationAmount),
false
);
+ this.lineItems = deepClone(this.getSubmittedOrder().lineItems);
} else {
this.showDonationError = true;
this.showDonationSuccess = false;
- const donationResponse = await baseMixin.methods.dispatchStoreAction(
- storeActions.ADD_DONATION_TO_SUBMITTED_ORDER,
- 0,
- false
- );
- }
- },
- },
- watch: {
- donationAmount(newValue) {
- newValue = parseInt(newValue);
-
- // this.addDonation(newValue);
- // TODO - when backend code is final, then remove the below and restore the line above
- if (newValue === 5) {
- this.addDonation(newValue, "FAILING_PLATE"); // throws error
- } else {
- this.addDonation(newValue, "2fast"); // should give successful response
+ this.donationAmount = 0;
}
},
},
diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js
index a87d6b0a6..49b90cfb3 100644
--- a/src/layouts/quote/quote.spec.js
+++ b/src/layouts/quote/quote.spec.js
@@ -836,6 +836,7 @@ describe("quote.vue", () => {
nextFunction
);
await nextTick();
+ await nextTick();
expect(nextFunction).toHaveBeenCalled();
expect(baseMixin.methods.isFormValid).toHaveBeenCalled();
expect(baseMixin.methods.isFormValid()).toBe(true);
diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue
index c062069af..fd21971b0 100644
--- a/src/layouts/quote/quote.vue
+++ b/src/layouts/quote/quote.vue
@@ -134,7 +134,7 @@ import { storeActions } from "@/constants/store-actions";
import { deepClone } from "@/helpers/object-helper";
import store from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
-import { experimentSettings } from "@/constants/experiments";
+import { experimentSettings, experimentUniverses } from "@/constants/experiments";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import { Form, defineRule } from "vee-validate";
@@ -154,12 +154,18 @@ import {
findLineItemsWithPartType,
} from "@/helpers/service-package-helper";
import { nextTick } from "vue";
-import { packageNames } from "@/constants/package-names";
+import { packageNames, externalParamPackageLabels } from "@/constants/package-names";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { containsRecalParts } from "@/helpers/recal-helper";
import { externalParameterStatus } from "@/constants/external-parameters";
import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
import { getBoolFromString } from "@/helpers/boolean-helper";
+import {
+ getSessionKeyValue,
+ getUserIdValue,
+ getDeviceIdValue,
+} from "@/helpers/heritage-integration/cookie-helper";
+import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@@ -189,6 +195,31 @@ export default {
"quote"
);
+ const shouldExposeSkipQuote = store.getters.isRecalibrationOnOrder;
+ const skipQuoteExperiment = store.getters.applicationUser.experiments.find(
+ (e) => e.universeName === experimentUniverses.IGQ_SkipQuote
+ );
+ const hasExposedSkipQuote = skipQuoteExperiment?.isExposed;
+
+ const shouldSendExposeRequest =
+ shouldExposeSkipQuote && !hasExposedSkipQuote && skipQuoteExperiment;
+
+ if (shouldSendExposeRequest) {
+ // Don't await -- response is not needed so can fire fully asynchronously.
+ baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
+ {
+ userId: getUserIdValue(),
+ deviceId: getDeviceIdValue(),
+ sessionKey: getSessionKeyValue(),
+ pageName: to.query.fmgPage,
+ experiment: skipQuoteExperiment,
+ },
+ "quote",
+ false
+ );
+ }
+
const promiseResultMap = [
{
resultKey: "cmsContent",
@@ -213,6 +244,12 @@ export default {
const isEmailInStoreOnPageLoad = emailFromStore?.length > 0;
const showSaveProgressPopup = isEmailInStoreOnPageLoad ? false : true;
const showSaveProgressModal = isEmailInStoreOnPageLoad ? false : true;
+ const shouldSkipToInsurance =
+ getBoolFromString(
+ experimentMixin.methods.getSettingValue(experimentSettings.SKIP_TO_INSURANCE)
+ ) &&
+ shouldExposeSkipQuote &&
+ !hasExposedSkipQuote;
const lineItems = deepClone(store.getters.order.lineItems);
lineItems.vaps = lineItems.vaps ?? [];
@@ -302,6 +339,7 @@ export default {
vm.availableLineItems = pricingResults;
vm.holdParentAccountNumber = holdParentAccountNumber;
vm.holdBillToAccountNumber = holdBillToAccountNumber;
+ vm.skipToInsurance = shouldSkipToInsurance;
if (revalidatePromoResponse) {
const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse(
@@ -434,21 +472,16 @@ export default {
}
}
- // did user intentionally select insurance?
- vm.isInsuranceSelected = true;
- vm.servicePackage = store.getters.externalParameterQuote.servicePackage;
- await nextTick();
+ const insuranceSelection = true;
+ const servicePackage = store.getters.externalParameterQuote.servicePackage;
if (
- vm.servicePackage != null &&
+ servicePackage != null &&
isExternalParameter === externalParameterStatus.ACTIVE
) {
- const isValid = await baseMixin.methods.isFormValid(vm.$refs.theForm);
- if (isValid) {
- baseMixin.methods.ResetExternalParamsAndHideModal();
- vm.forwardButtonAction();
- }
+ await vm.skip(insuranceSelection, servicePackage);
}
+
baseMixin.methods.ResetExternalParamsAndHideModal();
} else {
if (externalThreshold) thresholdToUse = externalThreshold;
@@ -467,6 +500,11 @@ export default {
baseMixin.methods.ResetExternalParamsAndHideModal();
}
}
+
+ if (vm.skipToInsurance && !vm.showSaveProgressPopup) {
+ // skip ahead only if no email popup.
+ await vm.skip(true, externalParamPackageLabels.GLASS_ONLY);
+ }
});
},
data() {
@@ -482,6 +520,7 @@ export default {
showSaveProgressModal: null,
isSaveProgressComplete: null,
packageNumber: null,
+ skipToInsurance: null,
};
},
mounted() {
@@ -584,8 +623,13 @@ export default {
updateSaveProgressAsSaved() {
this.showSaveProgressModal = false;
},
- closeSaveProgressPopup() {
+ async closeSaveProgressPopup() {
this.showSaveProgressPopup = false;
+
+ if (this.skipToInsurance) {
+ // skip ahead now if in IGQ skip experiment
+ await this.skip(true, externalParamPackageLabels.GLASS_ONLY);
+ }
},
forwardButtonAction() {
this.dispatchStoreAction(
@@ -729,6 +773,20 @@ export default {
this.pushEventToGA("quote", this.GaActions.SERVICE_PACKAGE_PRICE, eventLabel, true);
});
},
+ async skip(insuranceSelection, packageSelection) {
+ this.isInsuranceSelected = insuranceSelection;
+ this.servicePackage = packageSelection;
+
+ // Allow selections to filter through sub-logic
+ await nextTick();
+ await nextTick();
+
+ const isValid = await baseMixin.methods.isFormValid(this.$refs.theForm);
+ if (isValid) {
+ showFmgLoadingModal(true);
+ await this.forwardButtonAction();
+ }
+ },
},
watch: {
lineItemsCloneForWatcher: {
diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue
index ab4772107..68f031768 100644
--- a/src/layouts/quote/service-package-question/service-package-question.vue
+++ b/src/layouts/quote/service-package-question/service-package-question.vue
@@ -102,6 +102,11 @@ export default {
this.$emit("servicePackageDiscountSelected", supportingItems);
},
+ servicePackage(newValue) {
+ if (newValue !== null) {
+ this.selectDefaultPackage();
+ }
+ },
},
computed: {
nullSafeAvailableLineItems() {
diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue
index 90bd3d727..fcd20ace1 100644
--- a/src/layouts/schedule/schedule.vue
+++ b/src/layouts/schedule/schedule.vue
@@ -54,7 +54,7 @@
:estimatedServiceMinutesMaximum="
selectableDatesData.estimatedServiceMinutesMaximum
"
- @waitListRequested-emitted="handleWaitListRequested"
+ @waitListRequested="handleWaitListRequested"
@time-slot-modal-closed="timeSlotModalClosed"
validationRules="time-slot-selection-required"
@TimeSlotSelected="forwardButtonAction" />
@@ -250,9 +250,15 @@ export default {
isRecalibrationOnOrder && shouldHideRecalibration
? getItemsWithoutRecalParts(lineItems.glassParts)
: (lineItems.glassParts ?? []);
+ const supportingItemsFromStore = lineItems?.supportingItems;
const supportingItemsWithoutFees = baseMixin.methods.filterOutCertainPartTypesOrNumbers(
lineItems.supportingItems,
- { partNumbersToRemove: [partNumberStrings.RECYCLE_FEE] }
+ {
+ partNumbersToRemove: [
+ partNumberStrings.RECYCLE_FEE,
+ partNumberStrings.PRICING_BY_DAY_UPCHARGE,
+ ],
+ }
);
const lineItemsToBePriced = {
glassParts: glassParts,
@@ -266,8 +272,8 @@ export default {
// Check to see if includePricingByDayUpcharge should already be set (based on order lineItems)
let includePricingByDayUpcharge = false;
- if (supportingItemsWithoutFees) {
- const pricingByDayUpchargeFeeIndex = supportingItemsWithoutFees.findIndex(
+ if (supportingItemsFromStore) {
+ const pricingByDayUpchargeFeeIndex = supportingItemsFromStore?.findIndex(
(item) => item.partType == PRICING_BY_DAY_PART_TYPE
);
if (pricingByDayUpchargeFeeIndex > -1) {
@@ -300,7 +306,9 @@ export default {
});
// Get pricingByDayUpcharge needed for Pricing By Day
- const pricingByDayUpchargePartPromise = getPricingByDayPartWithPrice();
+ const pricingByDayUpchargePartPromise = showPricingByDay
+ ? getPricingByDayPartWithPrice()
+ : null;
const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_PREMIUM_FEE,
@@ -348,10 +356,12 @@ export default {
const resultMap = await settleAllPromises(promiseResultMap);
- const pricingByDayUpcharge = await baseMixin.methods.getTotalLineItemPrice(
- resultMap.pricingByDayUpchargePart,
- false
- );
+ const pricingByDayUpcharge = showPricingByDay
+ ? await baseMixin.methods.getTotalLineItemPrice(
+ resultMap.pricingByDayUpchargePart,
+ false
+ )
+ : null;
const datePickerInitialData = resultMap.datePickerInitialData;
datePickerInitialData.pricingByDayBasePrice = pricingByDayBasePrice;
@@ -620,24 +630,26 @@ export default {
const supportingItems = this.getSupportingItems();
// if we have a pricing by day upcharge, then save/update supporting items with it
- const pricingByDayUpchargeFeeIndex = supportingItems?.findIndex(
- (item) => item.partType == PRICING_BY_DAY_PART_TYPE
- );
+ if (this.showPricingByDay) {
+ const pricingByDayUpchargeFeeIndex = supportingItems?.findIndex(
+ (item) => item.partType == PRICING_BY_DAY_PART_TYPE
+ );
- if (this.includePricingByDayUpcharge) {
- if (pricingByDayUpchargeFeeIndex > -1) {
- supportingItems[pricingByDayUpchargeFeeIndex].laborAmount =
- this.pricingByDayUpchargeLineItem.laborAmount;
- supportingItems[pricingByDayUpchargeFeeIndex].sellingPrice =
- this.pricingByDayUpchargeLineItem.sellingPrice;
- supportingItems[pricingByDayUpchargeFeeIndex].kitPrice =
- this.pricingByDayUpchargeLineItem.kitPrice;
+ if (this.includePricingByDayUpcharge) {
+ if (pricingByDayUpchargeFeeIndex > -1) {
+ supportingItems[pricingByDayUpchargeFeeIndex].laborAmount =
+ this.pricingByDayUpchargeLineItem.laborAmount;
+ supportingItems[pricingByDayUpchargeFeeIndex].sellingPrice =
+ this.pricingByDayUpchargeLineItem.sellingPrice;
+ supportingItems[pricingByDayUpchargeFeeIndex].kitPrice =
+ this.pricingByDayUpchargeLineItem.kitPrice;
+ } else {
+ supportingItems.push(this.pricingByDayUpchargeLineItem);
+ }
} else {
- supportingItems.push(this.pricingByDayUpchargeLineItem);
- }
- } else {
- if (pricingByDayUpchargeFeeIndex >= 0) {
- supportingItems.splice(pricingByDayUpchargeFeeIndex, 1);
+ if (pricingByDayUpchargeFeeIndex >= 0) {
+ supportingItems.splice(pricingByDayUpchargeFeeIndex, 1);
+ }
}
}
diff --git a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue
index 82e11ea41..0cc9cba78 100644
--- a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue
+++ b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue
@@ -43,8 +43,8 @@
v-if="waitListRequested"
class="mt-5 rounded waitlist-success"
ref="waitlistSuccessMessage">
-
-
+
+
item.partNumber === partNumberStrings.RECYCLE_FEE
+ );
+
+ if (recycleFeePart) {
+ // Get the Recycle Fee Part Price
+ const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
+ {
+ availableLineItems: [recycleFeePart],
+ serviceZipCode: serviceZipCode,
+ serviceZipCodeCtu: zipCodeData.zipCodeCtu,
+ },
+ pageNameToLog,
+ false
+ );
+
+ return pricingResults[0];
+ }
+}
+
export async function getServiceabilityDetails(serviceZipCode, lineItems, pageNameToLog) {
// Get the Mobile Fee Part
return await baseMixin.methods.dispatchStoreActionWithLogging(
diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.spec.js b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.spec.js
index f38262005..3e2873fa1 100644
--- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.spec.js
+++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.spec.js
@@ -106,6 +106,23 @@ const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
return Promise.resolve(mobileFeePart);
};
+const mockGetPricedRecycleFeePart = (mockServiceZipCode) => {
+ let recycleFeePart = {};
+
+ if (mockServiceZipCode === "43235") {
+ recycleFeePart = {
+ partNumber: "RECYCLE FEE",
+ description: "RECYCLE FEE",
+ partType: "FEE",
+ laborAmount: 39.99,
+ sellingPrice: 0,
+ kitPrice: 0,
+ };
+ }
+
+ return Promise.resolve(recycleFeePart);
+};
+
const mockGetServiceabilityDetails = (mockServiceZipCode) => {
const serviceabilityDetails = {
isGlassServiceableInshop: true,
@@ -123,6 +140,9 @@ jest.mock(
getPricedMobileFeePart: jest.fn((mockServiceZipCode) => {
return mockGetPricedMobileFeePart(mockServiceZipCode);
}),
+ getPricedRecycleFeePart: jest.fn((mockServiceZipCode) => {
+ return mockGetPricedRecycleFeePart(mockServiceZipCode);
+ }),
getServiceabilityDetails: jest.fn((mockServiceZipCode) => {
return mockGetServiceabilityDetails(mockServiceZipCode);
}),
diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue
index 4672cca7f..0d7a82248 100644
--- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue
+++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue
@@ -82,6 +82,7 @@ import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location
import { deepClone } from "@/helpers/object-helper";
import {
getPricedMobileFeePart,
+ getPricedRecycleFeePart,
getServiceabilityDetails,
getBillToAccountNumber,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
@@ -267,6 +268,12 @@ export default {
"service-location"
);
+ // retrieve recycle fee part
+ const recycleFeePart = await getPricedRecycleFeePart(
+ serviceZipCode,
+ "service-location"
+ );
+
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
@@ -280,6 +287,7 @@ export default {
// update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart);
+ this.$emit("updated-recycle-fee-part", recycleFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
this.$emit("updated-mobile-ctu", zipCodeData.zipCodeCtu);
diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue
index ddb765da3..56c76bb2d 100644
--- a/src/layouts/service-location/service-location.vue
+++ b/src/layouts/service-location/service-location.vue
@@ -17,6 +17,7 @@
ref="serviceZipCodeQuestion"
:mobileFeePart="mobileFeePart"
@updated-mobile-fee-part="setMobileFeePart"
+ @updated-recycle-fee-part="setRecycleFeePart"
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-bill-to-account-number="setBillToAccountNumber"
@@ -90,6 +91,7 @@
:mobileFeePart="mobileFeePart"
:mobileFeeApplies="mobileFeeApplies"
@updated-mobile-fee-part="setMobileFeePart"
+ @updated-recycle-fee-part="setRecycleFeePart"
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-mobile-ctu="setCtuForMobile"
@@ -195,6 +197,7 @@ export default {
selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(),
mobileFeePart: null,
+ recycleFeePart: null,
zipContainsMilitaryBase: false,
zipCodeCtu: null,
billToAccountNumber: null,
@@ -480,6 +483,9 @@ export default {
setMobileFeePart(mobileFeePart) {
this.mobileFeePart = mobileFeePart;
},
+ setRecycleFeePart(recycleFeePart) {
+ this.recycleFeePart = recycleFeePart;
+ },
//creating async function as the computed property can not directly handle asynchronous operations or promises.
async setMobileLocationQuestions(newValue) {
var newZipCode = newValue.addressQuestions.zipCode;
@@ -592,6 +598,7 @@ export default {
},
updateAndSaveSupportingItems() {
let supportingItems = store.getters.lineItems.supportingItems;
+ let shouldSaveSupportingItems = false;
supportingItems =
!supportingItems && this.isMobileStaticRecalibrationApplicable
@@ -603,6 +610,21 @@ export default {
return;
}
+ // update recyle fee price
+ if (this.recycleFeePart) {
+ const recycleFeeIndex = supportingItems.findIndex(
+ (item) => item.partNumber == partNumberStrings.RECYCLE_FEE
+ );
+ if (recycleFeeIndex >= 0) {
+ supportingItems[recycleFeeIndex].laborAmount = this.recycleFeePart.laborAmount;
+ supportingItems[recycleFeeIndex].sellingPrice =
+ this.recycleFeePart.sellingPrice;
+ supportingItems[recycleFeeIndex].kitPrice = this.recycleFeePart.kitPrice;
+
+ shouldSaveSupportingItems = true;
+ }
+ }
+
// if we have a mobile fee, then save/update supporting items
if (this.selectedAppointmentType == "Mobile") {
const mobileFeeIndex = supportingItems.findIndex(
@@ -617,11 +639,7 @@ export default {
if (this.mobileFeePart !== null) supportingItems.push(this.mobileFeePart);
}
- this.dispatchStoreAction(
- this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
- supportingItems,
- false
- );
+ shouldSaveSupportingItems = true;
} else {
// if it's not a mobile, then make sure we remove any that may have been added
const removeMobileFeeIndex = supportingItems?.findIndex(
@@ -630,13 +648,18 @@ export default {
if (removeMobileFeeIndex >= 0) {
supportingItems.splice(removeMobileFeeIndex, 1);
- this.dispatchStoreAction(
- this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
- supportingItems,
- false
- );
+ shouldSaveSupportingItems = true;
}
}
+
+ // Use shouldSaveSupportingItems flag to determine if we need to save supporting items. Prevents unnecessary/multiple saves
+ if (shouldSaveSupportingItems) {
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
+ supportingItems,
+ false
+ );
+ }
},
async forwardButtonAction() {
this.navigatingForward = true;
diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.spec.js b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.spec.js
index ed8a811ed..b1231991b 100644
--- a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.spec.js
+++ b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.spec.js
@@ -32,6 +32,23 @@ const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
return Promise.resolve(mobileFeePart);
};
+const mockGetPricedRecycleFeePart = (mockServiceZipCode) => {
+ let recycleFeePart = {};
+
+ if (mockServiceZipCode === "43235") {
+ recycleFeePart = {
+ partNumber: "RECYCLE FEE",
+ description: "RECYCLE FEE",
+ partType: "FEE",
+ laborAmount: 39.99,
+ sellingPrice: 0,
+ kitPrice: 0,
+ };
+ }
+
+ return Promise.resolve(recycleFeePart);
+};
+
const mockGetServiceabilityDetails = (mockServiceZipCode) => {
const serviceabilityDetails = {
isGlassServiceableInshop: true,
@@ -49,6 +66,9 @@ jest.mock(
getPricedMobileFeePart: jest.fn((mockServiceZipCode) => {
return mockGetPricedMobileFeePart(mockServiceZipCode);
}),
+ getPricedRecycleFeePart: jest.fn((mockServiceZipCode) => {
+ return mockGetPricedRecycleFeePart(mockServiceZipCode);
+ }),
getServiceabilityDetails: jest.fn((mockServiceZipCode) => {
return mockGetServiceabilityDetails(mockServiceZipCode);
}),
diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue
index 24d063e5e..a2ab4134e 100644
--- a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue
+++ b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue
@@ -45,6 +45,7 @@ import alert from "@/ux-components/alert/alert";
import {
getPricedMobileFeePart,
+ getPricedRecycleFeePart,
getServiceabilityDetails,
getBillToAccountNumber,
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
@@ -164,6 +165,12 @@ export default {
"service-location"
);
+ // retrieve recycle fee part
+ const recycleFeePart = await getPricedRecycleFeePart(
+ serviceZipCode,
+ "service-location"
+ );
+
// retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
@@ -177,6 +184,7 @@ export default {
// update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart);
+ this.$emit("updated-recycle-fee-part", recycleFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
this.$emit("updated-bill-to-account-number", billToAccountNumber);
diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js
index 6fd6075ab..151d17dd9 100644
--- a/src/mixins/base-mixin.js
+++ b/src/mixins/base-mixin.js
@@ -230,6 +230,12 @@ export default {
return "widget-name-" + this.cmsWidgetName;
},
},
+
+ mounted() {
+ if (!store.getters.externalParameterState?.isExternalParameter) {
+ showFmgLoadingModal(false);
+ }
+ },
};
function encodeUriData(payload) {
diff --git a/src/router/index.js b/src/router/index.js
index 2230a652d..345790734 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -433,9 +433,6 @@ router.afterEach(async (to, from) => {
await saveSession({ pageNameToLog: to.query.fmgPage });
}
}
- if (!store.getters.externalParameterState?.isExternalParameter) {
- showFmgLoadingModal(false);
- }
// Push page view to GA
analyticsMixin.methods.pushPageViewToGA();
diff --git a/src/store/index.js b/src/store/index.js
index da849c127..032660c6f 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -720,7 +720,8 @@ export const mutations = {
// if we're loading a session and we do not have a provider number yet, then set isInsurance to null so that
// a service package is not selected by default on the quote page.
if (
- !sessionInformation.order.payment.isInsurance &&
+ (sessionInformation.order.payment.isInsurance === null ||
+ sessionInformation.order.payment.isInsurance === undefined) &&
!sessionInformation.order.serviceLocation.provider?.providerNumber
) {
state.order.payment.isInsurance = null;
@@ -1891,6 +1892,97 @@ export const actions = {
pageNameToLog: pageNameToLog,
});
},
+ submitDonationPartToSV2(
+ context,
+ { payload: { donationAmount, submittedOrder }, pageNameToLog }
+ ) {
+ donationAmount = parseInt(donationAmount);
+ /* Request contract
+ {
+ "donationAmount": 5,
+ "savedSessionId": "96b1a16e-410a-4d08-8421-fe36e82b2275",
+ "paymentInfo": {
+ "parentAccountNumber": 167132,
+ "billToAccountNumber": 87291,
+ "providerNumber": "005819",
+ "carId": "CR00069266",
+ "make": "Acura",
+ "model": "MDX",
+ "year": 2020,
+ "eon": "S1820397",
+ "zipCode": "43015",
+ "state": "OH",
+ "referralNumber": "1821895",
+ "referralDate": "2025-03-20T16:27:38.93",
+ "referralSequenceNumber": "1821895",
+ "serviceZipCode": "43015",
+ "isReplacement": true,
+ "deductible": 0,
+ "coverageStatus": null,
+ "coverageSubStatus": null,
+ "serverData": "ZtCNOF1tCLQBAx2NRKHE/n/fqD8zUxRfYtDuTu7uUiPpeg7i6TmwXwggssX5KsXVQbeft+PN+vXYfBFJytLVYgbafNDR0JH3izVTIOh7KGTeB6YaPuCJmodF4SfgVVDHD9LEmEH9wNzM9j3pFjgfXehOFH11SH9dNwTkXIbsGzAeu64DRyJsDdpamM/BPOxlJj60S/R8vnUM8mjyM7B9a63NskFI6BCVIh5mxDE4cSMUgb4zFtwvDgTPxJSwXqKShtoY6ryB8n5UPNkG/Z5tjVuJ7/8RV7WrURM7PN7xLfp6pVEeipWyeFnVEK33G9JOuB6yH75jPO6vMCYRNi0p0pg6laasFDy3hmfuy2W/N0qtcJLVa5N0bUMFFuPQusyEeqsEZ/zx1Srlvsk7i0DF8pTyBcm3JTF5BOWm60wg3ntEOqfRuQqzFc5updILwLAIp39CwJG/fs5Mj3HCDcUKwYlK70fAARtJwFu7wI0vqW5bUSUvJkvm1JRPlyWJ8JNY4Oq1apYqhZS9bps5ofgiOYJbBhPEboEnVukoTkQiYjgbMtBwVGKGL2nNEd77osoEg1KXhEQS+0KD3PoUu/bZkGub6FNGE6iKeFPt9gDSs+S21s22rVSaZkgaoC6SkufPpN6aV78Jx69nMIDd4go4pj2IJX4kS1pD1NEOSWjN4bgVKAtV6/bxBvhNJwnqUE+mE8eAmULsezub4LMhPqA58y/ZlO44jt/qkAw9nuVDRPBve+TdJTotID5dIqJJd6/vkDoHTVaSrbkCBbdG6d9By8AEbKuVrQS/+VurUnYEd2TGahPbOyRdNlud8RB4+4af0ZRFmiKkr6ppa5IbHbTIZ2qtyeOSbIOkv8O7xK/GL6e6Iaaz0wDHaQZC4Ea8FczjpFOlQgixfYcYJBb/W2db+uRvZ3lftVyJ/d082CGbYplOa+aTxymVcQSjBRbXzs6GZ2ytDqRYcRe94F9x/XSVj7g+mih7VS+YH0eOEp0ViTuWP8B1dVnOV5yTkdMbr8n9Q82G4v5brtQdiMDMY3HS6mxPb1IiKiqFX6Jgju8sIMxkJFZQ3iIkAJuOqUYkG1ZW7s4uI6hT46bRqzYaQA18CFu56qaMblOBFuxlJIsYsnMePh2xAH89oay5Kc88xwT2T/4+dzYQ0CqhQ4MTBB7nXelav2rMCjaLQ/V/a5r1TnLr2n0GbvODQeNl97qzi5lDYKlnuF8nOfOqZNOsPSsrsk3UZJJYZwNxDZaweHIeeg5P2ZW5agswPx/U5DkU1MzE1VAQ+DQAenaE2SLo4dSPBHMmt2gLpgu0LaCuzTR2IL/EM+Tpdn/Pcv1E7lPJIycIIwO7QGJcVkwCx8CJ/WCo6xDMq+9zjSVbqAaL5MzySZCeFCMyZxvEk7qXig6rflB3K3Jay4qSFjBxBXhOEuT6zKXyhR41IXRtZRmtPkscypFEUXvR5nJV7AQUIbljGEcikOLmSiCEnl8dI2Ny9xCHLOro8UXqVUy8dHGmTRGTE/mS1JNZ3y4XaZSyHcABVWbZcR3KIXVxd6rtUA/pBvWpbq8nH3WWlJGqkoxUVEridtXqcBuzTKg3/i6vLs0/pHGwdM38XizKcIxykvJrur3vEQ=="
+ }
+ }
+ */
+
+ /* Uncomment and use when endpoint integration is finalized
+ return globalMethods.callHttpClient({
+ method: endpoints.AddDonationPart.method,
+ endpoint: endpoints.AddDonationPart.url,
+ payload: {
+ donationAmount: donationAmount,
+ savedSessionId: "96b1a16e-410a-4d08-8421-fe36e82b2275",
+ paymentInfo: {
+ parentAccountNumber: submittedSessionData.parentAccountNumber,
+ billToAccountNumber: submittedSessionData.billToAccountNumber,
+ providerNumber: submittedSessionData.providerNumber,
+ carId: "CR00069266",
+ make: "Acura",
+ model: "MDX",
+ year: 2020,
+ eon: "S1820397",
+ zipCode: "43015",
+ state: "OH",
+ referralNumber: "1821895",
+ referralDate: "2025-03-20T16:27:38.93",
+ referralSequenceNumber: "1821895",
+ serviceZipCode: "43015",
+ isReplacement: true,
+ deductible: 0,
+ coverageStatus: null,
+ coverageSubStatus: null,
+ serverData: "ZtCNOF1tCLQBAx2NRKHE/n/fqD8zUxRfYtDuTu7uUiPpeg7i6TmwXwggssX5KsXVQbeft+PN+vXYfBFJytLVYgbafNDR0JH3izVTIOh7KGTeB6YaPuCJmodF4SfgVVDHD9LEmEH9wNzM9j3pFjgfXehOFH11SH9dNwTkXIbsGzAeu64DRyJsDdpamM/BPOxlJj60S/R8vnUM8mjyM7B9a63NskFI6BCVIh5mxDE4cSMUgb4zFtwvDgTPxJSwXqKShtoY6ryB8n5UPNkG/Z5tjVuJ7/8RV7WrURM7PN7xLfp6pVEeipWyeFnVEK33G9JOuB6yH75jPO6vMCYRNi0p0pg6laasFDy3hmfuy2W/N0qtcJLVa5N0bUMFFuPQusyEeqsEZ/zx1Srlvsk7i0DF8pTyBcm3JTF5BOWm60wg3ntEOqfRuQqzFc5updILwLAIp39CwJG/fs5Mj3HCDcUKwYlK70fAARtJwFu7wI0vqW5bUSUvJkvm1JRPlyWJ8JNY4Oq1apYqhZS9bps5ofgiOYJbBhPEboEnVukoTkQiYjgbMtBwVGKGL2nNEd77osoEg1KXhEQS+0KD3PoUu/bZkGub6FNGE6iKeFPt9gDSs+S21s22rVSaZkgaoC6SkufPpN6aV78Jx69nMIDd4go4pj2IJX4kS1pD1NEOSWjN4bgVKAtV6/bxBvhNJwnqUE+mE8eAmULsezub4LMhPqA58y/ZlO44jt/qkAw9nuVDRPBve+TdJTotID5dIqJJd6/vkDoHTVaSrbkCBbdG6d9By8AEbKuVrQS/+VurUnYEd2TGahPbOyRdNlud8RB4+4af0ZRFmiKkr6ppa5IbHbTIZ2qtyeOSbIOkv8O7xK/GL6e6Iaaz0wDHaQZC4Ea8FczjpFOlQgixfYcYJBb/W2db+uRvZ3lftVyJ/d082CGbYplOa+aTxymVcQSjBRbXzs6GZ2ytDqRYcRe94F9x/XSVj7g+mih7VS+YH0eOEp0ViTuWP8B1dVnOV5yTkdMbr8n9Q82G4v5brtQdiMDMY3HS6mxPb1IiKiqFX6Jgju8sIMxkJFZQ3iIkAJuOqUYkG1ZW7s4uI6hT46bRqzYaQA18CFu56qaMblOBFuxlJIsYsnMePh2xAH89oay5Kc88xwT2T/4+dzYQ0CqhQ4MTBB7nXelav2rMCjaLQ/V/a5r1TnLr2n0GbvODQeNl97qzi5lDYKlnuF8nOfOqZNOsPSsrsk3UZJJYZwNxDZaweHIeeg5P2ZW5agswPx/U5DkU1MzE1VAQ+DQAenaE2SLo4dSPBHMmt2gLpgu0LaCuzTR2IL/EM+Tpdn/Pcv1E7lPJIycIIwO7QGJcVkwCx8CJ/WCo6xDMq+9zjSVbqAaL5MzySZCeFCMyZxvEk7qXig6rflB3K3Jay4qSFjBxBXhOEuT6zKXyhR41IXRtZRmtPkscypFEUXvR5nJV7AQUIbljGEcikOLmSiCEnl8dI2Ny9xCHLOro8UXqVUy8dHGmTRGTE/mS1JNZ3y4XaZSyHcABVWbZcR3KIXVxd6rtUA/pBvWpbq8nH3WWlJGqkoxUVEridtXqcBuzTKg3/i6vLs0/pHGwdM38XizKcIxykvJrur3vEQ=="
+ }
+ },
+ logApiCall: true,
+ pageNameToLog: pageNameToLog,
+ });
+ */
+
+ /* Response contract
+ {
+ "wasSuccessful": true,
+ "wasLocked": true
+ }
+ */
+ let result;
+
+ switch (donationAmount) {
+ case 1:
+ result = { wasSuccessful: true, wasLocked: false };
+ break;
+ case 3:
+ result = { wasSuccessful: false, wasLocked: true };
+ break;
+ case 5:
+ result = { wasSuccessful: false, wasLocked: false };
+ break;
+ default:
+ result = { wasSuccessful: null, wasLocked: null }; // Default case if amount doesn't match
+ }
+
+ return result;
+ },
getPartFromCapabilityQuestionAnswer(context, { payload, pageNameToLog }) {
const glassLocation = payload;
@@ -3206,16 +3298,16 @@ export const actions = {
"running addDonationToSubmittedOrder()... adding donation to supportingItems in session storage "
);
submittedOrder.lineItems.supportingItems.push({
- partNumber: "DONATION",
- description: null,
- partType: "DONATION",
+ partNumber: `DONATION_${donationAmount}`,
+ description: `DONATION $${donationAmount}`,
+ partType: partTypeStrings.DONATION,
isInsurable: false,
isChildPart: false,
laborAmount: 0,
sellingPrice: donationAmount,
kitPrice: 0,
- salesTax: null,
- id: "donation-" + donationAmount,
+ salesTax: 0,
+ id: `donation-${donationAmount}`,
});
} else {
// filter out all donations