@nuskin/ns-checkout
Version:
Ecomm3 Checkout module
1,229 lines (1,084 loc) • 95.8 kB
JavaScript
'use strict';
import '../app.js';
import {BuyerCookieService,UserService, AccountManager, AuthenticationService} from '@nuskin/ns-account';
import {ConfigService, RunConfigService, StringService, storage, events, StickyHeaderStack, StickyHeader, StickyHeaderBoundary, util, SponsorStorageService, StoreFrontSponsorStorageService, PersonalOfferStorageService} from '@nuskin/ns-util';
import {CartService, ShopContextService, LocalStorageOrderService, AdrConstants, AdrService, PaymentType, PaymentService, TransBankPaymentAdapter, CurrencyCodeToNumber, PersonalOfferService, CheckoutModeService} from '@nuskin/ns-shop';
import {AdrUtil,CartUtil,PickupUtil,ExternalPaymentStatusEnum,ModalUtil,OrderUtil,PaymentUtil,SectionUtil,ShippingAddressUtil,StringUtil,ThreatMatrixUtil} from '@nuskin/ns-checkout-common';
import {STATE_CONFIG} from "@nuskin/ns-form-elements";
angular.module('checkout').controller('OrderSummaryCtrl', [
'$scope',
'$rootScope',
'$filter',
'$q',
'$http',
'$location',
'$timeout',
'nsAdrOptions',
'nsShipping',
'nsTimeDelivery',
'nsPayment',
'nsProduct',
'nsOrder',
'nsUtil',
'nsSection',
'nsExternalPayment',
'nsSections',
'nsDowntime',
'tdcService',
'nsADRType',
'nsAdrUtil',
'nsGuestSignup',
'googleMapsApi',
'NgMap',
function(
$scope,
$rootScope,
$filter,
$q,
$http,
$location,
$timeout,
nsAdrOptions,
nsShipping,
nsTimeDelivery,
nsPayment,
nsProduct,
nsOrder,
nsUtil,
nsSection,
nsExternalPayment,
nsSections,
nsDowntime,
tdcService,
nsADRType,
nsAdrUtil,
nsGuestSignup,
googleMapsApi,
NgMap) {
/**
* Request a check on the username (email).
*
* @param email
* @return {Promise}
* @private
*/
async function _duplicateUserCheck(email) {
let domain = RunConfigService.getRunConfig().baseUrl;
if (window.location.href.indexOf('localhost') !== -1) {
domain = "https://test.nuskin.com";
}
const DUPLICATE_USER_CHECK_URL = domain + "/account-util/api/v1/account/username-ck?login=";
let response = await(fetch(DUPLICATE_USER_CHECK_URL + email, {
method: "GET",
headers: {
'client_id': ConfigService.getMarketConfig().checkout.clientId,
'client_secret': ConfigService.getMarketConfig().checkout.clientSecret,
"Content-Type": "application/json"
},
credentials: 'omit'
}));
return await(response.json());
}
$scope.createAccount = false;
/**
* COM-1643
*
* Convert boolean to either a 1 (for true) or 0 (for false). This
* is beneficial for adding up boolean sums (e.g. password must meet 3 out of
* 4 criteria). If something besides a boolean is passed in just convert that
* to a zero or one with the default casting ruleset (truthy or falsy).
*/
function _boolToInt(bool) {
return bool ? 1 : 0;
}
/**
* COM-1643
*
* The state of the account create. This takes into account the password and username (email) and is applied
* towards error handling on account creation along with other fields in the form.
*
* Is the password invalid? Default is false.
* Is the password empty? Default is true.
* Do the passwords match? Default is true since by default password fields are empty (pristine).
* What was the last value of the user for account creation? Default is an empty string.
* Were the account creation fields validated? Default is false.
*
*/
const ACCOUNT_CREATION_STATES_DEFAULTS = {
isInvalidPassword: false,
emptyPassword: true,
emptyConfirmationPassword: true,
lastAttemptedUser: "",
passwordsDontMatch: false,
validated: false,
futureContactChecked: false,
createAccountTermsConditionsChecked: false,
contactByEmailChecked: false,
contactBySMSChecked: false
};
/**
* Invoke this function
*
* @param key
* @private
*/
function _resetAccountCreationState(key) {
$scope.accountCreationStates[key] = (key) ? ACCOUNT_CREATION_STATES_DEFAULTS[key] : key;
}
/**
* COM-1643
*
* Set the account creation states from the default map. This function is used to either init or reset to the
* account creation states.
*
* @private
*/
function _setAccountCreationStates() {
if (!$scope.config.checkout.enablePolymerForms) {
$scope.forms['preShippingForm'].showValidationError = false;
}
$scope.accountCreationStates = JSON.parse(JSON.stringify(ACCOUNT_CREATION_STATES_DEFAULTS));
}
/**
* COM-1643 invalidate the element
*
* @param element correlates to a form input element
* @param key correlates to an error key
* @private
*/
function _invalidate(element, key) {
if (key) {
element.$setValidity(key, false);
}
}
/**
* COM-1643 validate the element
*
* @param element correlates to a form input element
* @param key correlates to an error key
* @private
*/
function _validate(element, key) {
element.$setValidity(key, true);
}
/**
* COM-1643
*
* Validate passwords for new account's created in this order:
*
* Ensure the length is greater than 8
*
* AND
*
* Three out of the four conditions must be met:
*
* A special character
* A lowercase character
* An uppercase character
* A number
*
* @return {Boolean} true if password is valid else false
* @private
*/
function _validatePassword() {
let password = angular.element("#userPasswordField").val();
let confirmationPassword = angular.element("#confirmationUserPasswordField").val();
$scope.accountCreationStates.strength =
(_boolToInt(password && password.length >= 8)) &&
(_boolToInt(password && /[\@\#\$\%\^\&\*\(\)\_\+\!]/.test(password)) +
_boolToInt(password && /[a-z]/.test(password)) +
_boolToInt(password && /[A-Z]/.test(password)) +
_boolToInt(password && /[0-9]/.test(password)));
_setPasswordStates($scope.accountCreationStates.strength, password, confirmationPassword);
return _setPasswordFormValidity();
}
/**
* COM-1643
*
* Set the password state object.
*
* @param strength represents the strength of the password. Must exceed the criteria offset for a valid password.
* @param password
* @param confirmationPassword
* @private
*/
function _setPasswordStates(strength, password, confirmationPassword) {
const CRITERIA_STRENGTH_REQUIREMENT = 3;
$scope.accountCreationStates.emptyPassword = !password;
$scope.accountCreationStates.isInvalidPassword = !!password && (strength < CRITERIA_STRENGTH_REQUIREMENT);
$scope.accountCreationStates.emptyConfirmationPassword = !confirmationPassword;
// The passwords can only not match if the password and confirmation fields are populated (all or nothing):
if (!$scope.accountCreationStates.emptyPassword && !$scope.accountCreationStates.emptyConfirmationPassword) {
$scope.accountCreationStates.passwordsDontMatch = password !== confirmationPassword;
}
// It's ok to not have any text in either one of the password fields. If that's the case simply hide the
// password's don't match error.
else {
$scope.accountCreationStates.passwordsDontMatch = false;
}
}
/**
* COM-1643
*
* Set the form validity for the new account password. The order here is important so be careful when editing.
*
* @return {Boolean} true if valid password else false
* @private
*/
function _setPasswordFormValidity() {
let rv = false;
// If no password was provided remove the password requirements error message
// and just display the required message:
if ($scope.accountCreationStates.emptyPassword) {
_invalidate($scope.forms.preShippingForm.userPassword, "required");
// Technically the password isn't valid and isInvalidPassword should be true, but the required error above
// is shown instead and accounts for the invalidity.
$scope.accountCreationStates.isInvalidPassword = false;
$scope.accountCreationStates.passwordsDontMatch = false;
}
// Else-if the password is provided but invalid remove the required message
// and just display the password requirements error message:
else if ($scope.accountCreationStates.isInvalidPassword) {
_validate($scope.forms.preShippingForm.userPassword, "required");
$scope.accountCreationStates.isInvalidPassword = true;
$scope.accountCreationStates.passwordsDontMatch = false;
}
// If a valid password has been provided but no confirmation password is provided display an error.
else if ($scope.accountCreationStates.emptyConfirmationPassword) {
$scope.accountCreationStates.isInvalidPassword = false;
$scope.accountCreationStates.passwordsDontMatch = false;
_invalidate($scope.forms.preShippingForm.confirmationUserPassword, "required");
}
// If a valid password has been provided but the confirmation password doesn't equal the password display an error.
else if ($scope.accountCreationStates.passwordsDontMatch) {
$scope.accountCreationStates.isInvalidPassword = false;
$scope.accountCreationStates.passwordsDontMatch = true;
}
// Else a valid password and confirmation password has been provided.
else {
$scope.accountCreationStates.isInvalidPassword = false;
$scope.accountCreationStates.passwordsDontMatch = false;
_validate($scope.forms.preShippingForm.userPassword, "required");
rv = true;
}
// Make sure the DOM gets the updated state variables
apply();
return rv;
}
/**
* COM-2282 toggle create account terms and conditions (with privacy policy) checkbox
*/
$scope.toggleCreateAccountTermsConditionsChecked = () => {
if ($scope.createAccount) {
$scope.accountCreationStates.createAccountTermsConditionsChecked = !$scope.accountCreationStates.createAccountTermsConditionsChecked;
} else {
// Reset:
_resetAccountCreationState("createAccountTermsConditionsChecked");
}
};
/**
* COM-2282 toggle future contact checkbox
*/
$scope.toggleFutureContactChecked = () => {
if ($scope.createAccount) {
$scope.accountCreationStates.futureContactChecked = !$scope.accountCreationStates.futureContactChecked;
// If checking "contact me", set email and SMS checkboxes to checked by default
// If un-checking "contact me", set email and SMS checkboxes to unchecked
$scope.accountCreationStates.contactByEmailChecked = $scope.accountCreationStates.futureContactChecked;
$scope.accountCreationStates.contactBySMSChecked = $scope.accountCreationStates.futureContactChecked;
} else {
// Reset:
_resetAccountCreationState("futureContactChecked");
}
};
/**
* COM-2282 toggle contact-by-email checkbox
*/
$scope.toggleContactByEmailChecked = () => {
if ($scope.createAccount && $scope.accountCreationStates.futureContactChecked) {
$scope.accountCreationStates.contactByEmailChecked = !$scope.accountCreationStates.contactByEmailChecked;
} else {
// Reset:
_resetAccountCreationState("contactByEmailChecked");
}
};
/**
* COM-2282 toggle contact-by-sms checkbox
*/
$scope.toggleContactBySMSChecked = () => {
if ($scope.createAccount && $scope.accountCreationStates.futureContactChecked) {
$scope.accountCreationStates.contactBySMSChecked = !$scope.accountCreationStates.contactBySMSChecked;
} else {
// Reset:
_resetAccountCreationState("contactBySMSChecked");
}
};
/**
* COM-1643
*
* Toggle the create account fields. The Angular code in the orderSummary.html will update the fields based off of
* the value set on $scope.createAccount. Password is always required if visible whereas the email may or may not
* be required if visible (depends on if the create account checkbox is checked). If the order has an ADR item then
* the password and username must be visible and required.
*/
$scope.toggleCreateAccount = (createAcct) => {
$scope.createAccount = createAcct;
let shippingEmailLabel = angular.element("label[for=shippingEmail]");
let shippingEmailInput = angular.element("input[name=shippingEmail]");
if (shippingEmailLabel) {
shippingEmailLabel.toggleClass("nuskinRequiredBefore", $scope.createAccount || $scope.orderHasAdr);
// Set required attributes:
if(shippingEmailLabel.hasClass("nuskinRequiredBefore")) {
shippingEmailInput.prop("required", "true");
} else {
shippingEmailInput.removeAttr("required");
}
_validateEmail().then(() => {}).catch(() => {});
}
if ($scope.createAccount) {
$scope.sections.shipping.newAddress.saveShipping = true;
$scope.sections.payment.newPayment.savePayment__hide = false;
$scope.sections.payment.newPayment.defaultPayment__hide = false;
} else {
$scope.sections.shipping.newAddress.saveShipping = false;
$scope.sections.payment.newPayment.savePayment__hide = true;
$scope.sections.payment.newPayment.defaultPayment__hide = true;
}
};
/**
* COM-1643
*
* Tread lightly!
*
* This nest of boolean logic sets and unsets the error states and messages pertaining to the email (username).
* Promise issues a resolve if a valid email is provided and error messages are removed else set one or more error
* messages and issue a promise rejection.
*
* @private
* @return {Promise.<*>}
*/
function _validateEmail() {
let shippingEmailInput = angular.element("input[name=shippingEmail]");
let shippingEmailRequired = angular.element("div[id=shippingEmailRequired]");
let shippingEmailAlreadyTaken = angular.element("div[id=shippingEmailAlreadyTaken]");
let shippingEmailInputLength = shippingEmailInput.val().length;
// If the email is required:
if (shippingEmailInput.attr("required")) {
// If the email field is required and no text was provided print an error message and reject:
if (shippingEmailInputLength <= 0) {
let markup = `<div id='shippingEmailRequired'><br><span class='orderSummaryError'>${$scope.tdc.requiredFieldLabel}</span></div>`;
// Make sure not to re-add the markup
if (shippingEmailRequired.length <= 1) {
shippingEmailInput.after(markup);
}
$scope.forms.preShippingForm.shippingEmail.$setValidity("invalid", true);
return Promise.reject("Shipping email is required but no email was provided.");
}
// Else if there is text and the email is valid (matches the pattern) ensure it's not a duplicate:
else if ($scope.forms.preShippingForm.shippingEmail.$valid) {
return new Promise((resolve, reject) => {
return _duplicateUserCheck(shippingEmailInput.val())
.then((data) => {
if (!data.available) {
let markup = `<div id='shippingEmailAlreadyTaken'><br><span class='orderSummaryError'>${$scope.tdc.userAlreadyTaken}</span></div>`;
// Make sure not to re-add the markup
if ($("div[id=shippingEmailAlreadyTaken]").length <= 1) {
shippingEmailInput.after(markup);
}
$scope.accountCreationStates.lastAttemptedUser = shippingEmailInput.val();
$scope.forms.preShippingForm.shippingEmail.$setValidity("invalid", true);
shippingEmailRequired.remove();
$scope.forms.preShippingForm.shippingEmail.$setValidity("required", true);
reject("Shipping email can't be applied to account creation because it's already taken.");
} else {
shippingEmailAlreadyTaken.remove();
shippingEmailRequired.remove();
$scope.forms.preShippingForm.shippingEmail.$setValidity("required", true);
}
resolve();
})
.catch(console.error);
});
}
// Else it seems that there is some text but the email is still invalid. Remove the required error message
// as there is something but leave the invalid message:
else {
shippingEmailRequired.remove();
$scope.forms.preShippingForm.shippingEmail.$setValidity("required", true);
return Promise.reject("Shipping email provided but in an invalid format.");
}
}
// If the email field isn't required remove any required errors:
else {
shippingEmailRequired.remove();
angular.element("div[id=shippingEmailAlreadyTaken]").remove();
$scope.forms.preShippingForm.shippingEmail.$setValidity("required", true);
}
return Promise.resolve();
}
$scope.getItemCnt = () => {
return $scope.order.sapItemQty;
};
$scope.getAddressSummary = (address) => {
let result = "";
if (address) {
result = address.shippingAddress1;
if (address.shippingAddress2) {
result += `, ${address.shippingAddress2}`;
}
if (address.shippingCounty) {
result += `, ${address.shippingCounty}`;
}
if (address.addressType != '7-11') {
result += `, ${address.shippingCity}`;
}
if (address.shippingState) {
result += `, ${$filter('stateName')(address.shippingState)}`;
}
if (!$scope.config.checkout.hideShippingPostalCode) {
result += `, ${address.shippingPostalCode}`;
}
if (address.shippingCountry) {
result += `, ${address.shippingCountry}`;
}
}
return result;
};
$scope.shippingAccordionClicked = () => {
// Shipping section is open
// If we are busy don't let them do anything
if (!$scope.isBusy && !$scope.sections.shipping.pristine) {
if ($scope.sections.shipping.edit && $scope.order.selectedAddress) {
// If we are id edit mode don't let them try to continue if we are adding or have errors
if (!$scope.sections.shipping.adding && !$scope.sections.shipping.editing && !$scope.sections.shipping.error.hasErrorMessages()) {
// If they changed the address then we need to simulate and force the opening of the shipmethod section
if (!$scope.currentSelectedAddress || $scope.currentSelectedAddress.beShippingAddressId !== $scope.order.selectedAddress.beShippingAddressId) {
$scope.sections.shipmethod.pristine = true;
$scope.changeShippingAddressAndSimulate();
}
$scope.continueEdit('shipping');
}
} else if ($scope.enableChangeShippingAddress) {
// When the section is opened set the currentSelectedAddress so we can compare when it is closed to force a simulate
$scope.currentSelectedAddress = $scope.order.selectedAddress;
$scope.updateEdit('shipping');
}
}
};
$scope.shipMethodAccordionClicked = () => {
if (!$scope.isBusy && !$scope.sections.shipmethod.pristine) {
if ($scope.sections.shipmethod.edit) {
if (!$scope.shouldSelectPickupPoint() && !$scope.sections.shipmethod.error.hasErrorMessages()) {
//Collapse accordion
$scope.continueEdit('shipmethod');
}
} else if ($scope.enableChangeShippingAddress) {
$scope.updateEdit('shipmethod');
}
}
};
$scope.billingAccordionClicked = () => {
if (!$scope.isBusy && !$scope.sections.payment.pristine) {
if ($scope.sections.payment.edit) {
if (!$scope.isPaymentContinueBtnDisabled()) {
$scope.continueEdit('payment');
}
} else {
$scope.updateEdit('payment');
}
}
};
$scope.adrAccordionClicked = () => {
if (!$scope.isBusy && !$scope.sections.adrOptions.pristine) {
if ($scope.sections.adrOptions.edit) {
$scope.continueEdit('adrOptions');
} else {
$scope.updateEdit('adrOptions');
}
}
};
$scope.toggleFlag = (flag) => {
this[flag] = !(!!flag);
};
/**
* COM-1643
*
* Decorator for the ShippingAddressUtil addShippingAddress.
*
* @param type
* @param element
*/
$scope.addShippingAddressOrderSummary = (type, element, disabled = false) => {
if (disabled) {
return;
}
if ($scope.config.checkout.enablePolymerForms) {
document.getElementById('preShippingFormWithCreate').validate();
} else {
$scope.forms[type + "ShippingForm"].showValidationError = true;
}
// For a user to create the account they must be in a guest checkout flow and either the create account checkbox
// is checked or the order has an ADR.
if ($scope.isGuestCheckout && ($scope.createAccount || $scope.orderHasAdr)) {
// User didn't check the Terms and Conditions and Privacy Policy checkbox:
if (!$scope.accountCreationStates.createAccountTermsConditionsChecked) {
return;
}
if (!$scope.hideGovtIdfield && !$scope.sections.shipping.newAddress.govtID) {
$scope.forms.preShippingForm.govtID.$error.required = true;
return;
}
_validateEmail().then(() => {
if ($scope.createAccount) {
if (!_validatePassword()) {
return;
}
// The password and email (username) are valid; submit the form data:
$scope.addShippingAddress(false, type, element,
$scope.sections.shipping.newAddress.govtID,
$scope.sections.shipping.newAddress.userPassword,
$scope.accountCreationStates.contactBySMSChecked,
$scope.accountCreationStates.contactByEmailChecked);
}
}).catch(() => {});
} else if ($scope.isGuestCheckout) {
if (!$scope.hideGovtIdfield && !$scope.sections.shipping.newAddress.govtID) {
if (!$scope.config.checkout.enablePolymerForms) {
$scope.forms.preShippingForm.govtID.$error.required = true;
}
return;
} else {
$scope.addShippingAddress(false, type, element, $scope.sections.shipping.newAddress.govtID);
}
} else {
// Else add the shipping address. The password isn't embedded but that's fine since this isn't a create
// account (guest checkout with or without ADR) flow.
$scope.addShippingAddress(false, type, element);
}
};
$scope.resetShippingAccountState = () => {
_setAccountCreationStates();
};
// Returns true if the product's price list includes a points price and the item is not ADR, false otherwise
$scope.hasPointsPrice = item => (Number.isFinite(item.points) && !item.isAdr);
// Returns the total points price for this item
// (e.g. item that costs 4.5 pts each, user is purchasing 3 with points -> item points subtotal = 4.5 * 3 = 13.5 pts)
$scope.getItemSubtotalPoints = item => item.qtyRedeemWithPoints * item.points;
$scope.getItemUnitPrice = sapItem => {
let unitPrice = 0;
if (sapItem.price > 0) {
// calculate based off of simulate. Should not be a divide by 0 since lineItem price > 0. (qty > qtyRedeemWithPoints
unitPrice = sapItem.price / (sapItem.qty - sapItem.qtyRedeemWithPoints);
} else {
// can't base it off of simulate, use cart unit price.
CartService.getItemData({cartItems: true})
.filter(item => item.sku === sapItem.sku)
.forEach(item => {
unitPrice = item.price;
});
}
return unitPrice;
};
// Handle points allocation for line items with quantity = 1
$scope.syncSingleItemRedeem = item => {
if ($scope.showPointsPaymentOptions && validatePointsChange(item, item.redeem ? item.qty : 0)) {
updateDomItemPricing(item, item.redeem ? item.qty : 0);
$scope.updateEdit("cart");
} else {
item.redeem = false;
}
};
// Handle points allocation for line items with quantity > 1
$scope.updatePtsForMultipleQtyItem = (item, usePoints) => {
if ($scope.showPointsPaymentOptions && validatePointsChange(item, usePoints ? 1 : 0)) {
if (usePoints) {
item.redeem = true;
updateDomItemPricing(item, 1);
} else {
item.redeem = false;
updateDomItemPricing(item, 0);
}
$scope.updateEdit("cart");
} else {
item.redeem = false;
}
};
$scope.decreaseQtyRedeem = item => {
if ($scope.showPointsPaymentOptions && validatePointsChange(item, item.qtyRedeemWithPoints - 1)) {
if (item.qtyRedeemWithPoints > 1) {
updateDomItemPricing(item, item.qtyRedeemWithPoints - 1);
}
}
};
$scope.increaseQtyRedeem = item => {
if ($scope.showPointsPaymentOptions && validatePointsChange(item, item.qtyRedeemWithPoints + 1)) {
if (item.qtyRedeemWithPoints < item.qty) {
updateDomItemPricing(item, item.qtyRedeemWithPoints + 1);
}
}
};
/**
* This is the ng-click event function for the points Continue button.
* Simulate order then reset (un-pristine) the points fields.
*/
$scope.pointsContinueClicked = (disabled = false) => {
if (disabled) return;
if ($scope.pointsEdited) {
$scope.simulateOrder().then(function () {
$scope.continueEdit("cart");
$scope.pointsEdited = false;
});
} else {
$scope.continueEdit("cart");
}
};
$scope.updateQtyRedeem = item => {
if (!$scope.showPointsPaymentOptions || !item.redeem) {
return;
}
let newQtyRedeem = Math.floor(item.qtyRedeemWithPoints || 0);
newQtyRedeem = Math.floor(newQtyRedeem || 0);
newQtyRedeem = newQtyRedeem > item.qty ? item.qty: newQtyRedeem;
newQtyRedeem = newQtyRedeem < 1 ? 1 : newQtyRedeem;
if (validatePointsChange(item, newQtyRedeem)) {
updateDomItemPricing(item, newQtyRedeem);
}
};
// Sets the quantity to redeem with points on the item, and updates the item
// price, cart subtotal price, and grand total price accordingly.
function updateDomItemPricing(sapItem, qtyRedeem) {
CartService.getItemData({cartItems: true})
.filter(cartItem => cartItem.sku === sapItem.sku)
.forEach(cartItem => {
let {deltaItemTotal, sapItemData} = CartService.updateItemPointsPrice(sapItem.key, cartItem.key, qtyRedeem);
// update local sapItem with new information
Object.assign(sapItem, sapItemData);
$scope.order.orderTotals.itemSubtotal += deltaItemTotal;
$scope.order.orderTotals.grandTotal += deltaItemTotal;
$scope.cartPointsSubtotal = getPointsPriceTotal(false);
$scope.pointsTotal = getPointsPriceTotal();
});
$scope.order.orderTotals.psvTotal = getPsvTotal();
}
function getPsvTotal() {
let psvRunningTotal = 0;
CartService.getItemData({sapItems: true})
.forEach(sapItem => {
// update psv
psvRunningTotal += (sapItem.qty - sapItem.qtyRedeemWithPoints) * CartService.getItemUnitPsv(sapItem.key);
});
return psvRunningTotal;
}
function validatePointsChange(sapItem, newQtyRedeem) {
let pointsDiff = (sapItem.points * newQtyRedeem) - (sapItem.points * (sapItem.qtyRedeemWithPoints || 0)),
singlePsv = CartService.getItemUnitPsv(sapItem.key),
psvDiff = ((singlePsv * newQtyRedeem) - (singlePsv * sapItem.qtyRedeemWithPoints || 0)).toFixed(2),
retVal = false;
$scope.pointsEdited = true;
// Skip validation if user's points balance not yet initialized
if (!$scope.showPointsPaymentOptions || $scope.userAvailablePoints === null) {
return;
}
if ($scope.pointsTotal + pointsDiff > $scope.userAvailablePoints) {
addValidationMessage($scope.tdc.userPointsExceededError);
} else if (!AdrService.isAdrOverride() && getOrderTotalPsv() - psvDiff <= 0) {
addValidationMessage($scope.tdc.zeroPsvError);
} else {
removeValidationMessage();
retVal = true;
}
function addValidationMessage(message) {
let domHasMessage = $scope.sections.cart.error.infoMessages
.some(msg => msg.text === message);
if (!domHasMessage) {
$scope.sections.cart.error.addErrorMessage('info', {text: message});
}
}
function removeValidationMessage() {
let leftOverMessages = $scope.sections.cart.error.infoMessages
.filter(item => item.text !== $scope.tdc.userPointsExceededError && item.text !== $scope.tdc.zeroPsvError);
$scope.sections.cart.error.infoMessages = [];
leftOverMessages.forEach(message => $scope.sections.cart.error.addErrorMessage('info', {text: message}));
}
return retVal;
}
function getOrderTotalPsv() {
return parseFloat(CartService.getItemData({cartItems: true})
.map(item => {
return item.singlePv * (item.qty - (item.qtyRedeemWithPoints || 0));
})
.reduce((a, b) => a + b, 0))
.toFixed(2);
}
// Because the checkout DOM is built from SAP items list, and order requests are built from cart items list,
// this function copies changes to points allocation from SAP item to cart item
function syncCartItemToSapItem(sapItem) {
CartService.syncCartItemToSapItem(sapItem.key);
}
/***
* Acquire the total points.
*
* @param applyShipping{boolean} to the total point price. True by default.
* @returns {number} the point price total
*/
function getPointsPriceTotal(applyShipping = true) {
if (!$scope.order) {
return 0.0;
}
let productsTotal = CartService.getItemData({sapItems: true})
.filter(item => !!item.points)
.map(item => {
let qtyRedeem = item.qtyRedeemWithPoints > 0 ? item.qtyRedeemWithPoints : item.redeem ? item.qty : 0;
return item.points * qtyRedeem;
})
.reduce((a, b) => a + b, 0);
let pointsShippingCost = 0;
if (nuskin.configuration.adpManage && nuskin.configuration.adpManage.PayWithPointsShippingCost) {
pointsShippingCost = Number.parseInt(nuskin.configuration.adpManage.PayWithPointsShippingCost);
}
return productsTotal + (($scope.order.shippingPayWithPoints && applyShipping) ? pointsShippingCost : 0);
}
function resetPointsAllocations() {
if ($scope.showPointsPaymentOptions) {
CartService.getItemData({sapItems: true})
.filter(item => item.redeem)
.forEach(item => {
updateDomItemPricing(item, 0);
});
$scope.order.shippingPayWithPoints = false;
}
}
$scope.isPWPShippingMethod = selectedMethod => {
let adpManage = nuskin.configuration.adpManage;
if (!adpManage || !adpManage.enablePayWithPoints || !adpManage.PayWithPointsShippingMethods) return false;
if (!selectedMethod || !selectedMethod.Code) return false;
return adpManage.PayWithPointsShippingMethods.indexOf(selectedMethod.Code) > -1;
};
$scope.onTogglePWPShipping = () => {
$scope.pointsTotal = getPointsPriceTotal();
if ($scope.pointsTotal > $scope.userAvailablePoints) {
$scope.order.shippingPayWithPoints = false;
$scope.pointsTotal = getPointsPriceTotal();
// Add user error message if not already present
let hasPtsExceededError = $scope.sections.review.error.errorMessages
.some(msg => msg.text === $scope.tdc.userPointsExceededError);
if (!hasPtsExceededError) {
$scope.sections.review.error.addErrorMessage("error", {text: $scope.tdc.userPointsExceededError});
}
}
};
$scope.gmap = {
googleMapsApiUrl: googleMapsApi,
markers: null,
selectedMarker: null,
instance: null
};
$scope.showCheckout = storage.getItem(storage.metadata.RECEIVING_ITEMS) !== true;
var EXTERNAL_PAYMENT_NEXT_ORDER = "externalPaymentNextOrder";
if (!$scope.showCheckout) {
return;
}
$scope.forms = {};
$scope.runConfig = RunConfigService.getRunConfig();
// Provide a default landing page if needed.
if (!$scope.runConfig.landing){
$scope.runConfig.landing = "checkout.html#/orderSummary";
}
$scope.buyer = BuyerCookieService.getBuyer();
$scope.user = UserService.getUser();
// See: checkout/harness/index.html (search for checkoutSetup)
try {
$scope.editMode = checkoutSetup.editMode;
$scope.bruneiName = checkoutSetup.bruneiName;
$scope.indonesiaName = checkoutSetup.indonesiaName;
$scope.malaysiaName = checkoutSetup.malaysiaName;
$scope.philippinesName = checkoutSetup.philippinesName;
$scope.singaporeName = checkoutSetup.singaporeName;
$scope.thailandName = checkoutSetup.thailandName;
}
// If checkoutSetup is not set up:
catch(err) {
console.error(err.message);
}
$scope.saveSAPAddresstoProfile = false;
$scope.transBankInstallmentsBusy = false;
$scope.transBankErrorMessage = "";
$scope.isSummerPromo = false;
$scope.getCartUrl = function () {
if ($scope.runConfig.cartUrl && $scope.runConfig.cartUrl.length > 0) {
return $scope.runConfig.cartUrl;
} else {
var cart = "";
if (StoreFrontSponsorStorageService.getStoreFrontSponsor() && SponsorStorageService.getSponsor()) {
cart = 'cart.mysite.html#/cart';
} else {
cart = 'cart.html#/cart';
}
return cart;
}
};
if (nsAdrUtil.showAdrShipWhen()) {
$location.path("/adrShipWhen");
return;
}
// ---------------------------------------------
//
// Private Variables
//
// ---------------------------------------------
var buyer = $scope.buyer;
var user = $scope.user;
// ---------------------------------------------
//
// Public Variables
//
// ---------------------------------------------
$scope.hideGovtIdfield = true;
$scope.config = ConfigService.getMarketConfig();
ConfigService.loadCustomJSStylesOnPage('Checkout');
if ($scope.config.signup.governmentIDOptions !== "NONE" && $scope.config.signup.governmentIDOptions !== undefined) {
$scope.hideGovtIdfield = false;
}
if (nuskin.util.MobileUtils.isMobileLike() || (window.innerWidth < 1337 && $scope.config.checkout.usecheckoutSideBar)) {
$scope.pickupMapSelected = true;
}
$scope.tdc = tdcService.getTdc('checkout');
$scope.PaymentType = PaymentType;
$scope.hasEventItems = CartService.hasEventItems();
$scope.isPitchCart = !!CartService.getCartProperty("isPitchCart");
if (StoreFrontSponsorStorageService.getStoreFrontSponsor() && SponsorStorageService.getSponsor() && !$scope.isPitchCart) {
$scope.isStorefrontCart = true;
} else {
$scope.isStorefrontCart = false;
}
$scope.isGuestCheckout = CheckoutModeService.isGuestCheckout();
$scope.hidePointValue = false;
$scope.isGift = !!CartService.getCartProperty("isGift");
$scope.chosen = {};
$scope.chosen.element = null;
$scope.chosen.isValid = false;
$scope.termsConditionsCheckbox = false;
$scope.globalHeader = null;
$scope.errorHeader = null;
$scope.paymentIsWire = false;
$scope.deleteShippingDialogId = null;
$scope.deletePaymentDialogId = null;
$scope.emailOptIn = false;
$scope.stickySideBar = { value: false };
$scope.slideUpOrderReview = false;
$scope.isIOS = nuskin.util.MobileUtils.isIOS();
$scope.googleMapsChanged = false;
// The following properties on scope should not be modified in a isolate scope directive
$scope.contentLoaded = false;
$scope.showSpinner = true;
$scope.buyForOther = false;
$scope.isBusy = false;
$scope.forceDisableOrderButton = false;
$scope.backdatingToggle = false;
$scope.showExtraPaymentInfo = false;
$scope.usedPoints = CartService.getCartInfo().totalOrderPoints;
$scope.showPsv = ShopContextService.showPsv($scope.buyer ? $scope.buyer.accountType : 0) && !$scope.isGuestCheckout;
$scope.showPointsPaymentOptions = false;
$scope.detailsIcon = 'nuskin:icon-smallcart-empty';
$scope.userAvailablePoints = null;
$scope.pointsTotal = 0;
// Health warnings required by California for orders containing applicable SKUs
$scope.californiaWarnings = null;
if ($scope.isPitchCart) {
$scope.personalOffer = PersonalOfferStorageService.getPersonalOffer();
}
SectionUtil.initSections($scope, nsSections, nsAdrUtil, $timeout, PersonalOfferService);
var hideProcessingIndicators = function() {
$('#mobile-processing').hide();
$scope.isBusy = false;
};
// If authentication is required and the user is not authenticated we redirect back to the cart with a force login.
if (ConfigService.getMarketConfig().authenticationRequired) {
var redirectToCartPage = false;
if (UserService.isLTOSite()){
if (!AuthenticationService.isLoggedIn()){
redirectToCartPage = true;
}
}
else if (!AccountManager.isLoggedIn() && !$scope.isGuestCheckout){
redirectToCartPage = true;
}
if (redirectToCartPage){
var indexOfCheckout = window.location.href.indexOf('checkout.html');
window.location.href = window.location.href.substr(0, indexOfCheckout) + $scope.getCartUrl() + "#forceLogin";
return;
}
}
// Redirect to order confirmation if we have received a successful externalPayment
var needToHandleExternalPayment = nsExternalPayment.needToHandleExternalPayment();
if (needToHandleExternalPayment) {
hideProcessingIndicators();
if (nsExternalPayment.handlePayment($scope.tdc, $scope.sections)) {
hideProcessingIndicators();
return; // exit here... skip the rest of the functions
}
}
// ---------------------------------------------
//
// Public Methods
//
// ---------------------------------------------
$scope.initAndGo = function () {
$scope.isBusy = true;
var simulatePromiseDependencies = [];
OrderUtil.initOrders($scope, $rootScope, $location, $timeout, $q, $http, nsOrder, nsProduct, nsUtil, nsExternalPayment, simulatePromiseDependencies, nsGuestSignup);
PaymentUtil.initPayments($scope, $http, nsUtil, nsPayment, nsAdrUtil, simulatePromiseDependencies, PersonalOfferService);
// Special case for JP with Business Portfolio....
if ($scope.order.simulateWithoutShipping) {
CartUtil.initializeCart();
// For fixing the P1 production issue
$('#orderSummaryRoot').css('display', 'block');
} else {
ShippingAddressUtil.initShippingAddresses($scope, nsUtil, nsShipping, nsTimeDelivery, $timeout, simulatePromiseDependencies, nsGuestSignup, PersonalOfferService);
loadCheckout();
}
$scope.cuotas = $scope.order.selectedPayment && $scope.order.selectedPayment.useInstallments ? $scope.order.selectedPayment.getInstallmentSelectedNumber() : 1;
setTimeout(function () {
if (nuskin && ConfigService.getMarketConfig().enableThreatMetrics) {
ThreatMatrixUtil.initThreatMatrixScriptUrl($scope, nsUtil);
}
}, 2000);
};
$scope.initShippingAddresses = function() {
ShippingAddressUtil.initShippingAddresses($scope, nsUtil, nsShipping, nsTimeDelivery, $timeout, []);
};
$scope.setCaliforniaWarnings = function() {
let address = $scope.order.selectedAddress;
if (!address) return; // exit early if there is no shipping address
if ($scope.runConfig.country === 'US' && address.shippingState === 'CA') {
// Order is shipping to California; add warnings if order contains applicable SKUs;
let skuWarnings = window.californiaSkuWarnings || [];
let orderItems = CartService.getItemData({sapItems: true});
$scope.californiaWarnings = skuWarnings
.filter(s => orderItems.some(item => item.sku === s.sku))
.map(s => s.warning)
.join('\n');
} else {
$scope.californiaWarnings = null;
}
};
/**
* Load sticky headers and hide processing indicators once content has been loaded into DOM
*/
$scope.$watch("contentLoaded", function(newValue) {
if(newValue) {
$scope.enableCreateAccount = nuskin.configuration.signup.enableCreateAccount;
$scope.buildStickyHeaders();
hideProcessingIndicators();
$scope.pointsEdited = false;
showPointsPaymentOptions();
_setAccountCreationStates();
/**
* COM-1921 If there is an ADR item on the order then we need to require the account to be created and we
* need to create the account earlier in the process.
*/
let shippingEmailLabel = angular.element("label[for=shippingEmail]");
let shippingEmailInput = angular.element("input[name=shippingEmail]");
shippingEmailInput.blur(() => {
_validateEmail().then(() => {}).catch(() => {});
});
let userPasswordField = angular.element("#userPasswordField");
let confirmationUserPasswordField = angular.element("#confirmationUserPasswordField");
userPasswordField.blur(() => _validatePassword());
confirmationUserPasswordField.blur(() => _validatePassword());
if (CartService.getItemCnt({cartAdrItems: true}) > 0) {
$scope.createAccount = !!$scope.isGuestCheckout;
if (!shippingEmailLabel.hasClass("nuskinRequiredBefore")) {
shippingEmailLabel.attr("required", "");
shippingEmailLabel.addClass("nuskinRequiredBefore");
} else {
shippingEmailLabel.removeAttr("required");
}
// Set required attributes:
if(shippingEmailLabel.hasClass("nuskinRequiredBefore")) {
shippingEmailInput.attr("required", true);
} else {
shippingEmailInput.removeAttr("required");
}
$scope.orderHasAdr = true;
} else {
$scope.orderHasAdr = false;
shippingEmailLabel.removeAttr("required");
shippingEmailInput.removeAttr("required");
}
$scope.setCaliforniaWarnings();
$scope.sidebar = document.getElementById("checkoutSideBar");
if (nuskin.util.MobileUtils.isMobileLike()) {
window.requestAnimationFrame(stickyScroll);
}
}
});
function simulateWithoutShippingForJapan() {
$scope.order.simulateWithoutShipping = true;
$scope.saveSAPAddresstoProfile = true;
CartUtil.initializeCart();
// For fixing the P1 production issue
$('#orderSummaryRoot').css('display', 'block');
}
$scope.buildStickyHeaders = function () {
setTimeout(function () {
if (nuskin && StickyHeader && !$scope.busyHeader) {
// If we're inside AgeLoc, we need to watch a different scroll element
if(document.querySelector('.ageloc-wrap')) {
StickyHeaderStack.scrollWatch= '.ageloc-wrap';
}
StickyHeaderStack.reinitialize();
var headerBoundary = new StickyHeaderBoundary('#header');
StickyHeaderStack.stackOffsetMobile = '#header';
$scope.errorHeader = new StickyHeader('#checkoutErrorSummary .errorDialogue', {
customLeftAlignment: true,
onClick: 'scrollToOrigin'
});
$scope.shippingError = new StickyHeader('#checkout-shipping-address .errorDialogue', {
customLeftAlignment: true,
onClick: 'scrollToOrigin'
});
$scope.paymentError = new StickyHeader('#checkout-edit-payment .errorDialogue', {
customLeftAlignment: true,
onClick: 'scrollToOrigin'
});
$scope.busyHeader = new StickyHeader('#busyHeader', {
customLeftAlignment: true,
onClick: 'scrollToOrigin'
});
$scope.reviewError = new StickyHeader('#checkout-review-order .errorDialogue', {
customLeftAlignment: true,
onClick: 'scrollToOrigin'
});
StickyHeaderStack.$stackProxy.addClass('ns-atomic');
}
}, 5000);
};
function stickyScroll() {
let sidebar = document.getElementById("checkoutSideBar");
if (sidebar) {
let sticky = sidebar.getBoundingClientRect().top;
let paymentSection = document.getElementById("checkout-edit-payment");
let paymentBottom = paymentSection.getBoundingClientRect().bottom;
let adrSection = document.getElementById("checkout-adrOptions");
let adrBottom = adrSection.getBoundingClientRect().bottom;
let bottomSection = paymentBottom;
let body = document.getElementsByTagName('body')[0];
let windowWidth = body.offsetWidth;
let windowHeight = window.innerHeight;
if ($scope.order.adr || $scope.adr) {
bottomSection = adrBottom;
}
if (nuskin.util.MobileUtils.isMobileLike()) {
sidebar.removeAttribute('style');
if (windowHeight < windowWidth && windowWidth >= 767) {
$scope.stickySideBar.value = false;
} else if ((windowHeight - 60) < sticky && !$scope.stickySideBar.value) {
$scope.stickySideBar.value = true;
} else if ((windowHeight - 60) <= sticky && $scope.stickySideBar.value && bottomSection > (windowHeight - 70)) {
return
} else if (bottomSection < (windowHeight - 70)) {
$scope.stickySideBar.value = false;
$scope.slideUpOrderReview = false;
}
if ($scope.$$phase !=