PageSourceSearch

https://www.threadandpaper.com/1790254316/checkout/checkoutCtrl.js

js threadandpaper.com collected 2026-09-27 10:00:37 UTC 463,989 bytes, 11,672 lines download raw bytes

1'use strict';
2
3var checkoutApp = angular.module('checkoutApp', ['rain.form.validate', 'ngSanitize']).config(function ($sceDelegateProvider) {
4    $sceDelegateProvider.resourceUrlWhitelist([
5        'self',
6        "https://secure.networkmerchants.com**",
7        "https://**.paypal.com/cgi-bin/webscr",
8        "https://www.my-davelocal.com/**",
9        "https://www.my-quiltstorewebsites.com/**",
10        "https://certtransaction.hostedpayments.com/**",
11        "https://transaction.hostedpayments.com/**"
12    ]);
13});
14
15checkoutApp.controller('checkoutCtrl', ['$scope', '$http', '$window', '$timeout', '$interval', '$q', function ($scope, $http, $window, $timeout, $interval, $q) {
16    $scope.DATA_STATE_NONE = 0;
17    $scope.DATA_STATE_LOADING = 1;
18    $scope.DATA_STATE_NOT_SUPPORTED = 2;
19    $scope.DATA_STATE_ERROR = 3;
20    $scope.DATA_STATE_LOADED = 4;
21    $scope.geoLocationZipCode = '';
22
23    $scope.PROCESSOR_NMI = 'NMI3S';
24    $scope.PROCESSOR_VANTIV = 'VantivHosted';
25    $scope.PROCESSOR_RAIN = 'RAIN_PAYMENTS';
26    $scope.PROCESSOR_QUILT_ADYEN = 'Q_ADYEN';
27    $scope.PROCESSOR_PAYPAL = 'paypal';
28    $scope.PROCESSOR_PAY_AT_PICKUP = 'pay_at_pickup';
29    $scope.PROCESSOR_FREE = 'free';
30    $scope.PROCESSOR_GIFT_CARD = 'gc';
31    $scope.STRIPE_MIN = .5;
32    $scope.ITEM_TYPE_CARE_PLAN = 'care_plan';
33    $scope.RETAIL_DELIVERY_FEE = typeof COLORADO_RETAIL_DELIVERY_FEE !== 'undefined' ? COLORADO_RETAIL_DELIVERY_FEE : 0;
34
35    // Flags
36
37    $scope.enhancedShippingAndStorePickupOptions = false;
38    $scope.shippingCalcState = '';
39    $scope.shippingCalcZipCode = '';
40    $scope.enhancedShippingInStorePickup = false;
41    $scope.enhancedShippingCurbsidePickup = false;
42    $scope.enhancedShippingInfo = {};
43    $scope.enhancedShippingInfo.shippingOptions = {};
44    $scope.enhancedShippingInfo.storePickupOptions = {};
45    $scope.enhancedShippingInfo.curbsidePickupOptions = {};
46    $scope.enhancedShippingInfo.schoolPickupOptions = {};
47    $scope.enhancedShippingInfo.localDeliveryOptions = {};
48    $scope.Class = {};
49    $scope.InitInfo = {
50        InternationalEnabled: false,
51    };
52    $scope.forms = {};
53    $scope.StatusInfo = {};
54    $scope.StatusInfo.Initializing = true;
55    $scope.StatusInfo.CouponShow = false;
56    $scope.StatusInfo.GiftCardShow = false;
57    $scope.StatusInfo.IsCouponEntered = false;
58    $scope.StatusInfo.processing = false;
59
60    $scope.Processing = false;
61
62    $scope.CartData = {};
63    $scope.CartStatus = {};
64    $scope.CartStatus.Valid = true;
65    $scope.InventoryMessages = {};
66    $scope.AvailabilityMessages = {};
67
68    $scope.ContactInfo = {};
69    $scope.ContactStatus = {};
70
71    $scope.ShippingInfo = {};
72    $scope.HideBillingSameAsShipping = false;
73    $scope.ShippingInfoPrefilledFlags = {
74        Address1: false,
75        Address2: false,
76        City: false,
77        State: false,
78        Zip: false,
79        Country: false
80    };
81    $scope.ShippingInfo.State = "";
82    $scope.ShippingStatus = {};
83    $scope.ShippingStatus.Required = true;
84    $scope.ShippingStatus.Local = true;
85    $scope.ShippingStatus.OptionsUpdating = 0;
86    $scope.ShippingDiscount = 0;
87    $scope.ShippingInfo.shipToSchool = false;
88    $scope.ShippingInfo.selectedDistrict = null;
89    $scope.ShippingInfo.selectedSchool = null;
90    $scope.districts = [];
91    $scope.schools = [];
92    $scope.allSchools = [];
93
94    $scope.CardInfo = {};
95    $scope.CardStatus = {};
96    $scope.Rain = {};
97    $scope.Adyen = {};
98
99    $scope.BillingInfo = {};
100    $scope.BillingInfo.AddressSameAsShipping = true;
101    $scope.BillingStatus = {};
102    $scope.BillingStatus.Required = true;
103
104    $scope.BillingAddress = {};
105    $scope.BillingAddress.Required = false;
106
107    $scope.giftCardOrCouponDataState = $scope.DATA_STATE_NONE;
108    $scope.giftCardOrCouponCode = '';
109    $scope.giftCardOrCoupon = [];
110    $scope.giftCardCouponMsg = '';
111    $scope.giftCardCouponMsgTimeout = null;
112    $scope.giftCardOrCouponRemovalMap = {};
113
114    $scope.CheckoutInfo = {};
115    $scope.CheckoutInfo.sendTextNotifications = false;
116
117    $scope.GiftMessage = {};
118    $scope.orderNotes = {};
119    $scope.JurisdictionalTax = "";
120    $scope.JurisdictionalTaxes = {};
121    $scope.shipping_tax_jurisdictions = {};
122    $scope.taxable_sales = 0;
123    $scope.non_taxable_sales = 0;
124    $scope.non_sales = 0;
125    $scope.HasDynamicTaxes = false;
126    $scope.dynamicRateCache = null;
127    $scope.dynamicRateCacheKey = null;
128    $scope.status = {};
129    $scope.showCaptcha = null;
130    $scope.GST_Taxes = 0;
131    $scope.GST_SubTotal = 0;
132    $scope.GST_Due = 0;
133    $scope.isTestStore = false;
134    $scope.ShippingSubtotal = 0.00;
135    $scope.defaultOptions = {};
136    $scope.options = {};
137    $scope.dynamicTotalTax = 0;
138    $scope.knobby = {};
139    $scope.tax_service = 0;
140    $scope.couponStatusMessage = '';
141    $scope.domainDisabled = false;
142    $scope.subscribeToMarketingEmails = false;
143    $scope.isMobileBrowser = 0;
144    $scope.scheduledPaymentId = 0;
145    $scope.subscriptionId = 0;
146    $scope.invoiceId = 0;
147    $scope.invoicesCartId = 0;
148    $scope.rtoCartId = null;
149    $scope.customerId = 0;
150    $scope.paymentToken = null;
151    $scope.paymentType = null;
152    $scope.paymentDueDate = null;
153    $scope.mmsAmount = null;
154    $scope.dynamicTaxesPromiseAccumulator = null;
155    $scope.dynamicTaxesReady = true;
156    $scope.jmInsurance = {};
157    $scope.confirmedJMInsurance = false;
158    $scope.contactJMInsuranceInfo = {};
159    $scope.retailDeliveryFeeTaxJurisdiction = {};
160    $scope.digitalOrder = false;
161
162    $scope.retailDeliveryFee = 0.00;
163
164    $scope.isInsuranceSwitchedOn = false;
165    $scope.isLuxsuranceSwitchedOn = false;
166    $scope.recaptchaToken = null;
167    $scope.customerCards = [];
168    $scope.formData = {
169        selectedCustomerCardId: null,
170    };
171    $scope.paymentMode = 'manual';
172    $scope.isShippingOptionsProvided = true;
173    $scope.isLocalDeliveryOptionsProvided = true;
174
175    $scope.safeApply = function() {
176        var phase = this.$root.$$phase;
177
178        if (phase !== '$apply' && phase !== '$digest') {
179            this.$apply();
180        }
181    };
182
183    /**
184     * Model for registering stripe payment request(apple/google pay)
185     * @type {?PaymentRequestModel}
186     */
187    $scope.paymentRequestModel = null;
188
189    $scope.isRequestPaymentSupported = false;
190
191    $scope.getPaymentRequestModel = function () {
192        if (!$scope.paymentRequestModel) {
193            if (typeof PaymentRequestModel !== 'undefined' && typeof CheckoutContextWrapper !== 'undefined') {
194                $scope.paymentRequestModel = new PaymentRequestModel(new CheckoutContextWrapper($scope));
195            }
196        }
197
198        return $scope.paymentRequestModel;
199    };
200
201    $scope.showPaypalButtonOnCart = function () {
202        if (!$scope.CartData.Items) {
203            return false;
204        }
205
206        if ($scope.isCardProcessorRain() && $scope.isBNPLEnabled()) {
207            // Show paypal button in express checkout block
208            return false;
209        }
210
211        var isSupportedProcessor = $scope.allowCreditCardCheckout() || $scope.isCardProcessorVantiv() || $scope.isCardProcessorRain() || $scope.isCardProcessorQuiltAdyen();
212
213        return isSupportedProcessor && $scope.allowPayPalCheckout() && !$scope.isCardOnFileNeeded();
214    };
215
216    $scope.showExpressCheckout = function () {
217        return !$scope.checkFlagDisableExpressCheckout() && $scope.showMainCheckout() && $scope.isRequestPaymentSupported;
218    };
219
220    $scope.paymentMethod = 'cc';
221
222    $scope.onPaymentMethodChanged = function (paymentMethod) {
223        $scope.paymentMethod = paymentMethod;
224
225        if ($scope.paymentMethod === 'pickup') {
226            $scope.payAtPickupSelected();
227        } else {
228            $scope.payWithCardSelected()
229        }
230    };
231
232    /**
233     * @type {StripeBNPLModel|null}
234     */
235    $scope.stripeBNPLModel = null;
236
237    $scope.isAffirmEnabled = function() {
238        return $scope.isBNPLEnabled('affirm');
239    };
240
241    $scope.isBNPLEnabled = function (type) {
242        if (!$scope.CartData || !$scope.CartData.Items || $scope.isWebPayment()) {
243            return false;
244        }
245
246        var hasNotSupportedItems = $scope.getItemsWithPackageItems().some(function (item) {
247            if (['class', 'rto', 'subscription', 'rental', 'gc'].indexOf(item.type) !== -1) {
248                return true;
249            }
250
251            if ($scope.digitalOrder) {
252                return false
253            }
254
255            if (item.type === 'pslr' && item.downloadable) {
256                return true;
257            }
258
259            return false;
260        });
261
262        if (!$scope.isCardProcessorRain() || $scope.isWebPayment() || hasNotSupportedItems) {
263            return false;
264        }
265
266        var bnplSettings = null;
267        if ($scope.InitInfo && $scope.InitInfo.CheckoutSettings && $scope.InitInfo.CheckoutSettings.BNPL) {
268            bnplSettings = $scope.InitInfo.CheckoutSettings.BNPL;
269        }
270
271        if (!type) {
272            return Object.entries(bnplSettings).some(function (entry) {
273                return entry[1];
274            });
275        }
276
277        if (!bnplSettings || !$scope.stripeBNPLModel) {
278            return false;
279        }
280
281        return !!bnplSettings[type];
282    };
283
284    $scope.getBNPLAmount = function () {
285        var amountDue = $scope.getAmountDue();
286
287        if (typeof amountDue !== 'number' || isNaN(amountDue)) {
288            return 0;
289        }
290
291        return parseInt((amountDue * 100).toFixed(0));
292    };
293
294    $scope.getBNPLCurrency = function () {
295        var currency = 'usd';
296
297        if ($scope.InitInfo && $scope.InitInfo.Currency && typeof $scope.InitInfo.Currency === 'string') {
298            currency = $scope.InitInfo.Currency.toLowerCase();
299        }
300
301        return currency;
302    };
303
304    $scope.getBNPLCountry = function () {
305        var country = 'US';
306
307        if ($scope.BillingInfo && $scope.BillingInfo.CountryIso2Key) {
308            country = $scope.BillingInfo.CountryIso2Key || country;
309        }
310
311        return country.toUpperCase();
312    };
313
314    $scope.getBNPLMerchantCountry = function () {
315        var merchantCountryIso = $scope.CountryIso2KeyGet($scope.InitInfo.DomesticCountry);
316
317        var merchantCountry = 'US';
318        if (merchantCountryIso && typeof merchantCountryIso === 'string') {
319            merchantCountry = merchantCountryIso.toUpperCase();
320        }
321
322        return merchantCountry;
323    };
324
325    $scope.onBNPL = function (type, checkoutOptions) {
326        checkoutOptions = checkoutOptions || {};
327        var checkCaptcha = function () {
328            if (!$scope.isMinimumAmountMet()) {
329                return Promise.reject('Minimum Amount');
330            }
331
332            if ($scope.showCaptcha) {
333                if ($scope.knobby && $scope.knobby.module_update_to_captcha_v3) {
334                    var authToken = $scope.InitInfo.store_token;
335                    var url = $scope.RainApiUrl + 'store/captcha/validate';
336                    var headers = {};
337                    if (authToken) {
338                        headers.Authorization = 'Bearer ' + authToken;
339                    }
340
341                    return new Promise(function(resolve, reject) {
342                        grecaptcha.ready(function() {
343                            grecaptcha.execute(window.recaptchaSiteKey, {action: 'checkout'})
344                                .then(function(token) {
345                                    $http.post(url, {token: token}, {headers: headers})
346                                        .then(function() {
347                                            $scope.showCaptcha = false;
348
349                                            resolve(token);
350                                        })
351                                        .catch(function(response) {
352                                            $scope.showCaptcha = true;
353
354                                            $.growlUI('Error <i class="fa fa-exclamation text-danger"></i>', response.data.error);
355
356                                            reject(response.data.error);
357                                        });
358                                });
359                        });
360                    });
361                }
362
363                return $http.get($scope.BuildUrl('/captcha/check/' + window.captchaResponse), {withCredentials: true}).then(function (result) {
364                    var status = result.data.success === true;
365
366                    $scope.StatusInfo.ErrorMessageBottom = status ? null : "Please click the button that says, \"I'm not a robot\", to continue.";
367                    $scope.showCaptcha = !status;
368
369                    if (!status) {
370                        recaptchaOnload();
371                    }
372
373                    return status ? Promise.resolve() : Promise.reject('Wrong Captcha');
374                });
375            }
376
377            return Promise.resolve();
378        };
379
380        var vaidate = function () {
381            $scope.resetPayAtPickup();
382
383            if ($scope.forms.contact) {
384                $scope.$broadcast('form-check-validity');
385
386                if ($scope.forms.contact.$invalid) {
387                    return Promise.reject('Invalid Contact Form');
388                }
389            }
390
391            if ($scope.contactForm && $scope.showContactInfo()) {
392                $scope.contactForm.$setSubmitted();
393                if ($scope.contactForm.$invalid) {
394                    return Promise.reject('Invalid Contact Form');
395                }
396            }
397
398            if ($scope.shippingForm && $scope.isShippingRequired() && !$scope.isPickup()) {
399                $scope.ShippingStatus.Submitted = true;
400                $scope.shippingForm.$setSubmitted();
401
402                if ($scope.shippingForm.$invalid) {
403                    return Promise.reject('Invalid Valid Form');
404                }
405            }
406
407            $scope.BillingAddressVerify();
408
409            if ($scope.billingForm && $scope.isBillingRequired() && !$scope.isPayAtPickup()) {
410                $scope.billingForm.$setSubmitted();
411                if ($scope.billingForm.$invalid) {
412                    return Promise.reject('Invalid Billing Address');
413                }
414            } else if ($scope.billingForm && $scope.isTermsRequired()) {
415                if (!$scope.BillingInfo.TermsAgree) {
416                    $scope.billingForm.$setSubmitted();
417                    return Promise.reject('Invalid Terms Agree');
418                }
419            }
420
421            $scope.Validate();
422
423            $scope.ShippingStatus.MessagesShow = false;
424
425            if (!$scope.ShippingStatus.Valid) {
426                $scope.ShippingStatus.MessagesShow = true;
427
428                return Promise.reject('ShippingStatus is not valid');
429            }
430
431            var contactValid = !$scope.skipBilling() ? $scope.ContactStatus.Valid : true;
432
433            if (!contactValid) {
434                $scope.ContactStatus.MessagesShow = true;
435
436                return Promise.reject('Contact Status is no valid');
437            }
438
439            $scope.SessionUpdate();
440
441            if (!$scope.skipBilling() && ($scope.isResponsive() || $scope.isNewUIFlagSet())) {
442                $scope.ShippingStatus.Entered = true;
443                $scope.BillingEdit();
444
445                // return Promise.reject('Billing Edit');
446            }
447
448            return Promise.resolve();
449        };
450
451        var payment_method_types = {
452            afterpay: 'afterpay_clearpay',
453        };
454
455        if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
456            var prepareCheckout = function () {
457                if (checkoutOptions.taxesVerified) {
458                    if ($scope.HasDynamicTaxes && $scope.isDynamicTaxServiceEnabled()) {
459                        $scope.TotalsCalculate();
460                        $scope.CartVerify();
461                        $scope.BillingVerify();
462                    }
463
464                    return Promise.resolve();
465                }
466
467                return checkCaptcha().then(function () {
468                    return vaidate();
469                });
470            };
471
472            return prepareCheckout()
473                .then(function () {
474                    if (checkoutOptions.taxesVerified) {
475                        return Promise.resolve();
476                    }
477
478                    var freshTaxesPromise = $scope.ensureFreshDynamicTaxesForCheckout();
479
480                    if (!freshTaxesPromise) {
481                        return Promise.resolve();
482                    }
483
484                    $scope.setProcessingCheckout(true);
485
486                    return freshTaxesPromise.then(function () {
487                        if ($scope.hasActiveDynamicTaxFailure()) {
488                            $scope.setProcessingCheckout(false);
489                            return Promise.reject(new Error($scope.StatusInfo.DynamicTaxError || $scope.StatusInfo.DynamicTaxAddressError));
490                        }
491
492                        checkoutOptions.taxesVerified = true;
493                        return prepareCheckout();
494                    });
495                })
496                .then(function () {
497                    $scope.setProcessingCheckout(true);
498                    $scope.makeTaxInclusive();
499
500                    if ($scope.hasActiveDynamicTaxFailure()) {
501                        $scope.setProcessingCheckout(false);
502                        return Promise.reject(new Error($scope.StatusInfo.DynamicTaxError || $scope.StatusInfo.DynamicTaxAddressError));
503                    }
504
505                    return Promise.resolve();
506                })
507                .then(function () {
508                    return $scope.PreProcess($scope.buildCheckoutPreProcessData()).then(function(id) {
509                        if (!id) {
510                            throw new Error('Unable to create transaction');
511                        }
512                    });
513                })
514                .then(function () {
515                    return $scope.SubmitInfoGet($scope.getSubmitInfoGetData(null, [payment_method_types[type] || type]));
516                })
517                .then(function () {
518                    var shippingAddress;
519
520                    if ($scope.isPickup()) {
521                        var optionAddress = $scope.ShippingInfo.Option.address;
522
523                        shippingAddress = {
524                            country: $scope.CountryIso2KeyGet($scope.InitInfo.Countries[optionAddress.country]),
525                            state: optionAddress.state,
526                            city: optionAddress.city,
527                            line1: optionAddress.street1,
528                            line2: optionAddress.street2,
529                            postal_code: optionAddress.postalCode,
530                        };
531                    } else {
532                        shippingAddress = {
533                            country: $scope.CountryIso2KeyGet($scope.ShippingInfo.Country),
534                            state: $scope.ShippingInfo.State,
535                            city: $scope.ShippingInfo.City,
536                            line1: $scope.ShippingInfo.Address1,
537                            line2: $scope.ShippingInfo.Address2,
538                            postal_code: $scope.ShippingInfo.Zip,
539                        };
540                    }
541
542                    var billingName = [$scope.BillingInfo.FirstName, $scope.BillingInfo.LastName].join(' ');
543                    var shippingName = $scope.isPickup() ? billingName : [$scope.ShippingInfo.FirstName, $scope.ShippingInfo.LastName].join(' ');
544
545                    return $scope.stripeBNPLModel.getModel(type).confirmPayment(
546                        $scope.TransactionId,
547                        $scope.Rain.processorToken,
548                        {
549                            name: billingName,
550                            email: $scope.BillingInfo.Email,
551                            phone: $scope.BillingInfo.Phone,
552                            address: {
553                                country: $scope.CountryIso2KeyGet($scope.BillingInfo.Country),
554                                state: $scope.BillingInfo.State,
555                                city: $scope.BillingInfo.City,
556                                line1: $scope.BillingInfo.Address1,
557                                line2: $scope.BillingInfo.Address2,
558                                postal_code: $scope.BillingInfo.Zip,
559                            },
560                        },
561                        {
562                            name: shippingName,
563                            address: shippingAddress,
564                        }
565                    );
566                })
567                .catch(function (e) {
568                    $scope.setProcessingCheckout(false);
569                    $scope.setErrorMessageBottom(e.message || 'Error');
570
571                    $scope.safeApply();
572
573                    console.error(e);
574                });
575        }
576
577        return checkCaptcha()
578            .then(function () {
579                return vaidate();
580            })
581            .then(function () {
582                $scope.setProcessingCheckout(true);
583                $scope.makeTaxInclusive();
584
585                if (!$scope.dynamicTaxesReady) {
586                    $scope.setErrorMessageBottom(null);
587
588                    return $scope.updateDynamicTaxesOnce().then(function () {
589                        if (!$scope.dynamicTaxesReady) {
590                            return Promise.reject(new Error('Tax service is not responding, please wait a while and retry your order again.'));
591                        }
592
593                        return Promise.resolve();
594                    });
595                }
596
597                return Promise.resolve();
598            })
599            .then(function () {
600                return $scope.PreProcess().then(function(id) {
601                    if (!id) {
602                        throw new Error('Unable to create transaction');
603                    }
604                });
605            })
606            .then(function () {
607                return $scope.SubmitInfoGet($scope.getSubmitInfoGetData(null, [payment_method_types[type] || type]));
608            })
609            .then(function () {
610                var shippingAddress;
611
612                if ($scope.isPickup()) {
613                    var optionAddress = $scope.ShippingInfo.Option.address;
614
615                    shippingAddress = {
616                        country: $scope.CountryIso2KeyGet($scope.InitInfo.Countries[optionAddress.country]),
617                        state: optionAddress.state,
618                        city: optionAddress.city,
619                        line1: optionAddress.street1,
620                        line2: optionAddress.street2,
621                        postal_code: optionAddress.postalCode,
622                    };
623                } else {
624                    shippingAddress = {
625                        country: $scope.CountryIso2KeyGet($scope.ShippingInfo.Country),
626                        state: $scope.ShippingInfo.State,
627                        city: $scope.ShippingInfo.City,
628                        line1: $scope.ShippingInfo.Address1,
629                        line2: $scope.ShippingInfo.Address2,
630                        postal_code: $scope.ShippingInfo.Zip,
631                    };
632                }
633
634                var billingName = [$scope.BillingInfo.FirstName, $scope.BillingInfo.LastName].join(' ');
635                var shippingName = $scope.isPickup() ? billingName : [$scope.ShippingInfo.FirstName, $scope.ShippingInfo.LastName].join(' ');
636
637                return $scope.stripeBNPLModel.getModel(type).confirmPayment(
638                    $scope.TransactionId,
639                    $scope.Rain.processorToken,
640                    {
641                        name: billingName,
642                        email: $scope.BillingInfo.Email,
643                        phone: $scope.BillingInfo.Phone,
644                        address: {
645                            country: $scope.CountryIso2KeyGet($scope.BillingInfo.Country),
646                            state: $scope.BillingInfo.State,
647                            city: $scope.BillingInfo.City,
648                            line1: $scope.BillingInfo.Address1,
649                            line2: $scope.BillingInfo.Address2,
650                            postal_code: $scope.BillingInfo.Zip,
651                        },
652                    },
653                    {
654                        name: shippingName,
655                        address: shippingAddress,
656                    }
657                );
658            })
659            .catch(function (e) {
660                $scope.setProcessingCheckout(false);
661                $scope.setErrorMessageBottom(e.message || 'Error');
662
663                $scope.safeApply();
664
665                console.error(e);
666            });
667    };
668
669    // ******************************************************************************************
670    // ****  Initialization
671    // ******************************************************************************************
672
673    $scope.NoOpPromise = function () {
674        var deferred = $q.defer();
675        deferred.resolve();
676        return deferred.promise;
677    };
678
679    /**
680     * @return {number}
681     */
682    $scope.RoundTo2Dec = function (value) {
683
684        return $scope.roundNumber(value, 2);
685    };
686
687    $scope.roundNumber = function (value, precision) {
688        if (!value) {
689            return 0;
690        }
691
692        if (precision <= 0) {
693            precision = 0;
694        } else if (!precision) {
695            precision = 2;
696        }
697
698        if (precision < 10) {
699            var fixDecimalTailMultiplier = Math.pow(10, 10);
700            value = Math.round(value * fixDecimalTailMultiplier) / fixDecimalTailMultiplier;
701        }
702
703        /**
704        This one works correct:
705        > +(Math.round(2.175 + "e+" + 2)  + "e-" + 2)
706        2.18
707        */
708        return +(Math.round(value + "e+" + precision) + "e-" + precision);
709    };
710
711    $scope.Init = function (
712        wsUrl,
713        id,
714        geoLocationCountry,
715        geoLocationRegion,
716        geoLocationZipCode,
717        enhancedShippingAndStorePickupOptions,
718        options
719    ) {
720        $scope.WsUrl = wsUrl;
721        $scope.geoLocationZipCode = geoLocationZipCode;
722
723        if (window.domainDisabled && window.domainDisabled === true) {
724            $scope.domainDisabled = true;
725        }
726        if (window.scheduledPaymentId && +window.scheduledPaymentId > 0) {
727            $scope.scheduledPaymentId = +window.scheduledPaymentId;
728        }
729        if (window.invoiceId && +window.invoiceId > 0) {
730            $scope.invoiceId = +window.invoiceId;
731        }
732        if (window.invoicesCartId) {
733            $scope.invoicesCartId = window.invoicesCartId;
734        }
735        if (window.rtoCartId) {
736            $scope.rtoCartId = window.rtoCartId;
737        }
738        if (window.invoiceAmount && +window.invoiceAmount > 0) {
739            $scope.invoiceAmount = +window.invoiceAmount;
740        }
741        if (window.customerId && +window.customerId > 0) {
742            $scope.customerId = +window.customerId;
743        }
744        if (window.paymentToken) {
745            $scope.paymentToken = window.paymentToken;
746        }
747        if (window.paymentNonce) {
748            $scope.paymentNonce = window.paymentNonce;
749        }
750        $scope.BillingInfo.enrollAutoPay = false;
751        $scope.BillingInfo.addCardOnFile = false;
752        if (window.enrollAutoPay && window.enrollAutoPay == 1) {
753            $scope.BillingInfo.enrollAutoPay = true;
754        }
755        if (window.subscriptionId && +window.subscriptionId > 0) {
756            $scope.subscriptionId = +window.subscriptionId;
757            $scope.BillingInfo.enrollAutoPay = true;
758        }
759        if (window.paymentType) {
760            $scope.paymentType = window.paymentType;
761        }
762        if (window.paymentDueDate) {
763            $scope.paymentDueDate = window.paymentDueDate;
764        }
765        if (window.mmsAmount) {
766            $scope.mmsAmount = window.mmsAmount;
767        }
768        if (window.districts) {
769            $scope.districts = window.districts;
770        }
771        if (window.schools) {
772            $scope.allSchools = window.schools;
773        }
774
775        $scope.isMobileBrowser = window.isMobileBrowser;
776
777        // Internet Explorer: complicating JS since ever.  Used this solution for determining isIE.
778        // https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
779        let isIE = /*@cc_on!@*/false || !!document.documentMode;
780        if (isIE) {
781            for (let option in $scope.defaultOptions) {
782                if ($scope.defaultOptions.hasOwnProperty(option)) {
783                    $scope.options[option] = $scope.defaultOptions[option];
784                }
785            }
786
787            for (let option in options){
788                if(options.hasOwnProperty(option)) {
789                    $scope.options[option] = options[option];
790                }
791            }
792        } else {
793            $scope.options = Object.assign({}, $scope.defaultOptions, options);
794        }
795        $scope.CartId = '';
796        if (id) {
797            $scope.CartId = id;
798        }
799
800        $scope.enhancedShippingAndStorePickupOptions = enhancedShippingAndStorePickupOptions;
801
802        $scope.Class['visa'] = 'card-default';
803        $scope.Class['mastercard'] = 'card-default';
804        $scope.Class['amex'] = 'card-default';
805        $scope.Class['discover'] = 'card-default';
806
807        // Workaround for Safari bug causing selection of shipping calc zip code on focus to fail
808        try {
809            $("#shipping_calc_zip").mouseup(function (e) {
810                e.preventDefault();
811            });
812        } catch (err) {
813            // Consume and continue
814        }
815
816        $scope.ApiUrl = '/public/api';
817        if (wsUrl) {
818            $scope.ApiUrl = wsUrl + $scope.ApiUrl;
819        }
820        $scope.StatusInfo.ErrorMessage = getQueryString('error_message');
821        $scope.ExpirationOptionsGen();
822
823        return $scope.InitInfoGet().then(function () {
824            if (geoLocationCountry && geoLocationRegion && (geoLocationCountry === 'US') && ($scope.InitInfo.DomesticCountry == geoLocationCountry || $scope.InitInfo.InternationalEnabled == true)) {
825                $scope.shippingCalcState = $scope.stateNameToAbbr(geoLocationRegion);
826            }
827            if (geoLocationZipCode) {
828                $scope.shippingCalcZipCode = geoLocationZipCode;
829            }
830
831            if ($scope.checkFlagRetailDeliveryFee() && !$scope.COLORADO_RETAIL_DELIVERY_FEE && $scope.InitInfo.COLORADO_RETAIL_DELIVERY_FEE) {
832                $scope.COLORADO_RETAIL_DELIVERY_FEE = $scope.InitInfo.COLORADO_RETAIL_DELIVERY_FEE;
833            }
834
835            $scope.setCustomerInfo();
836            $scope.setWholesaleUserInfo();
837
838            if ($scope.checkFlagCarePlans()) {
839                $scope.registerCarePlanHandleEvent();
840            }
841
842            return $scope.CartDataGet().then(function () {
843                // Prefill shipping and customer information if we have subscription in cart
844                if ($scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
845                    var subscriptionItem = null;
846                    $scope.updateDigitalOrder();
847                    $scope.CartData.Items.forEach(function (item) {
848                        if (!subscriptionItem && 'subscription' === item.type) {
849                            subscriptionItem = item;
850                        }
851                    });
852
853                    if (subscriptionItem && subscriptionItem.notes) {
854                        $scope.ShippingInfo.FirstName = subscriptionItem.notes.firstName;
855                        $scope.ShippingInfo.LastName = subscriptionItem.notes.lastName;
856                        $scope.ShippingInfo.Address1 = subscriptionItem.notes.streetAddress1;
857                        $scope.ShippingInfo.Address2 = subscriptionItem.notes.streetAddress2;
858                        $scope.ShippingInfo.City = subscriptionItem.notes.city;
859                        $scope.ShippingInfo.State = subscriptionItem.notes.stateCode
860                            ? subscriptionItem.notes.stateCode
861                            : subscriptionItem.notes.state;
862                        $scope.ShippingInfo.Zip = subscriptionItem.notes.postalCode;
863                        $scope.ShippingInfo.Country = subscriptionItem.notes.countryCode;
864                        $scope.BillingInfo.Email = subscriptionItem.notes.email;
865                        $scope.BillingInfo.Phone = subscriptionItem.notes.phone;
866                        if (!subscriptionItem.notes.state.length) {
867                            if (!$scope.ShippingInfo.Option) {
868                                $scope.ShippingInfo.Option = {};
869                            }
870                            if (!$scope.ShippingInfo.Options) {
871                                $scope.ShippingInfo.Options = [];
872                            }
873                            $scope.ShippingInfo.Option.name = 'Store Pickup';
874                            if ($scope.ShippingInfo.Option.id) {
875                                $scope.ShippingInfo.Options[$scope.ShippingInfo.Option.id] = $scope.ShippingInfo.Option;
876                            }
877                        }
878
879                        $scope.BillingAddressVerify();
880                    }
881                }
882                $scope.StatusInfo.Initializing = false;
883
884                var shippingOptionsInit = function () {
885                    return $scope.ShippingOptionsGet().then(function () {
886                        $scope.setShippingOption();
887                    }).catch(function (e) {
888                        // Ignore dynamic tax errors on init step
889                        console.error(e);
890                    });
891                };
892
893                return shippingOptionsInit();
894            });
895        }).then(function () {
896            var paymentRequestModel = $scope.getPaymentRequestModel();
897            if (!paymentRequestModel) {
898                return Promise.resolve();
899            }
900
901            return paymentRequestModel.init();
902        }).then(function () {
903            if ($scope.isCardProcessorRain() && window.StripeBNPLModel && $scope.Rain.stripe && $scope.InitInfo && $scope.InitInfo.payLaterRedirectURL) {
904                $scope.stripeBNPLModel = new StripeBNPLModel(
905                    $scope.Rain.stripe,
906                    $scope.InitInfo.payLaterRedirectURL
907                );
908                $scope.updateBNPLMessage();
909            }
910        }).then(function () {
911            if (typeof gtag === 'undefined') {
912                $($scope.InitInfo.googleAnalyticsCode).appendTo(document.head);
913            }
914        });
915    };
916
917    $scope.BuildUrl = function (path, noCart, query) {
918        if (typeof noCart === 'undefined') {
919            noCart = false;
920        }
921
922        path = $scope.ApiUrl + path + (!noCart ? '/' : '');
923
924        if (!noCart) {
925            path += $scope.CartId;
926        }
927
928        if (query) {
929            path += "?" + query;
930        }
931
932        return path;
933    };
934
935    $scope.getDataWithDelay = function (callback) {
936        var timeToWaitForLoadData = 100;
937
938        window.localStorage.removeItem('loading-interrupted');
939
940        return new Promise((resolve, reject) => {
941            setTimeout(() => {
942                resolve(callback());
943            }, timeToWaitForLoadData);
944        });
945    };
946
947    $scope.isCarePlanEnable = function () {
948        return $scope.knobby && $scope.knobby.module_care_plans;
949    };
950
951    $scope.InitInfoGet = function () {
952        var query = null;
953        var noCart = false;
954
955        if ($scope.isScheduledPayment()) {
956            query = 'spid=' + $scope.scheduledPaymentId + '&token=' + $scope.paymentToken;
957        }
958
959        if ($scope.isSubscriptionPayment()) {
960            query = 'sid=' + $scope.subscriptionId + '&token=' + $scope.paymentToken;
961        }
962
963        if ($scope.isInvoicePayment()) {
964            query = 'iid=' + $scope.invoiceId + '&token=' + $scope.paymentToken;
965
966            noCart = true;
967        }
968
969        if ($scope.isInvoicesPayment()) {
970            query = 'icid=' + $scope.invoicesCartId + '&token=' + $scope.paymentToken;
971        }
972
973        if ($scope.isRtoPayment()) {
974            query = 'rpid=' + $scope.rtoCartId + '&token=' + $scope.paymentToken;
975        }
976
977        if ($scope.isAddCardOnly()) {
978            query = 'cid=' + $scope.customerId + '&token=' + $scope.paymentToken;
979        }
980
981        if ($scope.isMmsPayment()) {
982            query = 'cid=' + $scope.customerId + '&date=' + $scope.paymentDueDate + '&amount=' + $scope.mmsAmount + '&token=' + $scope.paymentToken;
983        }
984
985        var options = {withCredentials: true};
986
987        if (window.cartJWT) {
988            options.headers = {
989                Authorization: 'Bearer ' + window.cartJWT
990            };
991        }
992
993        var requestCard = () => {
994            return $http.get($scope.BuildUrl('/checkout/initinfo', noCart, query), options);
995        };
996
997        if (window.localStorage.getItem('loading-interrupted')) {
998            requestCard = () => {
999                return $scope.getDataWithDelay(() => {
1000                    return $http.get($scope.BuildUrl('/checkout/initinfo', noCart, query), options);
1001                });
1002            };
1003        }
1004
1005        return requestCard().then(function (result) {
1006            if (result.data.initinfo) {
1007                $scope.InitInfo = result.data.initinfo;
1008
1009                $scope.RainApiUrl = $scope.InitInfo['RainApiUrl'];
1010                $scope.isTestStore = $scope.InitInfo['isTestStore'];
1011
1012                // Default Country
1013                var defaultCountry = $scope.ShippingInfo.Country;
1014                $scope.ShippingCountrySet(defaultCountry);
1015
1016                // Custom header
1017                if ($scope.InitInfo.CheckoutSettings.HeaderType === 'Custom' &&  $scope.InitInfo.CheckoutSettings.LogoHtml) {
1018                    $('#customheader').html($scope.InitInfo.CheckoutSettings.LogoHtml);
1019                }
1020
1021                if ($scope.InitInfo.CurrencySymbol3) {
1022                    $scope.InitInfo.CurrencySymbol2 = JSON.parse($scope.InitInfo.CurrencySymbol3);
1023                }
1024
1025                $scope.knobby = $scope.InitInfo.knobby;
1026                $scope.tax_service = $scope.InitInfo.tax_service;
1027
1028                if ($scope.isSavedCardsFlagEnabled() && $scope.customerId) {
1029                    $scope.customerCards = Object.values($scope.InitInfo.customerCards || {});
1030                }
1031
1032                if ($scope.isWebPayment()) {
1033                    var processor = $scope.getCCProcessor();
1034                    if (processor) {
1035                        $scope.setProcessor(processor);
1036                    }
1037                    if (!$scope.isWebPaymentDue() && !$scope.isAddCardOnly()) {
1038                        $scope.setErrorMessageTop("This payment is paid - nothing due.");
1039                    }
1040                }
1041
1042                $scope.initRainProcessing();
1043                $scope.initAdyen();
1044                $scope.updateProcessor();
1045            }
1046        });
1047    };
1048
1049    $scope.getStoreUuid = function() {
1050        if ($scope.InitInfo && $scope.InitInfo.Store && $scope.InitInfo.Store.uuid) {
1051            return $scope.InitInfo && $scope.InitInfo.Store.uuid;
1052        }
1053        return '';
1054    };
1055
1056    $scope.getStoreCustomerServicePhone = function() {
1057        if ($scope.InitInfo && $scope.InitInfo.Store && $scope.InitInfo.Store) {
1058            return $scope.InitInfo && $scope.InitInfo.Store.phone;
1059        }
1060        return '';
1061    };
1062
1063    $scope.clearSavedCardSelection = function () {
1064        if (!$scope.formData?.selectedCustomerCardId) {
1065            return;
1066        }
1067
1068        $scope.formData.selectedCustomerCardId = null;
1069
1070        $scope.safeApply();
1071    };
1072
1073    $scope.initRainProcessing = function() {
1074        if ($scope.isCardProcessorRain() && $scope.InitInfo.Processors.cc_pk && typeof Stripe === 'function') {
1075            var stripeOptions = {betas: ['affirm_pilot_beta_1']};
1076            $scope.Rain.stripe = Stripe($scope.InitInfo.Processors.cc_pk, stripeOptions);
1077            var elements = $scope.Rain.stripe.elements();
1078
1079            $scope.Rain.card = elements.create('cardNumber', $scope.getRainCardStyles('card'));
1080            $scope.Rain.card.mount('#card-number');
1081            $scope.Rain.card.on('change', $scope.checkRainPaymentError);
1082
1083            if ($scope.isSavedCardsFlagEnabled()) {
1084                $scope.Rain.card.on('focus', $scope.clearSavedCardSelection);
1085            }
1086
1087            $scope.Rain.exp = elements.create('cardExpiry', $scope.getRainCardStyles());
1088            $scope.Rain.exp.mount('#card-exp');
1089            $scope.Rain.exp.on('change', $scope.checkRainPaymentError);
1090
1091            if ($scope.isSavedCardsFlagEnabled()) {
1092                $scope.Rain.exp.on('focus', $scope.clearSavedCardSelection);
1093            }
1094
1095            $scope.Rain.cvv = elements.create('cardCvc', $scope.getRainCardStyles());
1096            $scope.Rain.cvv.mount('#card-cvv');
1097            $scope.Rain.cvv.on('change', $scope.checkRainPaymentError);
1098
1099            if ($scope.isSavedCardsFlagEnabled()) {
1100                $scope.Rain.cvv.on('focus', $scope.clearSavedCardSelection);
1101            }
1102        }
1103    };
1104
1105    $scope.initAdyen = function() {
1106        let that = this;
1107        if (!$scope.checkFlagAdyenRegionFailover()) {
1108            if ($scope.isCardProcessorQuiltAdyen() && typeof AdyenCheckout === 'function') {
1109                let currency = window.currency_code;
1110                let clientKey = window.adyen_env ? 'live_C6KR4G2VQRAZPBUNDS7F7RTAZEWWRW27' : 'test_NLY6QP2DNZCW7BI3SNKBTJU6LUWEXBRP';
1111                if (currency === 'CAD') {
1112                    clientKey = window.adyen_env ? 'live_HWN53RBGFVASTJKFT2WPBJYKOARRXWTX' : 'test_ZV6G5SOS6JFNRAAAGNJ65JDDFUREPTAA';
1113                }
1114
1115                const configuration = {
1116                    environment: window.adyen_env ? 'live-us' : 'test',
1117                    clientKey: clientKey,
1118                    locale: 'en_US',
1119                    onChange: that.handleAdyenChange,
1120                    onError: (error, component) => {
1121                        console.error(error.name, error.message, error.stack, component);
1122                        $scope.logInvalidOriginError(error, 'initAdyen()');
1123                    }
1124                };
1125
1126                AdyenCheckout(configuration).then(checkout => {
1127                    that.checkout = checkout.create('securedfields', {
1128                        // Optional configuration
1129                        type: 'card',
1130                        brands: ['mc', 'visa', 'amex', 'discover'],
1131                        styles: {
1132                            error: {
1133                                color: 'red'
1134                            },
1135                            validated: {
1136                                color: 'green'
1137                            }
1138                        },
1139                        onBrand: data => {
1140                            that.cardBrand = data.brand !== 'card' ? data.brand : '';
1141                            that.cardImage = data.brandImageUrl;
1142                        },
1143                        onFocus: () => {
1144                            if (!$scope.isSavedCardsFlagEnabled()) {
1145                                return;
1146                            }
1147
1148                            if ($scope.paymentMode !== 'manual') {
1149                                $scope.paymentMode = 'manual';
1150                                $scope.formData.selectedCustomerCardId = null;
1151
1152                                $scope.safeApply();
1153                            }
1154                        },
1155                        onError: data => {
1156                            console.log('error');
1157                            console.log(data);
1158                        },
1159                    }).mount('#adyen-container');
1160                });
1161            }
1162        } else {
1163            if ($scope.isCardProcessorQuiltAdyen()) {
1164                let currency = window.currency_code;
1165                let clientKey = window.adyen_env ? 'live_C6KR4G2VQRAZPBUNDS7F7RTAZEWWRW27' : 'test_NLY6QP2DNZCW7BI3SNKBTJU6LUWEXBRP';
1166                if (currency === 'CAD') {
1167                    clientKey = window.adyen_env ? 'live_HWN53RBGFVASTJKFT2WPBJYKOARRXWTX' : 'test_ZV6G5SOS6JFNRAAAGNJ65JDDFUREPTAA';
1168                }
1169
1170                let testEnv = !window.adyen_env;
1171                let paymentEnv = testEnv ? 'test' : window.adyenRegion || 'live-us';
1172
1173                const configuration = {
1174                    environment: paymentEnv,
1175                    clientKey: clientKey,
1176                    locale: 'en_US',
1177                    onChange: that.handleAdyenChange,
1178                    onError: (error, component) => {
1179                        console.error(error.name, error.message, error.stack, component);
1180                        $scope.logInvalidOriginError(error, 'initAdyen()');
1181                    }
1182                };
1183
1184                var intervalId = setInterval(waitAdyen, 20);
1185                function waitAdyen() {
1186                    if (window.AdyenCheckout && window.adyenStyleLoaded) {
1187                        clearInterval(intervalId);
1188                        initalializeAdyen();
1189                    }
1190                }
1191
1192                function initalializeAdyen() {
1193                    AdyenCheckout(configuration).then(checkout => {
1194                        that.checkout = checkout.create('securedfields', {
1195                            // Optional configuration
1196                            type: 'card',
1197                            brands: ['mc', 'visa', 'amex', 'discover'],
1198                            styles: {
1199                                error: {
1200                                    color: 'red'
1201                                },
1202                                validated: {
1203                                    color: 'green'
1204                                }
1205                            },
1206                            onBrand: data => {
1207                                that.cardBrand = data.brand !== 'card' ? data.brand : '';
1208                                that.cardImage = data.brandImageUrl;
1209                            },
1210                            onFocus: () => {
1211                                if (!$scope.isSavedCardsFlagEnabled()) {
1212                                    return;
1213                                }
1214
1215                                if ($scope.paymentMode !== 'manual') {
1216                                    $scope.paymentMode = 'manual';
1217                                    $scope.formData.selectedCustomerCardId = null;
1218
1219                                    $scope.safeApply();
1220                                }
1221                            },
1222                            onError: data => {
1223                                console.log('error');
1224                                console.log(data);
1225                            },
1226                        }).mount('#adyen-container');
1227                    });
1228                }
1229            }
1230        }
1231    };
1232
1233    $scope.handleAdyenChange = function(state) {
1234        console.log('Adyen State: ', state);
1235        $scope.Adyen.state = state;
1236    };
1237
1238    $scope.isAdyenPaymentValid = function() {
1239        if ($scope.isSavedCardsFlagEnabled() && $scope.paymentMode === 'saved' && $scope.formData?.selectedCustomerCardId) {
1240            return true;
1241        }
1242
1243        return !!($scope.Adyen.state && $scope.Adyen.state.isValid);
1244    };
1245
1246    $scope.getAdyenError = function() {
1247        if ($scope.isSavedCardsFlagEnabled() && $scope.paymentMode === 'saved') {
1248            if (!$scope.formData?.selectedCustomerCardId) {
1249                return 'Please select a saved card.';
1250            }
1251
1252            return null;
1253        }
1254
1255        if (!$scope.isAdyenPaymentValid()) {
1256            if ($scope.Adyen.state && $scope.Adyen.state.valid) {
1257                if (!$scope.Adyen.state.valid.encryptedCardNumber) {
1258                    return "Please enter a valid credit card number.";
1259                }
1260                if (!$scope.Adyen.state.valid.encryptedExpiryMonth || !$scope.Adyen.state.valid.encryptedExpiryYear) {
1261                    return "Please enter a valid credit card expiration date.";
1262                }
1263                if (!$scope.Adyen.state.valid.encryptedSecurityCode) {
1264                    return "Please enter a valid credit card CVV code.";
1265                }
1266            }
1267            return "Please complete the credit information before placing your order.";
1268        }
1269    };
1270
1271    $scope.showOnlyHighestDiscountWarning = function() {
1272        let allCartItemsAreOnSale = false;
1273        if ($scope.CartData.Items && $scope.CartData.Items.length) {
1274            allCartItemsAreOnSale = $scope.CartData.Items.every(function(element) {
1275                return element?.on_sale === true;
1276            });
1277        }
1278
1279        return $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving' &&
1280            !($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && allCartItemsAreOnSale);
1281    };
1282
1283    $scope.showDontApplyOnSaleItemsWarning = function() {
1284        return $scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true;
1285    };
1286
1287    $scope.isApplyHighestSavingCouponsMode = function() {
1288        return $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving';
1289    };
1290
1291    $scope.updateBNPLMessage = function() {
1292        if (!$scope.isAffirmEnabled()) {
1293            return;
1294        }
1295
1296        var amountDue = $scope.getAmountDue();
1297        var amountDueInCents = 0;
1298        if (typeof amountDue === 'number' && !isNaN(amountDue)) {
1299            amountDueInCents = parseInt((amountDue * 100).toFixed(0));
1300        }
1301
1302        if (amountDueInCents < 5000) {
1303            return;
1304        }
1305
1306        var currency = 'USD';
1307        if ($scope.InitInfo && $scope.InitInfo.Currency && typeof $scope.InitInfo.Currency === 'string') {
1308            currency = $scope.InitInfo.Currency.toUpperCase();
1309        }
1310
1311        if (currency !== 'USD') {
1312            return;
1313        }
1314
1315        var elements = $scope.Rain.stripe.elements();
1316        $scope.Rain.affirmMessage = elements.create('affirmMessage', {
1317            amount: amountDueInCents,
1318            currency: currency
1319        });
1320
1321        $scope.Rain.affirmMessage.mount('#affirm-message');
1322    };
1323
1324    $scope.rainProcessingPrepareShippingAddress = function () {
1325        var shippingAddress = $scope.isShippingRequired() ? $scope.ShippingInfo : $scope.BillingInfo;
1326
1327        var shipping = {
1328            name: shippingAddress.FirstName + ' ' + shippingAddress.LastName,
1329            address: {
1330                line1: shippingAddress.Address1,
1331                line2: shippingAddress.Address2,
1332                city: shippingAddress.City,
1333                state: shippingAddress.State,
1334                postal_code: shippingAddress.Zip,
1335            }
1336        };
1337
1338        var shippingCountry = $scope.getISO2digitCountryCode(shippingAddress.Country);
1339
1340        if (shippingCountry) {
1341            shipping.address.country = shippingCountry;
1342        }
1343
1344        return shipping;
1345    };
1346
1347    $scope.rainProcessingPrepareBillingAddress = function () {
1348        if (!$scope.checkFlagAvsEcommerce()) {
1349            var billingCountry = $scope.getISO2digitCountryCode($scope.BillingInfo.Country);
1350        } else {
1351            var billingCountry = $scope.getIso2CountryCodeFromIso3($scope.BillingInfo.CountryKey) || null;
1352        }
1353
1354        var billing = {
1355            name: $scope.BillingInfo.FirstName + ' ' + $scope.BillingInfo.LastName,
1356            address: {
1357                line1: $scope.BillingInfo.Address1,
1358                line2: $scope.BillingInfo.Address2,
1359                city: $scope.BillingInfo.City,
1360                state: $scope.BillingInfo.State,
1361                postal_code: $scope.BillingInfo.Zip,
1362            }
1363        };
1364
1365        if ($scope.checkFlagAvsEcommerce()) {
1366            billing.email = $scope.BillingInfo.Email;
1367            billing.phone = $scope.BillingInfo.Phone;
1368        }
1369
1370        // Country must be ISO 2 digit
1371        if (billingCountry) {
1372            billing.address.country = billingCountry;
1373        }
1374
1375        return billing;
1376
1377    };
1378
1379    $scope.quiltAdyenPrepareBillingAddress = function () {
1380        let billing = {
1381            zip: $scope.BillingInfo.Zip,
1382            first_name: $scope.BillingInfo.FirstName,
1383            last_name: $scope.BillingInfo.LastName,
1384            address_1: $scope.BillingInfo.Address1,
1385            address_2: $scope.BillingInfo.Address2,
1386            city: $scope.BillingInfo.City,
1387            phone: $scope.BillingInfo.Phone,
1388            email: $scope.BillingInfo.Email
1389        };
1390
1391        let two_letter_country = $scope.getIso2CountryCodeFromIso3($scope.BillingInfo.CountryKey);
1392
1393        if (two_letter_country) {
1394            billing.two_letter_country = two_letter_country;
1395
1396            if (two_letter_country == "US") {
1397                billing.two_letter_state = $scope.BillingInfo.State;
1398            }
1399        }
1400
1401        return billing;
1402    };
1403
1404    $scope.quiltAdyenPrepareShippingAddress = function () {
1405        var shippingInfo = $scope.getEffectiveShippingInfo();
1406
1407        let shipping = {
1408            zip: shippingInfo.Zip,
1409            first_name: shippingInfo.FirstName,
1410            last_name: shippingInfo.LastName,
1411            address_1: shippingInfo.Address1,
1412            address_2: shippingInfo.Address2,
1413            city: shippingInfo.City
1414        };
1415
1416        let two_letter_country = $scope.getIso2CountryCodeFromIso3(shippingInfo.CountryKey);
1417
1418        if (two_letter_country) {
1419            shipping.two_letter_country = two_letter_country;
1420
1421            if (two_letter_country == "US") {
1422                shipping.two_letter_state = shippingInfo.State;
1423            }
1424        }
1425
1426        return shipping;
1427    };
1428
1429    $scope.quiltAdyenPrepareShopperDetails = function () {
1430        return {
1431            //device_type: navigator.platform,
1432            browser_info: {
1433                userAgent: navigator.userAgent,
1434                language: navigator.language || navigator.userLanguage,
1435                timeZoneOffset: new Date().getTimezoneOffset()
1436            }
1437        }
1438    };
1439
1440    $scope.getProcessRainPaymentData = function () {
1441        const isSavedCard =
1442            $scope.isSavedCardsFlagEnabled() &&
1443            $scope.paymentMode === 'saved' &&
1444            $scope.formData?.selectedCustomerCardId;
1445
1446        const data = {};
1447
1448        if (isSavedCard) {
1449            const card = $scope.getCard();
1450
1451            if (card?.gateway_token) {
1452                data.payment_method = card.gateway_token;
1453            }
1454        } else {
1455            data.payment_method = {
1456                card: $scope.Rain.card,
1457                billing_details: $scope.rainProcessingPrepareBillingAddress(),
1458            };
1459        }
1460
1461        if ($scope.getAmountDue() !== 0 && !$scope.isCardOnFileNeeded() && !$scope.isPickup()) {
1462            data.shipping = $scope.rainProcessingPrepareShippingAddress();
1463        }
1464
1465        return data;
1466    };
1467
1468    $scope.processRainPayment = function(data) {
1469        if (typeof data === 'undefined') {
1470            data = $scope.getProcessRainPaymentData();
1471        }
1472
1473        if ($scope.getAmountDue() === 0 && $scope.isCardOnFileNeeded()) {
1474            return $scope.Rain.stripe.confirmCardSetup($scope.Rain.processorToken, data).then(function(result) {
1475                return $scope.handleIntentResult(result);
1476            });
1477        } else {
1478            return $scope.Rain.stripe.confirmCardPayment($scope.Rain.processorToken, data).then(function(result) {
1479                return $scope.handleIntentResult(result);
1480            });
1481        }
1482    };
1483
1484    $scope.handleIntentResult = function(result) {
1485        var context = {};
1486        context.source = 'checkoutCtrl.js';
1487        context.details = {};
1488        context.store_uuid = $scope.InitInfo.Store.uuid;
1489        context.store_name = $scope.InitInfo.Store.name;
1490        context.details.function = 'confirmCardPayment()';
1491        Logger.setApiUrl($scope.InitInfo.RainApiUrl);
1492        Logger.setToken($scope.InitInfo.store_token);
1493
1494        if (result.error) {
1495            $scope.StatusInfo.ErrorMessageBottom = result.error.message;
1496            Logger.log('Info', 'Rain Method Error', context);
1497            return 'error';
1498        } else if (result.paymentIntent && (result.paymentIntent.status === 'succeeded' || result.paymentIntent.status === 'requires_capture')) {
1499            $scope.Rain.PaymentMethodId = result.paymentIntent.payment_method;
1500            Logger.log('Info', 'Rain Method Success', context);
1501            return 'success';
1502        } else if (result.setupIntent && (result.setupIntent.status === 'succeeded' || result.setupIntent.status === 'requires_capture')) {
1503            $scope.Rain.PaymentMethodId = result.setupIntent.payment_method;
1504            Logger.log('Info', 'Rain Method Success', context);
1505            return 'success';
1506        } else {
1507            // Not sure if this is even possible
1508            $scope.StatusInfo.ErrorMessageBottom = "(CH105) An unexpected error occured, please try again.";
1509            Logger.log('Info', 'Rain Method Failed', context);
1510            return 'failed';
1511        }
1512    };
1513
1514    $scope.checkRainPaymentError = function(event) {
1515        if (event.error) {
1516            $scope.Rain.error = event.error;
1517            console.log({type:'Rain Error', message:event.error.message});
1518        } else {
1519            $scope.Rain.error = null;
1520            $scope.Rain[event.elementType] = event.complete;
1521        }
1522    };
1523
1524    $scope.isRainPaymentValid = function() {
1525        if ($scope.isSavedCardsFlagEnabled() && $scope.formData?.selectedCustomerCardId) {
1526            return true;
1527        }
1528
1529        if ($scope.Rain.cardNumber && $scope.Rain.cardExpiry && $scope.Rain.cardCvc) {
1530            return true;
1531        }
1532        return false;
1533    };
1534
1535    $scope.getRainCardStyles = function(elementType) {
1536        var styles = {
1537            style: {
1538                base: {
1539                    color: "#565656",
1540                    fontFamily: '"Roboto", sans-serif',
1541                    fontSmoothing: "antialiased",
1542                    fontSize: "16px",
1543                    "::placeholder": {
1544                        color: "#bebebe",
1545                        fontWeight: 300
1546                    }
1547                },
1548                invalid: {
1549                    color: "#fa755a",
1550                    iconColor: "#fa755a"
1551                }
1552            },
1553        };
1554        if (elementType === 'card') {
1555            styles.showIcon = true;
1556        }
1557        return styles;
1558    };
1559
1560    $scope.setProcessingCheckout = function(processing) {
1561        $scope.StatusInfo.processingCheckout = processing;
1562        $scope.setProcessing(processing);
1563    };
1564
1565    $scope.setProcessing = function(processing) {
1566        $scope.StatusInfo.processing = processing;
1567        $scope.Processing = processing;
1568    };
1569
1570    $scope.isProcessing = function() {
1571        return $scope.StatusInfo.processing || $scope.Processing || $scope.StatusInfo.processingCheckout;
1572    };
1573
1574    $scope.clearErrorMessage = function()
1575    {
1576        $scope.StatusInfo.ErrorMessage = null;
1577        $scope.StatusInfo.ErrorMessageBottom = null;
1578    };
1579
1580    $scope.setErrorMessageTop = function(message) {
1581        $scope.StatusInfo.ErrorMessage = message;
1582    };
1583
1584    $scope.setErrorMessageBottom = function(message) {
1585        if (!$scope.StatusInfo.ErrorMessageBottom && message) {
1586            if ($scope.isResponsive()) {
1587                $scope.StatusInfo.ErrorMessageBottom = message;
1588            } else {
1589                $scope.StatusInfo.ErrorMessage = message;
1590            }
1591        }
1592        // TODO: Stack error messages
1593    }
1594
1595    $scope.stateNameToAbbr = function (stateName) {
1596        try {
1597            var stateData = $scope.InitInfo.Regions['USA'];
1598
1599            var names = Object.values(stateData);
1600            var index = names.indexOf(stateName);
1601            if (index >= 0) {
1602                var abbrs = Object.keys(stateData);
1603                return abbrs[index];
1604            }
1605            return '';
1606        } catch (err) {
1607            // Consume, continue
1608            return '';
1609        }
1610    };
1611
1612    $scope.isScheduledPayment = function() {
1613        return ($scope.scheduledPaymentId > 0 && $scope.paymentToken && $scope.paymentToken.length > 0);
1614    };
1615
1616    $scope.isEnrollAutoPay = function() {
1617        return ($scope.isScheduledPayment() && $scope.BillingInfo && $scope.BillingInfo.enrollAutoPay);
1618    };
1619
1620    $scope.isSubscriptionPayment = function() {
1621        return ($scope.subscriptionId > 0 && $scope.paymentToken && $scope.paymentToken.length > 0);
1622    };
1623
1624    $scope.isInvoicePayment = function() {
1625        return ($scope.invoiceId > 0 && $scope.paymentToken && $scope.paymentToken.length > 0);
1626    };
1627
1628    $scope.isInvoicesPayment = function() {
1629        return ($scope.invoicesCartId && $scope.paymentToken && $scope.paymentToken.length > 0);
1630    };
1631
1632    $scope.isRtoPayment = function () {
1633        return $scope.rtoCartId && $scope.paymentToken && $scope.paymentToken.length > 0;
1634    };
1635
1636    $scope.isMmsPayment = function() {
1637        return $scope.customerId && $scope.paymentDueDate && $scope.mmsAmount && $scope.paymentToken && $scope.paymentToken.length > 0 && $scope.paymentType === 'MMS';
1638    };
1639
1640    $scope.isAddCardOnly = function() {
1641        return ($scope.customerId > 0 && $scope.paymentToken && $scope.paymentToken.length > 0 && $scope.paymentType === 'AddCard');
1642    };
1643
1644    $scope.isWebPayment = function() {
1645        return ($scope.isScheduledPayment() || $scope.isSubscriptionPayment() || $scope.isInvoicePayment() || $scope.isInvoicesPayment() || $scope.isAddCardOnly() || $scope.isMmsPayment() || $scope.isRtoPayment());
1646    };
1647
1648    $scope.isWebPaymentDue = function() {
1649        if ($scope.isWebPayment() && !$scope.isAddCardOnly()) {
1650            return ($scope.InitInfo && $scope.InitInfo.payment_info && +$scope.InitInfo.payment_info.total_due > 0);
1651        }
1652        return false;
1653    };
1654
1655    $scope.checkFlagBugClassesCantCheckOutEcommerce = function () {
1656        return $scope.knobby && $scope.knobby.module_bug_classes_cant_check_out_ecommerce;
1657    }
1658
1659    $scope.checkFlagShowCouponErrorMessageOnCheckoutPage = function () {
1660        return $scope.knobby && $scope.knobby.module_show_coupon_error_message_on_checkout_page;
1661    }
1662
1663    $scope.checkFlagAdyenRegionFailover = function() {
1664        return $scope.knobby && $scope.knobby.module_adyen_region_failover;
1665    }
1666
1667    $scope.checkFlagLogInvalidOriginError = function() {
1668        return $scope.knobby && $scope.knobby.module_log_invalid_origin_error;
1669    }
1670
1671    $scope.logInvalidOriginError = function(error, methodName) {
1672        if (!$scope.checkFlagLogInvalidOriginError()) {
1673            return;
1674        }
1675
1676        var name = (error && error.name) || '';
1677        var message = (error && error.message) || '';
1678
1679        if ((name + ' ' + message).toLowerCase().indexOf('origin') === -1) {
1680            return;
1681        }
1682
1683        if (typeof Logger === 'undefined') {
1684            return;
1685        }
1686
1687        var context = {};
1688        context.source = 'checkoutCtrl.js';
1689        context.details = {};
1690        context.store_uuid = $scope.InitInfo.Store.uuid;
1691        context.store_name = $scope.InitInfo.Store.name;
1692        context.details.function = methodName;
1693        context.details.origin = window.location.origin;
1694        context.details.hostname = window.location.hostname;
1695        context.details.errorName = name;
1696        context.details.errorMessage = message;
1697        Logger.setApiUrl($scope.InitInfo.RainApiUrl);
1698        Logger.setToken($scope.InitInfo.store_token);
1699        Logger.log('Error', 'Manual card entry invalid origin', context);
1700    }
1701
1702    $scope.checkFlagBlockCheckoutOnDynamicTaxFailure = function () {
1703        return $scope.knobby && $scope.knobby.module_block_checkout_on_dynamic_tax_failure;
1704    };
1705
1706    $scope.checkFlagBlockCheckoutButtonWhileProcessing = function () {
1707        return $scope.knobby && $scope.knobby.module_block_checkout_button_while_processing;
1708    };
1709
1710    $scope.hasActiveDynamicTaxFailure = function () {
1711        return !!$scope.StatusInfo.DynamicTaxError || !!$scope.StatusInfo.DynamicTaxAddressError;
1712    };
1713
1714    $scope.buildCheckoutPreProcessData = function () {
1715        if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()
1716            && $scope.HasDynamicTaxes
1717            && $scope.isDynamicTaxServiceEnabled()) {
1718            $scope.TotalsCalculate();
1719        }
1720
1721        return $scope.getPreProcessData();
1722    };
1723
1724    $scope.ensureFreshDynamicTaxesForCheckout = function () {
1725        if (!$scope.checkFlagBlockCheckoutOnDynamicTaxFailure()
1726            || !$scope.HasDynamicTaxes
1727            || !$scope.isDynamicTaxServiceEnabled()) {
1728            return null;
1729        }
1730
1731        var startFreshRequest = function () {
1732            $scope.dynamicTaxRequestId = ($scope.dynamicTaxRequestId || 0) + 1;
1733            $scope.invalidateDynamicTaxCache();
1734            $scope.dynamicTaxesPromiseAccumulator = null;
1735
1736            return $scope.updateDynamicTaxesOnce();
1737        };
1738
1739        var inFlightPromise = $scope.dynamicTaxesInFlightPromise;
1740
1741        if (!inFlightPromise) {
1742            return startFreshRequest();
1743        }
1744
1745        return inFlightPromise.catch(function () {}).then(function () {
1746            return startFreshRequest();
1747        });
1748    };
1749
1750    $scope.calculateWaTaxesBasedOnStoreLocationAddress = function () {
1751        if (
1752            !$scope.InitInfo?.useWashingtonTax
1753            || ($scope.ShippingInfo && $scope.ShippingInfo.Zip)
1754        ) {
1755            return false;
1756        }
1757
1758        var items = ($scope.CartData && $scope.CartData.Items) || [];
1759        var hasOnlyClassItem = items.every(function (item) {
1760            return item && item.type === 'class';
1761        });
1762
1763        return !!hasOnlyClassItem;
1764    }
1765
1766    $scope.tryRequestDynamicTaxesForClasses = function () {
1767        if (!$scope.calculateWaTaxesBasedOnStoreLocationAddress()) {
1768            return;
1769        }
1770
1771        $scope.updateDynamicTaxesOnce();
1772    };
1773
1774    $scope.isSubscribeToMarketingEmailsEnabled = function() {
1775        return $scope.InitInfo.CheckoutSettings && $scope.InitInfo.CheckoutSettings.subscribeToMarketingEmailsAtCheckout;
1776    };
1777
1778    $scope.isNewUIFlagSet = function() {
1779       return ($scope.InitInfo && $scope.InitInfo.checkout_ux_2020_04_23_flag);
1780    };
1781
1782    // ******************************************************************************************
1783    // region Cart Data Methods
1784    // ******************************************************************************************
1785
1786    $scope.getAmountDue = function() {
1787        if ($scope.isWebPayment()) {
1788            var $totalDue = 0;
1789            if ($scope.InitInfo && $scope.InitInfo.payment_info && $scope.InitInfo.payment_info.total_due > 0) {
1790                $totalDue = $scope.InitInfo.payment_info.total_due;
1791            }
1792            if ($scope.isInvoicePayment() && $scope.invoiceAmount && $scope.invoiceAmount < $totalDue) {
1793                $totalDue = $scope.invoiceAmount;
1794            }
1795            return $totalDue;
1796        }
1797        return $scope.Due;
1798    };
1799
1800    $scope.CartDataGet = function () {
1801        if ($scope.isWebPayment()) {
1802            $scope.showCaptcha = $scope.isCaptchaEnabled();
1803            return $scope.NoOpPromise();
1804        }
1805
1806        var options = {withCredentials: true};
1807
1808        if ($scope.InitInfo.store_token) {
1809            options.headers = {
1810                Authorization: 'Bearer ' + $scope.InitInfo.store_token
1811            };
1812        }
1813
1814        return $http.get($scope.BuildUrl('/checkout/cartdata'), options).then(function (result) {
1815            $scope.CartDataAdapt(result.data);
1816            if ($scope.checkFlagCarePlans()) {
1817                $scope.updateCarePlanPrice();
1818            }
1819        })
1820            .then(function () {
1821                if ($scope.knobby['module_better_coupons']) {
1822                    $scope.updateCouponsByCustomerGroups();
1823                }
1824        });
1825    };
1826
1827    $scope.MakeNewCartId = function() {
1828        var uuid = $scope.create_UUID();
1829        var name = 'cart_id';
1830        var days = 31;
1831        var expires = "";
1832        var date = new Date();
1833        date.setTime(date.getTime() + (days*24*60*60*1000));
1834        expires = "; expires=" + date.toUTCString();
1835        document.cookie = name + "=" + (uuid || "")  + expires + "; path=/";
1836    };
1837
1838    $scope.CartDataAdapt = function (data) {
1839        var tmpCaptcha = data.imnar;
1840        if (tmpCaptcha === true || tmpCaptcha === false) {
1841            $scope.showCaptcha = tmpCaptcha;
1842        }
1843
1844        $scope.CartData = data.cartdata || {};
1845
1846        if ($scope.knobby['module_better_coupons']) {
1847            $scope.CartData.Coupons = $scope.CartData.Coupons.map(function (coupon) {
1848                var createdCoupon = new BetterCouponFactory(coupon).createCoupon();
1849
1850                if (!$scope.couponWithCustomerGroupsIsAvailable(createdCoupon)) {
1851                    createdCoupon.notEligible = true;
1852
1853                    return createdCoupon;
1854                }
1855
1856                if (typeof createdCoupon.setAmountsBasedOnTiers === 'function' || 'setAmountsBasedOnTiers' in createdCoupon) {
1857                    if (createdCoupon.couponBaseType === CouponConstants.coupon_bogo) {
1858                        createdCoupon.setAmountsBasedOnTiers({lines: $scope.CartData.Items, disableCouponsForSalePriceItems: $scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems});
1859                    } else {
1860                        createdCoupon.setAmountsBasedOnTiers($scope.SubTotal);
1861                    }
1862                }
1863
1864                return createdCoupon;
1865            });
1866        }
1867
1868        if ($scope.CartData.CartIsPurchased) {
1869            $scope.CartStatus.Valid = false;
1870
1871            return;
1872        }
1873        if ($scope.CartData.CartIsEmpty) {
1874            if(data.cartdata && data.cartdata.resetCartId && data.cartdata.resetCartId === true) {
1875                $scope.MakeNewCartId();
1876            }
1877
1878            $scope.CartStatus.Valid = false;
1879            return;
1880        }
1881
1882        // Retrieve from session Storage and merge results
1883        var stored = $scope.SessionStorageGet();
1884
1885        if (stored) {
1886            if (stored.ShippingInfo) {
1887                if ($scope.InitInfo.InternationalEnabled === false) {
1888
1889                    if(stored.ShippingInfo.Country != $scope.InitInfo.DomesticCountry) {
1890                        stored.ShippingInfo.Country = $scope.InitInfo.DomesticCountry;
1891                        stored.ShippingInfo.Country = $scope.InitInfo.DomesticCountry;
1892                        stored.ShippingInfo.CountryIso2Key = $scope.CountryIso2KeyGet($scope.InitInfo.DomesticCountry);
1893                        stored.ShippingInfo.CountryKey = $scope.CountryKeyGet($scope.InitInfo.DomesticCountry);
1894                    }
1895
1896                    if($scope.shippingCountryHasRegions() &&  stored.ShippingInfo.State !='' && typeof $scope.InitInfo.Regions[$scope.getShippingCountryKey()][stored.ShippingInfo.State] == 'undefined'){
1897                        stored.ShippingInfo.State = '' ;
1898                        $scope.shippingCalcState = '';
1899                    }
1900
1901                    $scope.ShippingInfo = $.extend($scope.ShippingInfo, stored.ShippingInfo);
1902                    if ($scope.ShippingInfo.Country) {
1903                        $scope.ShippingCountrySet($scope.ShippingInfo.Country);
1904                    }
1905                    $scope.setShippingOption();
1906
1907                }
1908
1909                $scope.ShippingInfo = $.extend($scope.ShippingInfo, stored.ShippingInfo);
1910                if ($scope.ShippingInfo.Country) {
1911                    $scope.ShippingCountrySet($scope.ShippingInfo.Country);
1912                }
1913                $scope.setShippingOption();
1914            }
1915            if (stored.BillingInfo) {
1916                $scope.BillingInfo = $.extend($scope.BillingInfo, stored.BillingInfo);
1917            }
1918        }
1919
1920        if (
1921            $scope.isFlagSaveLuxsuranceInsuranceInfoAfterReloadingEnabled()
1922            && ($scope.isFlagInsuranceEnabled() || $scope.isFlagLuxsuranceEnabled())
1923        ) {
1924            $scope.setInsuranceInfo();
1925        }
1926
1927        // Items
1928        if ($scope.CartData.Items) {
1929            for (var i = 0, ilen = $scope.CartData.Items.length; i < ilen; i++) {
1930                var item = $scope.CartData.Items[i];
1931
1932                var qty = item.qty;
1933
1934                // Fat Quarter
1935                if (item.qty_min === 'fat quarter') {
1936                    item.FatQuarter = true;
1937                    item.qty_min = .25;
1938                }
1939
1940                if (qty === 'fat quarter') {
1941                    item.qty = .25;
1942                }
1943                var measureUnit = item.measure_unit;
1944                if ((measureUnit === 'Meter') && (qty === 0.25)) {
1945                    item.FatQuarter = true;
1946                }
1947
1948                // minimum inventory
1949                if (qty < item.qty_min) {
1950                    item.qty = item.qty_min;
1951                }
1952
1953                item.total = $scope.RoundTo2Dec(fractionalMultiplication(item.qty, $scope.getItemPrice(item)));
1954                item.qty_integer = (Math.floor(qty)).toString();
1955
1956                // fractional inventory
1957                item.qty_remainder = $scope.getItemQtyRemainder(item);
1958
1959
1960                item.integer_options = $scope.IntegerOptions(item);
1961                item.remainder_options = $scope.RemainderOptions(item);
1962            }
1963        }
1964
1965        if ($scope.CartData.message) {
1966            $scope.StatusInfo.ErrorMessage = $scope.CartData.message;
1967        }
1968
1969        if ($scope.CartData.MessageArray) {
1970            if ($scope.CartData.MessageArray.RemovedItems) {
1971                $scope.InventoryMessages.RemovedItems = $scope.CartData.MessageArray.RemovedItems;
1972            }
1973
1974            if ($scope.CartData.MessageArray.InventoryChanges) {
1975                $scope.InventoryMessages.InventoryChanges = $scope.CartData.MessageArray.InventoryChanges;
1976            }
1977
1978            if ($scope.CartData.MessageArray.StoreAvailableItems) {
1979                $scope.AvailabilityMessages.StoreAvailableItems = $scope.CartData.MessageArray.StoreAvailableItems;
1980            }
1981        }
1982
1983        $scope.Validate();
1984        $scope.updateProcessor();
1985        console.log($scope.CartData);
1986        if ($scope.isSubscriptionOnly()) {
1987            $scope.autoFillBillingDataWithSubscription();
1988        }
1989
1990        $scope.tryRequestDynamicTaxesForClasses();
1991    };
1992
1993    $scope.getItemQtyRemainder = function(item) {
1994        var qtyRemainder = null;
1995
1996        var qty = item.qty;
1997        var measureUnit = item.measure_unit;
1998
1999        if (measureUnit === 'Meter') {
2000            qtyRemainder = ((Math.round(qty * 100) % 100) + '').toString();
2001        } else {
2002            qtyRemainder = ((Math.round(qty * 100) % 100) + '').toString();
2003            qtyRemainder = ((Math.round(qtyRemainder / 25) * 25) + '').toString();
2004        }
2005
2006        return qtyRemainder;
2007    };
2008
2009    $scope.create_UUID = function () {
2010        var dt = new Date().getTime();
2011        var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
2012            var r = (dt + Math.random()*16)%16 | 0;
2013            dt = Math.floor(dt/16);
2014            return (c=='x' ? r :(r&0x3|0x8)).toString(16);
2015        });
2016        return uuid;
2017    }
2018
2019    $scope.autoFillBillingDataWithSubscription = function () {
2020        if ($scope.CartStatus.Valid && $scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
2021
2022            var subscriptionItems = $scope.CartData.Items.filter(function (cartItem) {
2023                return cartItem.type === 'subscription'
2024            });
2025
2026            if (subscriptionItems.length > 0) {
2027                var subscriptionItem = subscriptionItems.shift();
2028                if (subscriptionItem && subscriptionItem.notes) {
2029                    $scope.BillingInfo.FirstName = subscriptionItem.notes.firstName;
2030                    $scope.BillingInfo.LastName = subscriptionItem.notes.lastName;
2031                    $scope.BillingInfo.Address1 = subscriptionItem.notes.streetAddress1;
2032                    $scope.BillingInfo.Address2 = subscriptionItem.notes.streetAddress2;
2033                    $scope.BillingInfo.City = subscriptionItem.notes.city;
2034                    $scope.BillingInfo.State = subscriptionItem.notes.stateCode;
2035                    $scope.BillingInfo.Zip = subscriptionItem.notes.postalCode;
2036                    $scope.BillingInfo.Country = subscriptionItem.notes.country;
2037                    $scope.BillingInfo.CountryKey = subscriptionItem.notes.countryCode;
2038                }
2039            }
2040        }
2041    };
2042
2043
2044    $scope.SessionStorageGet = function () {
2045        var stored = null;
2046        if (typeof sessionStorage !== 'undefined' && sessionStorage.checkout_info) {
2047            var temp = JSON.parse(sessionStorage.checkout_info);
2048            if (temp) {
2049                if (temp.cartId && temp.cartId == $scope.CartId) {
2050                    stored = temp;
2051                } else {
2052                    sessionStorage.removeItem('checkout_info')
2053                }
2054            }
2055        }
2056        return stored;
2057    };
2058
2059    $scope.SessionStorageSet = function () {
2060        if (typeof sessionStorage !== 'undefined') {
2061            var temp = {};
2062            temp.cartId = $scope.CartId;
2063            temp.ShippingInfo = $scope.ShippingInfo;
2064            temp.BillingInfo = $scope.BillingInfo;
2065            sessionStorage.checkout_info = JSON.stringify(temp);
2066        }
2067    };
2068
2069    //endregion
2070
2071    // ******************************************************************************************
2072    // region Calculators
2073    // ******************************************************************************************
2074
2075    $scope.resetTaxes = function() {
2076        $scope.Taxes = 0;
2077        $scope.GST_Taxes = 0;
2078        $scope.dynamicTotalTax = 0;
2079        $scope.taxable_sales = 0;
2080        $scope.non_taxable_sales = 0;
2081        $scope.non_sales = 0;
2082        $scope.JurisdictionalTax = "";
2083        $scope.JurisdictionalTaxes = {};
2084
2085        var items = $scope.getItemsWithPackageItems();
2086        var nonTaxable = 0;
2087        if (items) {
2088            for (var i = 0; i < items.length; i++) {
2089                var item = items[i];
2090                item.tax = 0;
2091                item.taxable = 0;
2092                item.nonsales = 0;
2093                item.nontaxable = item.total;
2094                if (item.total) {
2095                    nonTaxable += item.total;
2096                }
2097                item.jurisdictional_taxes = {};
2098            }
2099        }
2100
2101        $scope.non_taxable_sales = nonTaxable;
2102        if ($scope.Discounts) {
2103            $scope.non_taxable_sales -= $scope.Discounts;
2104        }
2105    };
2106
2107    // Keep in sync with Cart.model.php::TransJurisdictionTaxUpdate
2108    $scope.TransJurisdictionTaxUpdate = function (transaction_jurisdictional_taxes, jurisdiction_name, jurisdiction_tax, total) {
2109        if (transaction_jurisdictional_taxes) {
2110            var temp_tax = {};
2111            var moneyPrecision = $scope.checkFlagFixCheckoutJurisdictionTaxPrecision() ? 2 : 5;
2112            if (transaction_jurisdictional_taxes.hasOwnProperty(jurisdiction_name)) {
2113                var prior_tax_value = Number(transaction_jurisdictional_taxes[jurisdiction_name]['tax_paid']);
2114                var prior_taxable_amount = Number(transaction_jurisdictional_taxes[jurisdiction_name]['taxable_sales']);
2115                prior_tax_value += $scope.roundNumber(jurisdiction_tax, moneyPrecision);
2116                if (jurisdiction_name !== 'Shipping' && !jurisdiction_name.match('Shipping')) {
2117                    prior_taxable_amount += $scope.roundNumber(total, moneyPrecision);
2118                }
2119                temp_tax['tax_paid'] = $scope.roundNumber(prior_tax_value, moneyPrecision);
2120                temp_tax['taxable_sales'] = $scope.roundNumber(prior_taxable_amount, moneyPrecision);
2121            } else {
2122                temp_tax['tax_paid'] = $scope.roundNumber(jurisdiction_tax, moneyPrecision);
2123                temp_tax['taxable_sales'] = $scope.roundNumber(total, moneyPrecision);
2124            }
2125
2126            transaction_jurisdictional_taxes[jurisdiction_name] = temp_tax;
2127        }
2128    };
2129
2130    $scope.calculateRtoTaxes = function (item, transaction_jurisdictional_taxes, useStoreTaxTable) {
2131        var info = {
2132            'line_jurisdictional_taxes': {},
2133            'line_tax': 0,
2134            'taxable_amount': 0,
2135            'non_taxable_amount': 0,
2136            'non_sales_amount': 0,
2137            'transaction_jurisdictional_taxes': {}
2138        };
2139
2140        var isCanadaTax = $scope.useCanadaTax();
2141
2142        // Product
2143        var rtoProductPayment = parseFloat(item.rtoItems.rto_principle_payment || 0) * parseInt(item.rtoItems.rto_initial_payments || 0) + parseFloat(item.rtoItems.rto_down_payment || 0);
2144        if (rtoProductPayment) {
2145            var rtoProductPaymentTaxBitmap = isCanadaTax ? item.tax_bitmap : item.rtoItems.rto_product_tax_bitmap;
2146            var taxInfoProductPayment = $scope.calculateBitmapTax(rtoProductPaymentTaxBitmap, rtoProductPayment, transaction_jurisdictional_taxes, useStoreTaxTable, false, null, true);
2147            for (var nameProduct in taxInfoProductPayment.line_jurisdictional_taxes) {
2148                info['line_jurisdictional_taxes'][nameProduct] = taxInfoProductPayment['line_jurisdictional_taxes'][nameProduct];
2149            }
2150            if (taxInfoProductPayment['line_tax']) {
2151                info['line_tax'] += $scope.roundNumber(taxInfoProductPayment['line_tax'], 2);
2152                info['taxable_amount'] += taxInfoProductPayment['taxable_amount'];
2153                info['non_taxable_amount'] += taxInfoProductPayment['non_taxable_amount'];
2154            }
2155        }
2156
2157        // Maintenance
2158        var rtoMaintenancePayment = parseFloat(item.rtoItems.rto_maintenance_payment || 0) * parseInt(item.rtoItems.rto_initial_payments || 0);
2159        if (rtoMaintenancePayment) {
2160            var rtoMaintenanceTaxBitmap = isCanadaTax ? item.tax_bitmap : item.rtoItems.rto_maintenance_tax_bitmap;
2161            var taxInfoMaintenancePayment = $scope.calculateBitmapTax(rtoMaintenanceTaxBitmap, rtoMaintenancePayment, transaction_jurisdictional_taxes, useStoreTaxTable, false, null, true);
2162            for (var name in taxInfoMaintenancePayment.line_jurisdictional_taxes) {
2163                if (info['line_jurisdictional_taxes'][name] === undefined) {
2164                    info['line_jurisdictional_taxes'][name] = taxInfoMaintenancePayment['line_jurisdictional_taxes'][name];
2165                } else {
2166                    info['line_jurisdictional_taxes'][name]['tax_collected'] += taxInfoMaintenancePayment['line_jurisdictional_taxes'][name]['tax_collected'];
2167                    info['line_jurisdictional_taxes'][name]['tax_paid'] += taxInfoMaintenancePayment['line_jurisdictional_taxes'][name]['tax_paid'];
2168                    info['line_jurisdictional_taxes'][name]['taxable_amount'] += taxInfoMaintenancePayment['line_jurisdictional_taxes'][name]['taxable_amount'];
2169                }
2170            }
2171            if (taxInfoMaintenancePayment['line_tax']) {
2172                info['line_tax'] += $scope.roundNumber(taxInfoMaintenancePayment['line_tax'], 2);
2173                info['taxable_amount'] += taxInfoMaintenancePayment['taxable_amount'];
2174                info['non_taxable_amount'] += taxInfoMaintenancePayment['non_taxable_amount'];
2175            }
2176        }
2177
2178        // Insurance
2179        var rtoInsurancePayment = parseFloat(item.rtoItems.rto_insurance_payment) * parseInt(item.rtoItems.rto_initial_payments);
2180        if (rtoInsurancePayment) {
2181            var rtoInsuranceTaxBitmap = isCanadaTax ? item.tax_bitmap : item.rtoItems.rto_insurance_tax_bitmap;
2182            var taxInfoInsurancePayment = $scope.calculateBitmapTax(rtoInsuranceTaxBitmap, rtoInsurancePayment, transaction_jurisdictional_taxes, useStoreTaxTable, false, null, true);
2183            for (var name in taxInfoInsurancePayment.line_jurisdictional_taxes) {
2184                if (info['line_jurisdictional_taxes'][name] === undefined) {
2185                    info['line_jurisdictional_taxes'][name] = taxInfoInsurancePayment['line_jurisdictional_taxes'][name];
2186                } else {
2187                    info['line_jurisdictional_taxes'][name]['tax_collected'] += taxInfoInsurancePayment['line_jurisdictional_taxes'][name]['tax_collected'];
2188                    info['line_jurisdictional_taxes'][name]['tax_paid'] += taxInfoInsurancePayment['line_jurisdictional_taxes'][name]['tax_paid'];
2189                    info['line_jurisdictional_taxes'][name]['taxable_amount'] += taxInfoInsurancePayment['line_jurisdictional_taxes'][name]['taxable_amount'];
2190                }
2191            }
2192            if (taxInfoInsurancePayment['line_tax']) {
2193                info['line_tax'] += $scope.roundNumber(taxInfoInsurancePayment['line_tax'], 2);
2194                info['taxable_amount'] += taxInfoInsurancePayment['taxable_amount'];
2195                info['non_taxable_amount'] += taxInfoInsurancePayment['non_taxable_amount'];
2196            }
2197        }
2198
2199        // Deposit
2200        var rtoDeposit = parseFloat(item.rtoItems.rto_security_deposit);
2201        if (rtoDeposit > 0) {
2202            info.non_sales_amount += rtoDeposit;
2203        }
2204
2205        // Cleanup rounding
2206        info.taxable_amount = $scope.roundNumber(info.taxable_amount, 2);
2207        info.non_taxable_amount = $scope.roundNumber(info.non_taxable_amount, 2);
2208        info.non_sales_amount = $scope.roundNumber(info.non_sales_amount, 2);
2209        var moneyPrecision = $scope.checkFlagFixCheckoutJurisdictionTaxPrecision() ? 2 : 5;
2210
2211        for (var name in info.line_jurisdictional_taxes) {
2212            info.line_jurisdictional_taxes[name]['tax_collected'] = $scope.roundNumber(info.line_jurisdictional_taxes[name]['tax_collected'], 2);
2213            info.line_jurisdictional_taxes[name]['tax_paid'] = $scope.roundNumber(info.line_jurisdictional_taxes[name]['tax_paid'], moneyPrecision);
2214            info.line_jurisdictional_taxes[name]['taxable_amount'] = $scope.roundNumber(info.line_jurisdictional_taxes[name]['taxable_amount'], moneyPrecision);
2215        }
2216        return info;
2217    }
2218
2219    $scope.calculateSubscriptionTaxes = function (item, transaction_jurisdictional_taxes, useStoreTaxTable, alternativeState) {
2220        var info = {
2221            'line_jurisdictional_taxes': {},
2222            'line_tax': 0,
2223            'taxable_amount': 0,
2224            'non_taxable_amount': 0,
2225            'transaction_jurisdictional_taxes': {}
2226        };
2227
2228        var registrationFee = parseFloat(item.subscriptionData.registration_fee);
2229        var subscriptionTaxInfo = $scope.calculateBitmapTax(item.tax_bitmap, registrationFee, transaction_jurisdictional_taxes, useStoreTaxTable, false, alternativeState);
2230        for (var nameProduct in subscriptionTaxInfo.line_jurisdictional_taxes) {
2231            info['line_jurisdictional_taxes'][nameProduct] = subscriptionTaxInfo['line_jurisdictional_taxes'][nameProduct];
2232            info['transaction_jurisdictional_taxes'] = subscriptionTaxInfo['transaction_jurisdictional_taxes'];
2233        }
2234
2235        info['line_tax'] += subscriptionTaxInfo['line_tax'];
2236        info['taxable_amount'] += subscriptionTaxInfo['taxable_amount'];
2237        info['non_taxable_amount'] += subscriptionTaxInfo['non_taxable_amount'];
2238        info.subscription_details = {
2239            'line_jurisdictional_taxes': JSON.stringify(Object.assign({}, info.transaction_jurisdictional_taxes)),
2240            'line_tax': info['line_tax'],
2241            'taxable_amount': info['taxable_amount'],
2242            'non_taxable_amount': info['non_taxable_amount'],
2243            'kit_taxes': [],
2244        };
2245
2246        if (item.subscriptionData.previous_items.length > 0) {
2247            var taxAtCountryLevel = $scope.taxAtCountryLevel();
2248            var showAsGST = $scope.showAsGST();
2249            var useStoreTaxTableForProduct = taxAtCountryLevel || showAsGST;
2250            var pricePerKit = parseFloat(item.subscriptionData.price_per_kit);
2251
2252            if ($scope.isPickup() || useStoreTaxTable) {
2253                useStoreTaxTableForProduct = true;
2254            }
2255
2256            for (var i = 0; i < item.subscriptionData.previous_items.length; i++) {
2257                var prevItem = item.subscriptionData.previous_items[i];
2258                prevItem.tax_paid = 0;
2259                var productBitmap = prevItem.bitmap;
2260
2261                if (!productBitmap) {
2262                    continue;
2263                }
2264                var productTaxInfo = $scope.calculateBitmapTax(productBitmap, pricePerKit, transaction_jurisdictional_taxes, useStoreTaxTableForProduct, false);
2265                info['line_tax'] += productTaxInfo['line_tax'];
2266                info['taxable_amount'] += productTaxInfo['taxable_amount'];
2267                info['non_taxable_amount'] += productTaxInfo['non_taxable_amount'];
2268                prevItem.tax_paid = productTaxInfo['line_tax'];
2269                for (var name in productTaxInfo.line_jurisdictional_taxes) {
2270                    if (info['line_jurisdictional_taxes'][name] === undefined) {
2271                        info['line_jurisdictional_taxes'][name] = productTaxInfo['line_jurisdictional_taxes'][name];
2272                    } else {
2273                        info['line_jurisdictional_taxes'][name]['tax_collected'] += productTaxInfo['line_jurisdictional_taxes'][name]['tax_collected'];
2274                        info['line_jurisdictional_taxes'][name]['tax_paid'] += productTaxInfo['line_jurisdictional_taxes'][name]['tax_paid'];
2275                        info['line_jurisdictional_taxes'][name]['taxable_amount'] += productTaxInfo['line_jurisdictional_taxes'][name]['taxable_amount'];
2276                    }
2277
2278                    info['transaction_jurisdictional_taxes'] = productTaxInfo['transaction_jurisdictional_taxes'];
2279                }
2280                info.subscription_details.kit_taxes.push({
2281                    'tax_paid': prevItem.tax_paid,
2282                    'subscription_offering_item_id': prevItem.subscription_offering_item_id,
2283                    'line_jurisdictional_taxes': JSON.stringify(Object.assign({}, productTaxInfo['line_jurisdictional_taxes']))
2284                });
2285            }
2286        }
2287        return info;
2288    };
2289
2290    $scope.CalculateItemTaxes = function () {
2291        var items = $scope.getItemsWithPackageItems();
2292
2293        /* if we have GC or coupon and discount exceeds total price we need skip al taxes */
2294        var isGcOrCouponExceeds = false;
2295        if ($scope.Discounts === $scope.SubTotal) {
2296            isGcOrCouponExceeds = true;
2297        }
2298        if (!items || items.length === 0 || $scope.isWholesaleTaxExempt() || isGcOrCouponExceeds) {
2299            $scope.resetTaxes();
2300            return;
2301        }
2302
2303        var class_bitmap = $scope.getClassTaxBitmap();
2304        var product_bitmap = $scope.getProductTaxBitmap();
2305        var taxAtCountryLevel = $scope.taxAtCountryLevel();
2306        var showAsGST = $scope.showAsGST();
2307        var taxable_sales_total = 0;
2308        var non_taxable_sales_total = 0;
2309        var non_sales_total = 0;
2310        var transaction_jurisdictional_taxes = {};
2311        var transaction_line_tax = 0;
2312        var total_gst_tax = 0;
2313        var stateCode = null;
2314        var totalAmount = 0;
2315        var moneyPrecision = $scope.checkFlagFixCheckoutJurisdictionTaxPrecision() ? 2 : 5;
2316
2317        for (var i = 0; i < items.length; i++) {
2318            var item = items[i];
2319            var total = item.discounted_total ? item.discounted_total : item.total;
2320            totalAmount += total;
2321
2322            // Each item could use a different tax table
2323            var useStoreTaxTable = false;
2324            if (item.type == 'pitem' || item.type == 'pslr' || item.type == 'rto' || item.type == 'product') {
2325                if (item.tax_bitmap > 0 && product_bitmap && $scope.InitInfo.useCanadaTax) {
2326                    item.tax_bitmap = product_bitmap;
2327                }
2328
2329                useStoreTaxTable = taxAtCountryLevel || showAsGST;
2330
2331                if ($scope.isPickup()) {
2332                    useStoreTaxTable = true;
2333                }
2334
2335                // NOTE: Don't do this for everything because PayPal only stores will have shipping off so PayPal can calculate the shipping
2336                // But we still need to calculate the taxes correctly
2337                if ($scope.InitInfo.ShippingEnabled == false && item.type == 'rto') {
2338                    useStoreTaxTable = true;
2339                }
2340            }
2341            if (item.type == 'class') {
2342                if (class_bitmap && (!item.tax_bitmap || item.tax_bitmap == 0 || $scope.InitInfo.useCanadaTax)) {
2343                    item.tax_bitmap = class_bitmap;
2344                }
2345
2346                useStoreTaxTable = true;
2347            }
2348            if (item.type == 'pitem' || item.type == 'rto' || item.type == 'product') {
2349                if (product_bitmap && (!item.tax_bitmap || item.tax_bitmap == 0)) {
2350                    item.tax_bitmap = product_bitmap;
2351                }
2352                if (item.parent_type == 'class') {
2353                    useStoreTaxTable = true;
2354                }
2355            }
2356            if (item.type == 'subscription') {
2357                if (item.notes && item.notes.stateCode) {
2358                    stateCode = item.notes.stateCode;
2359                } else if (item.subscriptionData && item.subscriptionData.notes && item.subscriptionData.notes.stateCode) {
2360                    stateCode = item.subscriptionData.notes.stateCode;
2361                }
2362
2363                if (product_bitmap && (!item.tax_bitmap || item.tax_bitmap == 0 || $scope.InitInfo.useCanadaTax)) {
2364                    item.tax_bitmap = product_bitmap;
2365                }
2366                useStoreTaxTable = taxAtCountryLevel || showAsGST;
2367                if ($scope.isPickup() || !item.notes || !item.notes.type || item.notes.type.toLowerCase() !== 'ship') {
2368                    useStoreTaxTable = true;
2369                }
2370
2371                var taxTable = $scope.getTaxTable(true, stateCode, item.tax_bitmap);
2372
2373                if (stateCode !== null && taxTable) {
2374                    if (taxTable.region !== stateCode) {
2375                        item.tax_bitmap = 0;
2376                    } else {
2377                        useStoreTaxTable = true;
2378                    }
2379                }
2380            }
2381
2382            if (item.notes && item.notes.taxable_amount) {
2383                item.taxable_amount = item.notes.taxable_amount;
2384                item.notes = '';
2385            }
2386
2387            if (!item.taxable_amount) {
2388                item.taxable_amount = total;
2389            }
2390
2391            var isClassWithDynamicTaxes = item.type === 'class' && $scope.HasDynamicTaxes;
2392
2393            // Calculate taxes
2394            if (item.type === 'rto' && item.rtoItems !== undefined) {
2395                var info = $scope.calculateRtoTaxes(item, transaction_jurisdictional_taxes, useStoreTaxTable);
2396            } else if (item.type === 'subscription') {
2397                var info = $scope.calculateSubscriptionTaxes(item, transaction_jurisdictional_taxes, useStoreTaxTable, stateCode);
2398            } else {
2399                if (isClassWithDynamicTaxes && item.tax_bitmap == 0) {
2400                    //We don't want to calculate taxes for classes with dynamic taxes
2401                    var info = $scope.calculateBitmapTax(item.tax_bitmap, 0, transaction_jurisdictional_taxes, useStoreTaxTable, false);
2402                } else if ($scope.InitInfo.customer?.tax_exempt === 'yes' && $scope.InitInfo.customer?.tax_exempt_products && (item.type == 'pitem' || item.type == 'product' || item.type == 'pslr')) {
2403                    var info = $scope.calculateBitmapTax(item.tax_bitmap, 0, transaction_jurisdictional_taxes, useStoreTaxTable, false);
2404                } else if (($scope.InitInfo.customer?.tax_exempt === 'yes' && $scope.InitInfo.customer?.tax_exempt_classes && (item.type == 'class' || item.parent_type == 'class'))) {
2405                    var info = $scope.calculateBitmapTax(item.tax_bitmap, 0, transaction_jurisdictional_taxes, useStoreTaxTable, false);
2406                } else {
2407                    if ($scope.checkFlagAvalaraTaxCodeIssue()) {
2408                        if (item.nonTaxableItem) {
2409                            var info = $scope.calculateBitmapTax(item.tax_bitmap, 0, transaction_jurisdictional_taxes, useStoreTaxTable, false);
2410                        } else {
2411                            var info = $scope.calculateBitmapTax(item.tax_bitmap, item.taxable_amount, transaction_jurisdictional_taxes, useStoreTaxTable, false);
2412                        }
2413                    } else {
2414                        var info = $scope.calculateBitmapTax(item.tax_bitmap, item.taxable_amount, transaction_jurisdictional_taxes, useStoreTaxTable, false);
2415                    }
2416                }
2417            }
2418
2419            if (!item.gst_tax || !item.gst_price) {
2420                var gst_info = $scope.calculateBitmapTax(item.tax_bitmap, item.price, null, useStoreTaxTable, false);
2421                item.gst_tax = $scope.roundNumber(gst_info.line_tax, 5);
2422                item.gst_price = $scope.roundNumber(+item.price + item.gst_tax, 2);
2423            }
2424
2425            total_gst_tax += (item.gst_tax * item.qty);
2426
2427            // Update item tax details
2428            item.jurisdictional_taxes = info.line_jurisdictional_taxes;
2429
2430            item.tax = $scope.roundNumber(info.line_tax, moneyPrecision);
2431            transaction_line_tax += info.line_tax;
2432            if (info['subscription_details']) {
2433                item.subscription_details = info['subscription_details'];
2434            }
2435            item.taxable = 0;
2436            if (info.taxable_amount > 0) {
2437                item.taxable = info.taxable_amount;
2438                taxable_sales_total += info.taxable_amount;
2439            }
2440            item.nontaxable = 0;
2441            if (info.non_taxable_amount) {
2442                item.nontaxable = info.non_taxable_amount;
2443                non_taxable_sales_total += info.non_taxable_amount;
2444            }
2445            item.nonsales = 0;
2446            if (info.non_sales_amount) {
2447                item.nonsales = info.non_sales_amount;
2448                non_sales_total += info.non_sales_amount;
2449            }
2450        }
2451
2452        // sum taxes by jurisdictional value
2453        var total_transaction_tax = 0;
2454        for (var tax_jurisdiction in transaction_jurisdictional_taxes) {
2455            if (transaction_jurisdictional_taxes.hasOwnProperty(tax_jurisdiction)) {
2456                total_transaction_tax += transaction_jurisdictional_taxes[tax_jurisdiction]['tax_paid'];
2457                transaction_jurisdictional_taxes[tax_jurisdiction]['tax_paid'] = $scope.roundNumber(transaction_jurisdictional_taxes[tax_jurisdiction]['tax_paid'], moneyPrecision);
2458            }
2459        }
2460
2461        var diff = subFloats(subFloats(subFloats(totalAmount, taxable_sales_total), non_sales_total),  non_taxable_sales_total);
2462        if (diff > 0.005) {
2463            non_taxable_sales_total += diff;
2464        } else if (diff < -0.005) {
2465            console.error('We have a negative difference on the tax totals');
2466            console.debug('diff is ' + diff);
2467        }
2468
2469        if (total_transaction_tax < 0.005) {
2470            $scope.resetTaxes();
2471            return;
2472        }
2473
2474        var transaction_tax = items.reduce(function (tax, item) {
2475            return tax + item.tax;
2476        }, 0);
2477
2478        if ($scope.useCanadaTax()) {
2479            // Fix: Ensure consistency between individual item taxes and total transaction tax
2480            let jurisdictional_tax_total = $scope.roundNumber(total_transaction_tax, 2);
2481
2482            let tax_difference =  jurisdictional_tax_total - transaction_tax;
2483            if (Math.abs(tax_difference) >= 0.01) {
2484                // Use the higher precision jurisdictional total if there's a significant difference
2485                transaction_tax = jurisdictional_tax_total;
2486                // sum taxes by line value & take the difference and apply it to the first taxable line item. (*magic penny*)
2487                for (let i = 0; i < items.length; i++) {
2488                    let line_item = items[i];
2489                    if (line_item.taxable !== 0 && line_item.tax && (line_item.tax + tax_difference) > 0) {
2490                        let item_tax_value = line_item.tax;
2491                        line_item.tax = $scope.roundNumber(item_tax_value + tax_difference, 2);
2492                        let jurisdictionIndexOfLargestTax = null;
2493                        let jurisdictionLargestTaxCollected = 0;
2494                        for (let jurisdictionIndex in line_item.jurisdictional_taxes) {
2495                            let jTaxCollected = line_item.jurisdictional_taxes[jurisdictionIndex].tax_collected;
2496                            if (!jTaxCollected) {
2497                                continue;
2498                            }
2499                            if (jTaxCollected > jurisdictionLargestTaxCollected) {
2500                                jurisdictionLargestTaxCollected = jTaxCollected;
2501                                jurisdictionIndexOfLargestTax = jurisdictionIndex;
2502                            }
2503                        }
2504                        if (jurisdictionIndexOfLargestTax) {
2505                            let taxCollected = line_item.jurisdictional_taxes[jurisdictionIndexOfLargestTax].tax_collected;
2506                            taxCollected = $scope.roundNumber(taxCollected + tax_difference, 2);
2507                            line_item.jurisdictional_taxes[jurisdictionIndexOfLargestTax].tax_collected = taxCollected;
2508                        }
2509                        break;
2510                    }
2511                }
2512            }
2513        }
2514
2515        $scope.taxable_sales = $scope.RoundTo2Dec(taxable_sales_total);
2516        $scope.non_taxable_sales = $scope.RoundTo2Dec(non_taxable_sales_total);
2517        $scope.non_sales = $scope.RoundTo2Dec(non_sales_total);
2518        $scope.setTransactionJurisdictionTaxes(transaction_jurisdictional_taxes);
2519
2520        $scope.GST_Taxes = $scope.RoundTo2Dec(total_gst_tax);
2521        $scope.Taxes = $scope.RoundTo2Dec(transaction_tax);
2522    };
2523
2524    $scope.calculateBitmapTax = function (taxBitmap, total, jurisdictionTaxes, useStoreTaxTable, isShippingTax, alternativeState, ignoreFixedTax) {
2525        var lineTaxes = {};
2526        var line_tax = 0;
2527        var taxable_amount = 0;
2528        var total_amount = $scope.roundNumber(total, 2);
2529        var originalTaxBitmap = +taxBitmap;
2530
2531        if ($scope.checkFlagFixCheckoutJurisdictionTaxPrecision()) {
2532            total = $scope.roundNumber(total, 2);
2533        } else {
2534            total = total || 0; // Make sure it's a number and not something like NaN
2535        }
2536
2537        if (originalTaxBitmap === 0) {
2538            return {
2539                line_jurisdictional_taxes: {},
2540                line_tax: 0,
2541                taxable_amount: 0,
2542                non_taxable_amount: total_amount,
2543                transaction_jurisdictional_taxes: jurisdictionTaxes,
2544            };
2545        }
2546
2547        if ($scope.InitInfo.CheckoutSettings.FixedTax === true && !ignoreFixedTax) {
2548            var fixedRate = Number($scope.InitInfo.CheckoutSettings.FixedTaxRate / 100);
2549            if (fixedRate > 0) {
2550                var jurisdiction = 'Fixed';
2551                if (isShippingTax) {
2552                    jurisdiction = 'Shipping'
2553                }
2554
2555                if (taxBitmap) {
2556                    if ($scope.checkFlagFixCheckoutJurisdictionTaxPrecision()) {
2557                        line_tax = $scope.roundNumber(total * fixedRate, 2);
2558                        taxable_amount = total;
2559                        lineTaxes[taxBitmap] = {
2560                            taxable_amount: total,
2561                            tax_paid: line_tax,
2562                            rate: $scope.roundNumber(fixedRate * 100, 5),
2563                            jurisdiction: jurisdiction,
2564                            jurisdiction_id: 'web_fixed'
2565                        };
2566                    } else {
2567                        line_tax = total * fixedRate;
2568                        taxable_amount = total;
2569                        lineTaxes[taxBitmap] = {
2570                            taxable_amount: $scope.roundNumber(total, 5),
2571                            tax_paid: $scope.roundNumber(line_tax, 5),
2572                            rate: $scope.roundNumber(fixedRate * 100, 5),
2573                            jurisdiction: jurisdiction,
2574                            jurisdiction_id: 'web_fixed'
2575                        };
2576                    }
2577
2578                    lineTaxes[taxBitmap].tax_collected = $scope.roundNumber(+lineTaxes[taxBitmap].tax_paid, 2);
2579                    $scope.TransJurisdictionTaxUpdate(jurisdictionTaxes, jurisdiction, line_tax, total);
2580                }
2581            }
2582        } else {
2583            var taxTable = $scope.getTaxTable(useStoreTaxTable, alternativeState, taxBitmap);
2584            if (taxTable) {
2585                for (var bitmap_id in taxTable.rates) {
2586                    var isTaxMatch = (+bitmap_id & +taxBitmap) || $scope.HasDynamicTaxes;
2587
2588                    if (isTaxMatch) {
2589                        var type = taxTable.rates[bitmap_id].tax_type;
2590                        if (type === 'dynamic rate') {
2591                            if ($scope.HasDynamicTaxes) {
2592                                var result = $scope.dynamicRateCache;
2593                                if (result !== null && typeof result !== 'undefined') {
2594                                    for (var dynamic_bitmap in result) {
2595                                        var dynamicRate = result[dynamic_bitmap];
2596                                        var dynamic_name = dynamicRate.title;
2597
2598                                        if (!$scope.isDynamicTaxServiceEnabled()) {
2599                                            if (isShippingTax) {
2600                                                dynamic_name += ' Shipping';
2601                                            }
2602                                        }
2603
2604                                        var dynamic_rate = Number(dynamicRate.rate);
2605
2606                                        if ($scope.isDynamicTaxServiceEnabled()) {
2607                                            if (isShippingTax && !dynamic_name.match('Shipping')) {
2608                                                continue;
2609                                            }
2610
2611                                            if (!isShippingTax && dynamic_name.match('Shipping')) {
2612                                                continue;
2613                                            }
2614                                        }
2615
2616                                        var dynamic_tax = total * Number(dynamic_rate);
2617                                        if ($scope.checkFlagFixC
2617heckoutJurisdictionTaxPrecision()) {
2618                                            dynamic_tax = $scope.roundNumber(total * Number(dynamic_rate), 2);
2619                                        }
2620
2621                                        if (dynamic_tax) {
2622                                            if ($scope.checkFlagFixCheckoutJurisdictionTaxPrecision()) {
2623                                                line_tax = $scope.roundNumber(line_tax + dynamic_tax, 2);
2624
2625                                                lineTaxes[dynamic_bitmap] = {
2626                                                    taxable_amount: total,
2627                                                    tax_paid: dynamic_tax,
2628                                                    rate: $scope.roundNumber(dynamic_rate * 100, 5),
2629                                                    jurisdiction: dynamic_name,
2630                                                    jurisdiction_id: dynamic_bitmap,
2631                                                };
2632                                            } else {
2633                                                line_tax += dynamic_tax;
2634
2635                                                lineTaxes[dynamic_bitmap] = {
2636                                                    taxable_amount: $scope.roundNumber(total, 5),
2637                                                    tax_paid: $scope.roundNumber(dynamic_tax, 5),
2638                                                    rate: $scope.roundNumber(dynamic_rate * 100, 5),
2639                                                    jurisdiction: dynamic_name,
2640                                                    jurisdiction_id: dynamic_bitmap,
2641                                                };
2642                                            }
2643
2644                                            lineTaxes[dynamic_bitmap].tax_collected = $scope.roundNumber(+lineTaxes[dynamic_bitmap].tax_paid, 2);
2645                                            $scope.TransJurisdictionTaxUpdate(jurisdictionTaxes, dynamic_name, dynamic_tax, total);
2646                                        }
2647                                    }
2648                                }
2649                            }
2650                        } else {
2651                            if (!$scope.HasDynamicTaxes) {
2652                                var rate_obj = taxTable.rates[bitmap_id];
2653                                var useCanadaTax = $scope.useCanadaTax();
2654
2655                                // Special case for Canada, PST shipping tax is only applied if shipping to or from BC
2656                                if (taxTable && isShippingTax && useCanadaTax && rate_obj.pos_tax_id === 'PST') {
2657                                    var shipToState = $scope.getShippingState();
2658                                    var shipFromState = $scope.getStoreState();
2659                                    if (shipToState !== 'BC' && shipFromState !== 'BC') {
2660                                        // Shipping tax not applicable
2661                                        continue;
2662                                    }
2663                                }
2664
2665                                // Calculate tax for this rate
2666                                var total_to_tax = $scope.calculateTaxableAmountForJurisdiction(total, rate_obj);
2667
2668                                // Set shipping jurisdiction name if shipping
2669                                var jurisdiction_name = rate_obj.title;
2670                                if (isShippingTax) {
2671                                    if (useCanadaTax) {
2672                                        jurisdiction_name += ' - Shipping';
2673                                    }
2674                                }
2675
2676                                // Adjust Canada jurisdiction name to include title
2677                                if (useCanadaTax) {
2678                                    jurisdiction_name = taxTable.title + ' - ' + jurisdiction_name;
2679                                }
2680
2681                                // No rate for Canada if no bitmap
2682                                var rate = Number(rate_obj.rate);
2683                                if (useCanadaTax && !originalTax
2683Bitmap) {
2684                                    rate = 0;
2685                                }
2686
2687                                // Calculate tax
2688                                var jurisdiction_tax = total_to_tax * rate;
2689                                if ($scope.checkFlagFixCheckoutJurisdictionTaxPrecision()) {
2690                                    jurisdiction_tax = $scope.roundNumber(total_to_tax * rate, 2);
2691                                }
2692                                var pos_tax_id = rate_obj.pos_tax_id;
2693                                var tax_profile_id = taxTable.rates[bitmap_id].pos_tax_profile_id !== 'undefined'
2694                                    ? taxTable.rates[bitmap_id].pos_tax_profile_id : null;
2695
2696                                if (jurisdiction_tax) {
2697                                    if ($scope.checkFlagFixCheckoutJurisdictionTaxPrecision()) {
2698                                        line_tax = $scope.roundNumber(line_tax + jurisdiction_tax, 2);
2699                                        lineTaxes[bitmap_id] = {
2700                                            taxable_amount: total_to_tax,
2701                                            tax_paid: jurisdiction_tax,
2702                                            rate: $scope.roundNumber(rate * 100, 5),
2703                                            jurisdiction: jurisdiction_name,
2704                                            jurisdiction_id: pos_tax_id,
2705                                            tax_profile_id: tax_profile_id
2706                                        };
2707                                        lineTaxes[bitmap_id].tax_collected = $scope.roundNumber(+lineTaxes[bitmap_id].tax_paid, 2);
2708                                        $scope.TransJurisdictionTaxUpdate(jurisdictionTaxes, jurisdiction_name, jurisdiction_tax, total_to_tax);
2709                                    } else {
2710                                        line_tax += jurisdiction_tax;
2711                                        lineTaxes[bitmap_id] = {
2712                                            taxable_amount: $scope.roundNumber(total_to_tax, 5),
2713                                            tax_paid: $scope.roundNumber(jurisdiction_tax, 5),
2714                                            rate: $scope.roundNumber(rate * 100, 5),
2715                                            jurisdiction: jurisdiction_name,
2716                                            jurisdiction_id: pos_tax_id,
2717                                            tax_profile_id: tax_profile_id
2718                                        };
2719                                        lineTaxes[bitmap_id].tax_collected = $scope.roundNumber(+lineTaxes[bitmap_id].tax_paid, 2);
2720                                        $scope.TransJurisdictionTaxUpdate(jurisdictionTaxes, jurisdiction_name, jurisdiction_tax, total_to_tax);
2721                                    }
2722                                }
2723                            }
2724                        }
2725                    }
2726                }
2727            }
2728        }
2729
2730        if (line_tax) {
2731            taxable_amount = total;
2732        }
2733        taxable_amount = $scope.roundNumber(taxable_amount, 2);
2734        var non_taxable_amount = 0;
2735        if (total_amount - taxable_amount > .005) {
2736            non_taxable_amount = total_amount - taxable_amount;
2737        }
2738
2739        return {
2740            line_jurisdictional_taxes: $scope.recalculateTaxJurisdictionApplied(line_tax, lineTaxes),
2741            line_tax: line_tax,
2742            taxable_amount: taxable_amount,
2743            non_taxable_amount: non_taxable_amount,
2744            transaction_jurisdictional_taxes: jurisdictionTaxes
2745        };
2746    };
2747
2748    $scope.recalculateTaxJurisdictionApplied = function (tax, taxJurisdictionsApplied) {
2749        if (Object.keys(taxJurisdictionsApplied).length) {
2750            var sumOfFixedTaxPaid = 0;
2751
2752            for (var key in taxJurisdictionsApplied) {
2753                sumOfFixedTaxPaid += $scope.roundNumber(+taxJurisdictionsApplied[key]['tax_paid'], 2);
2754            }
2755
2756            var taxDifference = $scope.roundNumber(tax, 2) - sumOfFixedTaxPaid;
2757
2758            if (taxDifference !== 0) {
2759                var lastKey = Object.keys(taxJurisdictionsApplied)[Object.keys(taxJurisdictionsApplied).length - 1];
2760                var tax_collected = taxJurisdictionsApplied[lastKey]['tax_collected'] + taxDifference;
2761                taxJurisdictionsApplied[lastKey]['tax_collected'] = $scope.roundNumber(tax_collected, 2);
2762            }
2763        }
2764
2765        return taxJurisdictionsApplied;
2766    };
2767
2768    $scope.calculateTaxableAmountForJurisdiction = function (total, jurisdiction) {
2769        total = +total;
2770        var taxableAmount = total;
2771        if (jurisdiction.tax_type === 'tiered rate') {
2772            var min_price = +jurisdiction.min_price || 0;
2773            var max_price = +jurisdiction.max_price || Number.MAX_VALUE;
2774            if (total < min_price) {
2775                taxableAmount = 0;
2776            } else {
2777                taxableAmount = total - min_price;
2778                if (total > max_price) {
2779                    taxableAmount = max_price - min_price;
2780                }
2781            }
2782        }
2783
2784        if ($scope.checkFlagFixCheckoutJurisdictionTaxPrecision()) {
2785            return $scope.roundNumber(taxableAmount, 2);
2786        }
2787
2788        return taxableAmount;
2789    };
2790
2791    /** * @return {number} */
2792    $scope.calculateShippingTax = function (shipping) {
2793        var shippingTaxes = 0;
2794        shipping = +shipping;
2795        $scope.shipping_tax_jurisdictions = {};
2796
2797        if ($scope.getShippingDiscount() > 0) {
2798            shipping -= $scope.getShippingDiscount();
2799        }
2800
2801        if (!shipping || shipping < 0 || $scope.isWholesaleTaxExempt()) {
2802            return shippingTaxes;
2803        }
2804
2805        if ($scope.InitInfo.TaxShipping != 'yes' && !$scope.useCanadaTax() && !($scope.HasDynamicTaxes && $scope.isDynamicTaxServiceEnabled())) {
2806            return shippingTaxes;
2807        }
2808
2809        var transaction_jurisdictional_taxes = {};
2810        if ($scope.JurisdictionalTaxes) {
2811            transaction_jurisdictional_taxes = $scope.JurisdictionalTaxes;
2812            if (transaction_jurisdictional_taxes['Shipping']) {
2813                delete transaction_jurisdictional_taxes['Shipping'];
2814            }
2815        }
2816
2817        // Hard coding Shipping tax bases on Product tax for now until we know something more
2818        var tax_bitmap = $scope.getProductTaxBitmap();
2819
2820        var useStoreTaxTable = $scope.taxAtCountryLevel();
2821        var info = $scope.calculateBitmapTax(tax_bitmap, shipping, transaction_jurisdictional_taxes, useStoreTaxTable, true);
2822        $scope.setTransactionJurisdictionTaxes(info.transaction_jurisdictional_taxes);
2823        $scope.shipping_tax_jurisdictions = info.line_jurisdictional_taxes;
2824        return $scope.RoundTo2Dec(info.line_tax);
2825    };
2826
2827    $scope.calculateShippingTaxAmount = function (shipping) {
2828        var shippingTaxes = 0;
2829        shipping = +shipping;
2830        if (!shipping || shipping < 0 || $scope.isWholesaleTaxExempt()) {
2831            return shippingTaxes;
2832        }
2833
2834        if ($scope.InitInfo.TaxShipping != 'yes' && !$scope.useCanadaTax()) {
2835            return shippingTaxes;
2836        }
2837
2838        var transaction_jurisdictional_taxes = {};
2839        if ($scope.JurisdictionalTaxes && $scope.JurisdictionalTaxes && transaction_jurisdictional_taxes.Shipping) {
2840            return $scope.RoundTo2Dec(transaction_jurisdictional_taxes.Shipping.tax_paid);
2841        }
2842
2843        // Hard coding Shipping tax bases on Product tax for now until we know something more
2844        var tax_bitmap = $scope.getProductTaxBitmap();
2845
2846        var useStoreTaxTable = $scope.taxAtCountryLevel();
2847        var info = $scope.calculateBitmapTax(tax_bitmap, shipping, transaction_jurisdictional_taxes, useStoreTaxTable, true);
2848        return $scope.RoundTo2Dec(info.line_tax);
2849    };
2850
2851    $scope.taxAtCountryLevel = function () {
2852        var storeCountry = $scope.getStoreCountryKey();
2853        var shippingCountry = $scope.getShippingCountryKey();
2854        if (storeCountry == shippingCountry) {
2855            switch (storeCountry) {
2856                case 'CAN':
2857                    return (!$scope.InitInfo.useCanadaTax);
2858                case 'AUS':
2859                    return true;
2860            }
2861        }
2862        return false;
2863    };
2864
2865    // alternativeState can be used for example for subscription taxes calculation
2866    // where we need to calculate taxes separately for subscription with some delivery method
2867    // and for previous products with another delivery method
2868    $scope.getTaxTable = function (useStoreTaxTable, alternativeState, taxBitmap) {
2869        var taxTable = null;
2870        var state = null;
2871        if ($scope.isPickup()) {
2872            useStoreTaxTable = true;
2873        }
2874        if ($scope.InitInfo.TaxableRegions) {
2875            if (alternativeState !== null && typeof alternativeState !== 'undefined') {
2876                state = alternativeState;
2877            } else if ($scope.digitalOrder && $scope.BillingInfo.State) {
2878                state = $scope.BillingInfo.State;
2879            } else if (useStoreTaxTable) {
2880                if ($scope.isPickup() && $scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.address && $scope.ShippingInfo.Option.address.state) {
2881                    state = $scope.ShippingInfo.Option.address.state;
2882                } else if ($scope.InitInfo.Store) {
2883                    state = $scope.InitInfo.Store.state;
2884                }
2885            } else {
2886                state = $scope.ShippingInfo.State;
2887            }
2888            if ($scope.getStoreCountryKey() === $scope.getShippingCountryKey()
2889                || $scope.taxAtCountryLevel()
2890                || $scope.digitalOrder
2891            ) {
2892                if (state && $scope.InitInfo.TaxableRegions[state]) {
2893                    if ($scope.knobby['module_ignore_empty_tax_rates']) {
2894                        taxTable = angular.copy($scope.InitInfo.TaxableRegions[state]);
2895                        for (let bitmap in taxTable.rates) {
2896                            let jurisdiction = taxTable.rates[bitmap];
2897                            if (jurisdiction.rate <= 0 && jurisdiction.tax_type !== 'dynamic rate') {
2898                                delete taxTable.rates[bitmap];
2899                            }
2900                        }
2901                    } else {
2902                        taxTable = $scope.InitInfo.TaxableRegions[state];
2903                    }
2904                }
2905            }
2906        }
2907
2908        if (taxTable === null && $scope.ShippingInfo.State && !$scope.digitalOrder) {
2909            taxTable = $scope.InitInfo.TaxableRegions[$scope.ShippingInfo.State];
2910        }
2911
2912        $scope.HasDynamicTaxes = false;
2913
2914        if (taxTable) {
2915            var rates = taxTable.rates;
2916            for (var bitmap in rates) {
2917                var rate = rates[bitmap];
2918                if (rate.tax_type === 'dynamic rate') {
2919                    $scope.HasDynamicTaxes = true;
2920                    break;
2921                }
2922            }
2923        }
2924
2925        if (taxTable && useStoreTaxTable) {
2926            var hasNonDynamicTax = false;
2927            var rates = taxTable.rates;
2928            for (var bitmap in rates) {
2929                var rate = rates[bitmap];
2930                if (+bitmap & +taxBitmap) {
2931                    if (rate.tax_type !== 'dynamic rate') {
2932                        hasNonDynamicTax = true;
2933                        break;
2934                    }
2935                }
2936            }
2937
2938            // Ignore store taxes when dynamic taxes are presented
2939            if (hasNonDynamicTax && !$scope.HasDynamicTaxes) {
2940                return taxTable;
2941            }
2942        }
2943
2944        if (!$scope.InitInfo.TaxableRegions) {
2945            return taxTable;
2946        }
2947
2948        //dynamic_id == 2 Cali
2949        if (typeof $scope.InitInfo.TaxableRegions['California'] !== 'undefined') {
2950            if (state && state == 'CA') {
2951                var caData = $scope.InitInfo.TaxableRegions['California'];
2952                var rates = caData['rates'];
2953                for (var bitmap in rates) {
2954                    var taxInfo = rates[bitmap];
2955                    if (typeof taxInfo['dynamic_id'] !== 'undefined' && taxInfo['dynamic_id'] == '2') {
2956                        taxTable = caData;
2957                        $scope.HasDynamicTaxes = true;
2958                        break;
2959                    }
2960                }
2961            }
2962        }
2963
2964        //dynamic_id == 3 Taxjar
2965        if (typeof $scope.InitInfo.TaxableRegions['All'] !== 'undefined') {
2966            var taxJarData = $scope.InitInfo.TaxableRegions['All'];
2967            var rates = taxJarData['rates'];
2968            for (var bitmap in rates) {
2969                var taxInfo = rates[bitmap];
2970                if (typeof taxInfo['dynamic_id'] !== 'undefined' && taxInfo['dynamic_id'] == '3') {
2971                    taxTable = taxJarData;
2972                    $scope.HasDynamicTaxes = true;
2973                    break;
2974                }
2975            }
2976        }
2977
2978        //dynamic_id == 4 Avalara
2979        if (typeof $scope.InitInfo.TaxableRegions['All'] !== 'undefined') {
2980            var avalaraData = $scope.InitInfo.TaxableRegions['All'];
2981            var rates = avalaraData['rates'];
2982
2983            for (var bitmap in rates) {
2984                var taxInfo = rates[bitmap];
2985                if (typeof taxInfo['dynamic_id'] !== 'undefined' && taxInfo['dynamic_id'] == '4') {
2986                    taxTable = avalaraData;
2987                    $scope.HasDynamicTaxes = true;
2988
2989                    break;
2990                }
2991            }
2992        }
2993
2994        return taxTable;
2995    };
2996
2997    $scope.setTransactionJurisdictionTaxes = function (tjtArray) {
2998        if (!tjtArray) {
2999            tjtArray = {};
3000        }
3001        $scope.JurisdictionalTax = JSON.stringify(tjtArray);
3002        $scope.JurisdictionalTaxes = tjtArray;
3003        var jurisdictional_taxes = $scope.JurisdictionalTaxes;
3004        if (jurisdictional_taxes['Shipping']) {
3005            delete jurisdictional_taxes['Shipping'];
3006        }
3007    };
3008
3009    $scope.updateDynamicTaxes = function () {
3010        // Update dynamic taxes flag
3011        var taxTable = $scope.getTaxTable($scope.calculateWaTaxesBasedOnStoreLocationAddress());
3012
3013        if (!$scope.HasDynamicTaxes) {
3014            return $scope.NoOpPromise();
3015        }
3016
3017        if (!taxTable || !taxTable.rates) {
3018            return $scope.NoOpPromise();
3019        }
3020
3021        var requestDynamicTaxes = function (rate) {
3022            $scope.setProcessing(true);
3023
3024            var dynamicTaxRequest = $scope.isDynamicTaxServiceEnabled() ? $scope.getDynamicRateForOrder(rate) : $scope.getDynamicRate(rate);
3025
3026            return dynamicTaxRequest.then(function () {
3027                $scope.TotalsCalculate();
3028
3029                $scope.safeApply();
3030            }).finally(function () {
3031                $scope.setProcessing(false);
3032            });
3033        };
3034
3035        var dynamicRates = Object.entries(taxTable.rates)
3036            .map(function (entry) {
3037                var bitmap = entry[0];
3038                var rate = entry[1];
3039
3040                return Object.assign({}, rate, {
3041                    bitmap: bitmap
3042                });
3043            })
3044            .filter(function (rate) {
3045                return rate.tax_type === 'dynamic rate';
3046            });
3047
3048        if (dynamicRates.length) {
3049            return requestDynamicTaxes(dynamicRates[0]);
3050        }
3051
3052        return $scope.NoOpPromise();
3053    };
3054
3055    $scope.updateDynamicTaxesOnce = function () {
3056        if (!window.PromiseAccumulator) {
3057            return $scope.updateDynamicTaxes();
3058        }
3059
3060        if (!$scope.dynamicTaxesPromiseAccumulator) {
3061            $scope.dynamicTaxesPromiseAccumulator = new PromiseAccumulator(function () {
3062                return $scope.updateDynamicTaxes();
3063            });
3064        }
3065
3066        if (!$scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
3067            return $scope.dynamicTaxesPromiseAccumulator.getPromise();
3068        }
3069
3070        var promise = $scope.dynamicTaxesPromiseAccumulator.getPromise();
3071        $scope.dynamicTaxesInFlightPromise = promise;
3072
3073        promise.finally(function () {
3074
3075            if ($scope.dynamicTaxesInFlightPromise === promise) {
3076                $scope.dynamicTaxesInFlightPromise = null;
3077            }
3078        });
3079
3080        return promise;
3081    };
3082
3083    $scope.getDynamicRate = function (rate) {
3084        var token = $scope.InitInfo.store_token;
3085        var url = $scope.RainApiUrl + 'store/taxes/dynamic-jurisdiction';
3086        var headers = {};
3087        if (token) {
3088            headers.Authorization = 'Bearer ' + token;
3089        }
3090
3091        var destination = $scope.digitalOrder ? $scope.BillingInfo : $scope.ShippingInfo;
3092        var street1 = null;
3093        if (destination.Address1) {
3094            street1 = destination.Address1;
3095        }
3096
3097        var street2 = null;
3098        if (destination.Address2) {
3099            street2 = destination.Address2;
3100        }
3101
3102        var city = null;
3103        if (destination.City) {
3104            city = destination.City;
3105        }
3106
3107        var state = null;
3108        if (destination.State) {
3109            state = destination.State;
3110        }
3111
3112        var zip = null;
3113        if (destination.Zip) {
3114            zip = destination.Zip;
3115        }
3116
3117        var country = null;
3118        if (destination.Country) {
3119            country = destination.Country;
3120        }
3121
3122        var shouldUseStoreAddress = $scope.calculateWaTaxesBasedOnStoreLocationAddress() && (!zip || zip === '');
3123
3124        if (shouldUseStoreAddress && $scope.InitInfo && $scope.InitInfo.Store) {
3125            var store = $scope.InitInfo.Store;
3126            street1 = store.address1 || street1;
3127            street2 = store.address2 || street2;
3128            city = store.city || city;
3129            state = store.state || state;
3130            zip = store.zip || zip;
3131            country = store.country || $scope.InitInfo.DomesticCountry || country;
3132        }
3133
3134        if ($scope.isPickup()) {
3135            var option = $scope.ShippingInfo.Option;
3136            if (option.address) {
3137                street1 = option.address.street1;
3138                street2 = option.address.street2;
3139                city = option.address.city;
3140                state = option.address.state;
3141                zip = option.address.postalCode;
3142                country = option.address.country;
3143            }
3144        }
3145
3146        // no zip, nothing to do
3147        if (typeof zip === 'undefined' || zip === null || zip === '') {
3148            return $scope.NoOpPromise();
3149        }
3150
3151        // string together the URL params, will also be used as the cache key
3152        var params = '';
3153        params += '?zip=' + zip;
3154        params += '&tax_type_id=' + rate.pos_tax_type_id;
3155        if (city) {
3156            params += '&city=' + city;
3157        }
3158        if (street1) {
3159            params += '&addr=' + street1;
3160            if (street2) {
3161                params += ' ' + street2;
3162            }
3163        }
3164        if (state) {
3165            params += '&state=' + state;
3166        }
3167        if (country) {
3168            params += '&country=' + country;
3169        }
3170
3171        // have we made this request before?
3172        var key = params;
3173        if ($scope.useDynamicTaxesCache() && key === $scope.dynamicRateCacheKey && $scope.hasDynamicRateCache()) {
3174            console.log("Use cached rate for key " + $scope.dynamicRateCacheKey);
3175            return $scope.NoOpPromise($scope.dynamicRateCache);
3176        }
3177
3178        // slap the params on the url
3179        url += params;
3180
3181        var request = $http({
3182            method: 'GET',
3183            url: url,
3184            headers: headers
3185        });
3186
3187        return request.then(function (result) {
3188            if (result.data.error) {
3189                $scope.dynamicTaxesReady = false;
3190
3191                return Promise.reject(new Error(result.data.error));
3192            } else {
3193                $scope.dynamicRateCacheKey = key;
3194                $scope.dynamicRateCache = result.data;
3195
3196                return result.data;
3197            }
3198        }, function (error) {
3199            $scope.dynamicTaxesReady = false;
3200
3201            throw new Error(error.data.error);
3202        });
3203    };
3204
3205    $scope.invalidateDynamicTaxCache = function() {
3206        $scope.dynamicRateCache = null;
3207        $scope.dynamicRateCacheKey = null;
3208    };
3209
3210    $scope.hasDynamicRateCache = function () {
3211        if (!$scope.dynamicRateCache) {
3212            return false;
3213        }
3214
3215        if (Array.isArray($scope.dynamicRateCache)) {
3216            return $scope.dynamicRateCache.length > 0;
3217        }
3218
3219        if (typeof $scope.dynamicRateCache === 'object') {
3220            return Object.keys($scope.dynamicRateCache).length > 0;
3221        }
3222
3223        return true;
3224    };
3225
3226    $scope.useDynamicTaxesCache = function() {
3227        var allowCacheForDynamicTaxes = ['WA State'];
3228
3229        if ($scope.HasDynamicTaxes && $scope.isDynamicTaxServiceEnabled() && !allowCacheForDynamicTaxes.includes($scope.InitInfo.dynamic_tax_service)) {
3230            return false;
3231        }
3232
3233        return true;
3234    };
3235
3236    $scope.getDynamicRateForOrder = function (rate) {
3237        if (!$scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
3238            $scope.StatusInfo.DynamicTaxError = ''; //we need to reset vars like that before each request
3239            $scope.StatusInfo.DynamicTaxAddressError = '';
3240        }
3241
3242        var isStaleDynamicTaxResponse = function () {
3243            return false;
3244        };
3245
3246        if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
3247            var dynamicTaxRequestId = $scope.dynamicTaxRequestId || 0;
3248
3249            isStaleDynamicTaxResponse = function () {
3250                return dynamicTaxRequestId !== ($scope.dynamicTaxRequestId || 0);
3251            };
3252        }
3253
3254        $scope.status.error = '';
3255        var token = $scope.InitInfo.store_token;
3256        var url = $scope.RainApiUrl + 'store/taxes/dynamic-jurisdiction';
3257        var headers = {};
3258        if (token) {
3259            headers.Authorization = 'Bearer ' + token;
3260        }
3261
3262        $scope.invalidateDynamicTaxCache();
3263
3264        var destination = $scope.digitalOrder ? $scope.BillingInfo : $scope.ShippingInfo;
3265
3266        var zip = destination.Zip;
3267        var state = destination.State;
3268        var city = destination.City;
3269        var country = destination.Country;
3270
3271        if ($scope.isPickup() && $scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.address) {
3272            zip = $scope.ShippingInfo.Option.address.postalCode;
3273            state = $scope.ShippingInfo.Option.address.state;
3274            city = $scope.ShippingInfo.Option.address.city;
3275            country = $scope.InitInfo.Countries[$scope.ShippingInfo.Option.address.country] || $scope.ShippingInfo.Option.address.country;
3276        }
3277
3278        if (!zip || !state || !city) {
3279            return $scope.NoOpPromise();
3280        }
3281
3282        // When the WA taxes glitch flag is enabled and dynamic tax caching is allowed,
3283        // only invalidate the dynamic tax cache when the relevant address inputs or tax type change.
3284        if ($scope.useDynamicTaxesCache) {
3285            var address1 = (destination.Address1 || destination.address1 || destination.Street || destination.street || '');
3286            var address2 = (destination.Address2 || destination.address2 || destination.street2 || '');
3287
3288            if ($scope.isPickup() && $scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.address) {
3289                var pickupAddress = $scope.ShippingInfo.Option.address;
3290                address1 = (pickupAddress.Address1 || pickupAddress.address1 || pickupAddress.line1 || pickupAddress.street || address1);
3291                address2 = (pickupAddress.Address2 || pickupAddress.address2 || pickupAddress.line2 || pickupAddress.street2 || address2);
3292            }
3293
3294            var taxTypeId = null;
3295            if (rate && rate.taxTypeId) {
3296                taxTypeId = rate.taxTypeId;
3297            } else if ($scope.InitInfo && $scope.InitInfo.dynamic_tax_type_id ) {
3298                taxTypeId = $scope.InitInfo.dynamic_tax_type_id;
3299            }
3300
3301            var dynamicTaxCacheKey = [
3302                zip || '',
3303                state || '',
3304                city || '',
3305                country || '',
3306                address1 || '',
3307                address2 || '',
3308                taxTypeId || ''
3309            ].join('|');
3310
3311            if ($scope.lastDynamicTaxCacheKey !== dynamicTaxCacheKey) {
3312                $scope.invalidateDynamicTaxCache();
3313                $scope.lastDynamicTaxCacheKey = dynamicTaxCacheKey;
3314            }
3315        }
3316        var shippingPrice = 0;
3317
3318        if (parseFloat(destination.OptionId) && destination.Options !== null && (destination.Options[destination.OptionId] !== undefined && destination.Options[destination.OptionId] !== null)) {
3319            shippingPrice = parseFloat(destination.Options[destination.OptionId].price)
3320        }
3321
3322        // have we made this request before?
3323        var key = zip + ':' + state + ':' + city;
3324        if (destination.OptionId) {
3325            key += destination.OptionId;
3326        }
3327
3328        if ($scope.useDynamicTaxesCache() && key === $scope.dynamicRateCacheKey && $scope.hasDynamicRateCache()) {
3329            console.log("Use cached rate for key " + $scope.dynamicRateCacheKey);
3330            return $scope.NoOpPromise($scope.dynamicRateCache);
3331        }
3332
3333        var street = '';
3334        if (destination.Address1) {
3335            street += destination.Address1;
3336        }
3337        if (destination.Address2) {
3338            street += ' ' + destination.Address2;
3339        }
3340
3341        var items = [];
3342        var shipping = {
3343            street: street,
3344            city: city,
3345            state: state,
3346            zip: zip,
3347            country: country,
3348            price: shippingPrice
3349        };
3350
3351        //Create line for sending it to tax service
3352        for (var i = 0; i < $scope.CartData.Items.length; i++) {
3353            var line = $scope.CartData.Items[i];
3354            var type = line.type.toLowerCase();
3355            if (type == 'pslr') {
3356                type = 'product';
3357            }
3358
3359            // Class taxes handled in a different way
3360            if (type === 'class') {
3361                continue;
3362            }
3363
3364            var lineObj = {
3365                title: line.title,
3366                qty: parseFloat(line.qty),
3367                price: parseFloat(line.price),
3368                type: type,
3369                productId: parseInt(line.product_id),
3370                pslrId: line.pslr_id ? parseInt(line.pslr_id) : null,
3371            };
3372
3373            items.push(lineObj);
3374        }
3375
3376        if ($scope.checkFlagRetailDeliveryFee() && shipping.state === 'CO') {
3377            var lineRetailDeliveryFee = {
3378                title: 'RetailDeliveryFee',
3379                qty: 1,
3380                productId: null,
3381                price: 0,
3382                type: 'shipping_fee',
3383            };
3384            items.push(lineRetailDeliveryFee);
3385        }
3386
3387        var orderData = {
3388            shipping: shipping,
3389            items: items,
3390            taxTypeId: parseInt(rate.pos_tax_type_id)
3391        }
3392
3393        return $http.post(url, orderData, {headers: headers}).then(function (result) {
3394            if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure() && isStaleDynamicTaxResponse()) {
3395                return;
3396            }
3397
3398            if (result.data.error) {
3399                if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
3400                    $scope.StatusInfo.DynamicTaxError = result.data.error.message || result.data.error;
3401                    $scope.dynamicTaxesReady = false;
3402                } else {
3403                    $scope.status.error = result.data.error;
3404                }
3405            } else {
3406                if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
3407                    $scope.StatusInfo.DynamicTaxError = '';
3408                    $scope.StatusInfo.DynamicTaxAddressError = '';
3409                    $scope.dynamicTaxesReady = true;
3410                }
3411
3412                $scope.dynamicRateCacheKey = key;
3413                $scope.dynamicTotalTax = parseFloat(result.data.totalTax);
3414                $scope.dynamicRateCache = result.data.rates;
3415                if ($scope.checkFlagAvalaraTaxCodeIssue()) {
3416                    for(var i = 0; i < result.data.lines_applied_jurisdictions?.length; i++) {
3417                        if (result.data.lines_applied_jurisdictions[i][0].taxable_amount === 0){
3418                            $scope.CartData.Items[i].nonTaxableItem = true;
3419                        } else {
3420                            $scope.CartData.Items[i].nonTaxableItem = false;
3421                        }
3422                    }
3423                }
3424                if ($scope.checkFlagRetailDeliveryFee()) {
3425                    Object.values(result.data.lines_applied_jurisdictions).forEach(function (items) {
3426                        items.forEach(function (item) {
3427                            if (item.jurisdiction.title === 'Retail Delivery Fee') {
3428                                $scope.retailDeliveryFeeTaxJurisdiction = item.jurisdiction;
3429                            }
3430                        });
3431                    });
3432                }
3433                return result.data.rates;
3434            }
3435        }, function (result) {
3436            if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure() && isStaleDynamicTaxResponse()) {
3437                return;
3438            }
3439
3440            if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
3441                $scope.dynamicTaxesReady = false;
3442            }
3443
3444            if (result.data) {
3445                if (result.data.error && result.data.error.error_code && result.data.error.error_code === 'address_error') {
3446                    $scope.StatusInfo.DynamicTaxAddressError = 'We were unable to match your address, please check address';
3447                } else {
3448                    if (typeof result.data.error !== "undefined" && typeof result.data.error.message !== "undefined") {
3449                        $scope.StatusInfo.DynamicTaxError = result.data.error.message;
3450                    } else {
3451                        $scope.StatusInfo.DynamicTaxError = result.data;
3452                    }
3453                }
3454            } else {
3455                $scope.StatusInfo.DynamicTaxError = "An unhandled error has occurred: Dynamic Tax";
3456            }
3457            $scope.dynamicTotalTax = 0;
3458            $scope.dynamicRateCache = null;
3459            $scope.dynamicRateCacheKey = null;
3460        });
3461    };
3462
3463    $scope.useCanadaTax = function () {
3464        return ($scope.InitInfo.useCanadaTax && $scope.getShippingCountryKey() === 'CAN');
3465    };
3466
3467    $scope.useWashingtonTax = function () {
3468        return $scope.InitInfo.useWashingtonTax;
3469    };
3470
3471    // keep in sync with Cart.model
3472    $scope.getDiscountableTotalForPslrLimitedCoupon = function (coupon) {
3473        var discountable = 0;
3474        var discountableQty = 0;
3475
3476        for (var i = 0; i < $scope.CartData.Items.length; i++) {
3477            var item = $scope.CartData.Items[i];
3478
3479            if (item.type != 'pslr' || item.discount_eligible == 'false') {
3480                continue;
3481            }
3482            var pslr = item.id;
3483            for (var p = 0; p < coupon.pslrIdArray.length; p++) {
3484                var valid = coupon.pslrIdArray[p];
3485                console.log('compare ' + valid + ' to ' + pslr);
3486                if (valid == pslr) {
3487                    if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3488                        coupon.disabledForSalePriceItem = true;
3489                        continue;
3490                    }
3491
3492                    item.discount_applied = true;
3493                    discountable += item.total;
3494
3495                    if ($scope.knobby['module_better_coupons']) {
3496                        if (coupon.appliedLineUuid.indexOf(item.uuid) === -1) {
3497                            coupon.appliedLineUuid.push(item.uuid);
3498                        }
3499                    }
3500
3501                    break;
3502                }
3503            }
3504        }
3505
3506        if ($scope.isDollarOffFiltersAllowed()) {
3507            return {discountable: discountable, discountableQty: discountableQty};
3508        }
3509
3510        return discountable;
3511    };
3512
3513    $scope.getDiscountableTotalForClassLimitedCoupon = function (coupon) {
3514        var discountable = 0;
3515        var discountableQty = 0;
3516
3517        for (var i = 0; i < $scope.CartData.Items.length; i++) {
3518            var item = $scope.CartData.Items[i];
3519
3520            if (item.type != 'class' || item.discount_eligible == 'false') {
3521                continue;
3522            }
3523            var classId = item.quiltClassId;
3524            for (var p = 0; p < coupon.classIdArray.length; p++) {
3525                var valid = coupon.classIdArray[p];
3526                if (valid == classId) {
3527                    if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3528                        coupon.disabledForSalePriceItem = true;
3529                        continue;
3530                    }
3531
3532                    item.discount_applied = true;
3533                    discountable += item.total;
3534
3535                    if ($scope.knobby['module_better_coupons']) {
3536                        if (coupon.appliedLineUuid.indexOf(item.uuid) === -1) {
3537                            coupon.appliedLineUuid.push(item.uuid);
3538                        }
3539                    }
3540
3541                    break;
3542                }
3543            }
3544        }
3545
3546        if ($scope.isDollarOffFiltersAllowed()) {
3547            return {discountable: discountable, discountableQty: discountableQty};
3548        }
3549
3550        return discountable;
3551    };
3552
3553    // keep in sync with Cart.model
3554    $scope.getDiscountableTotalForDeptLimitedCoupon = function (coupon) {
3555        var discountable = 0;
3556        var discountableQty = 0;
3557
3558        console.info("Applying Department Coupon");
3559        console.group();
3560        console.info({s: 'Dep Array', o: coupon.departmentIdArray}
3560);
3561        for (var i = 0; i < $scope.CartData.Items.length; i++) {
3562            var item = $scope.CartData.Items[i];
3563
3564            if (item.type !== 'pslr' || item.discount_eligible == 'false' || typeof item.departmentId === 'undefined') {
3565                console.info({s: "Item Not discountable", o: item});
3566                continue;
3567            }
3568            var deptId = parseInt(item.departmentId);
3569            for (var d = 0; d < coupon.departmentIdArray.length; d++) {
3570                var valid = coupon.departmentIdArray[d];
3571                if (valid == deptId) {
3572                    if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3573                        coupon.disabledForSalePriceItem = true;
3574                        continue;
3575                    }
3576
3577                    discountable += item.total;
3578                    item.discount_applied = true;
3579
3580                    if ($scope.knobby['module_better_coupons']) {
3581                        if (coupon.appliedLineUuid.indexOf(item.uuid) === -1) {
3582                            coupon.appliedLineUuid.push(item.uuid);
3583                        }
3584                    }
3585
3586                    if ($scope.isDollarOffFiltersAllowed()) {
3587                        discountableQty++;
3588                    }
3589
3590                    console.info({s: "item discounted", o: item});
3591                    break;
3592                }
3593            }
3594        }
3595        console.groupEnd();
3596
3597        if ($scope.isDollarOffFiltersAllowed()) {
3598            return {discountable: discountable, discountableQty: discountableQty};
3599        }
3600
3601        return discountable;
3602    };
3603
3604    $scope.getDiscountableTotalForItemTypesLimitedCoupon = function (coupon) {
3605        var discountable = 0;
3606        var discountableQty = 0;
3607        var itemType;
3608
3609        for (var i = 0; i < $scope.CartData.Items.length; i++) {
3610            var item = $scope.CartData.Items[i];
3611
3612            if ($scope.knobby['module_better_coupons']) {
3613                if (item.discount_eligible == 'false' || typeof item.type === 'undefined') {
3614                    continue;
3615                }
3616            } else {
3617                if (item.package_id || item.discount_eligible == 'false' || typeof item.type === 'undefined') {
3618                    continue;
3619                }
3620            }
3621
3622            if (!is_set(item.type)) {
3623                continue;
3624            }
3625
3626            if (item.product_id) {
3627                itemType = 'products';
3628            } else if (item.type === 'class' && item.package_id && item.package_id > 0) {
3629                itemType = 'packages';
3630            } else if (item.type === 'class') {
3631                itemType = 'classes';
3632            }
3633
3634            var itemDiscountable = 0;
3635
3636            if (coupon.itemTypesArray.indexOf(itemType) !== -1) {
3637                if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3638                    coupon.disabledForSalePriceItem = true;
3639                    continue;
3640                }
3641
3642                item.discount_applied = true;
3643
3644                if ($scope.knobby['module_better_coupons']) {
3645                    itemDiscountable += item.qty * $scope.getItemPriceForWeb(item);
3646                    discountable += item.qty * $scope.getItemPriceForWeb(item);
3647
3648                    if (coupon.appliedLineUuid.indexOf(item.uuid) === -1) {
3649                        coupon.appliedLineUuid.push(item.uuid);
3650                    }
3651                } else {
3652                    itemDiscountable += item.total;
3653                    discountable += item.total;
3654                }
3655
3656                if ($scope.isDollarOffFiltersAllowed()) {
3657                    discountableQty++;
3658                }
3659            }
3660        }
3661
3662        if ($scope.isDollarOffFiltersAllowed()) {
3663            return {discountable: discountable, discountableQty: discountableQty};
3664        }
3665
3666        return discountable;
3667    };
3668
3669    // keep in sync with Cart.model
3670    $scope.getDiscountableTotalForCategoryLimitedCoupon = function (coupon) {
3671        var discountable = 0;
3672        var discountableQty = 0;
3673
3674        console.log(coupon.categoryIdArray);
3675
3676        // Each item in the cart
3677        for (var i = 0; i < $scope.CartData.Items.length; i++) {
3678            var item = $scope.CartData.Items[i];
3679            if (item.type != 'pslr' || item.discount_eligible == 'false' || typeof item.categoryIdArray === 'undefined') {
3680                continue;
3681            }
3682            console.log(item);
3683            var itemCategoryIds = item.categoryIdArray;
3684
3685            // this item doesn't need checked
3686            if (itemCategoryIds === null) {
3687                continue;
3688            }
3689
3690            // Each coupon
3691            var itemDiscountable = 0;
3692            for (var c = 0; c < coupon.categoryIdArray.length; c++) {
3693                var couponCategoryId = parseInt(coupon.categoryIdArray[c]);
3694
3695                // each category this item is in - does it match the coupon category?
3696                for (var ic = 0; ic < itemCategoryIds.length; ic++) {
3697                    var catId = parseInt(itemCategoryIds[ic]);
3698                    console.log('compare ' + couponCategoryId + ' to ' + catId);
3699                    if (couponCategoryId == catId) {
3700                        if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3701                            coupon.disabledForSalePriceItem = true;
3702                            continue;
3703                        }
3704
3705                        item.discount_applied = true;
3706                        itemDiscountable += item.total;
3707                        discountable += item.total;
3708
3709                        if ($scope.knobby['module_better_coupons']) {
3710                            if (coupon.appliedLineUuid.indexOf(item.uuid) === -1) {
3711                                coupon.appliedLineUuid.push(item.uuid);
3712                            }
3713                        }
3714
3715                        if ($scope.isDollarOffFiltersAllowed()) {
3716                            discountableQty++;
3717                        }
3718
3719                        break;
3720                    }
3721                }
3722                if (itemDiscountable > 0) {
3723                    break;
3724                }
3725            }
3726        }
3727
3728        if ($scope.isDollarOffFiltersAllowed()) {
3729            return {discountable: discountable, discountableQty: discountableQty};
3730        }
3731
3732        return discountable;
3733    };
3734
3735    $scope.getDiscountableTotalForVendorLimitedCoupon = function (coupon) {
3736        var discountable = 0;
3737        var discountableQty = 0;
3738
3739        $scope.CartData.Items.forEach(function (item) {
3740            if (item.type !== 'pslr' || item.discount_eligible === 'false' || typeof item.pos_vendor_id === 'undefined' || item.pos_vendor_id === null) {
3741                return;
3742            }
3743
3744            coupon.vendorIdArray = coupon.vendorIdArray.map(function (vendorId) {
3745                return parseInt(vendorId);
3746            });
3747
3748            if (coupon.vendorIdArray.indexOf(item.pos_vendor_id) !== -1) {
3749                if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3750                    coupon.disabledForSalePriceItem = true;
3751                    return;
3752                }
3753
3754                item.discount_applied = true;
3755                discountable += item.total;
3756
3757                if ($scope.knobby['module_better_coupons']) {
3758                    if (coupon.appliedLineUuid.indexOf(item.uuid) === -1) {
3759                        coupon.appliedLineUuid.push(item.uuid);
3760                    }
3761                }
3762
3763                if ($scope.isDollarOffFiltersAllowed()) {
3764                    discountableQty++;
3765                }
3766            }
3767        });
3768
3769        if ($scope.isDollarOffFiltersAllowed()) {
3770            return {discountable: discountable, discountableQty: discountableQty};
3771        }
3772
3773        return discountable;
3774    };
3775
3776    $scope.getDiscountableTotalForMnfLimitedCoupon = function (coupon) {
3777        var discountable = 0;
3778        var discountableQty = 0;
3779
3780        $scope.CartData.Items.forEach(function (item) {
3781            if (item.type !== 'pslr' || item.discount_eligible === 'false' || typeof item.manufacturer_name === 'undefined' || item.manufacturer_name === null) {
3782                return;
3783            }
3784
3785            if (coupon.manufacturerNameArray.indexOf(item.manufacturer_name) !== -1) {
3786                if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3787                    coupon.disabledForSalePriceItem = true;
3788                    return;
3789                }
3790
3791                item.discount_applied = true;
3792                discountable += item.total;
3793
3794                if ($scope.knobby['module_better_coupons']) {
3795                    if (coupon.appliedLineUuid.indexOf(item.uuid) === -1) {
3796                        coupon.appliedLineUuid.push(item.uuid);
3797                    }
3798                }
3799
3800                if ($scope.isDollarOffFiltersAllowed()) {
3801                    discountableQty++;
3802                }
3803            }
3804        });
3805
3806        if ($scope.isDollarOffFiltersAllowed()) {
3807            return {discountable: discountable, discountableQty: discountableQty};
3808        }
3809
3810        return discountable;
3811    };
3812
3813    function isDiscountable(item) {
3814        return item.type != 'gc' && (item.discount_eligible === true || item.discount_eligible === 'true');
3815    }
3816
3817//help solve the magic penny issue with discounts
3818    $scope.MagicPennyDiscount = function(discount_left) {
3819        discount_left = $scope.RoundTo2Dec(discount_left);
3820        if (discount_left != 0) {
3821            //we are ignoring a few pennies from the discount so lets put them into the first available discountable item
3822            for (var j = 0; j < $scope.CartData.Items.length; j++) {
3823                var item = $scope.CartData.Items[j];
3824                if(discount_left < 0 ){
3825                    var disc = -.01;
3826                } else {
3827                    var disc = .01;
3828                }
3829                if (isDiscountable(item) && item.discount_applied && (item.total - item.discount - disc >= 0)) {
3830                    //subtract one penny at a time add it to each discount until we are out of discount
3831
3832                    discount_left -= disc;
3833                    discount_left = Math.round(discount_left * 100) / 100;
3834                    disc = Math.round(disc * 100) / 100;
3835                    item.discount += disc;
3836                    item.discount = Math.round(item.discount * 100) / 100;
3837                    item.discounted_total = $scope.roundNumber(item.total - item.discount, 3);
3838                }
3839                if (Math.abs(discount_left) < .01) {
3840                    break;
3841                }
3842
3843            }
3844
3845        }
3846
3847    }
3848
3849    $scope.getDiscountableTotalForDiscountableLines = function() {
3850        var discountable = 0;
3851
3852        for (var i = 0; i < $scope.CartData.Items.length; i++) {
3853            var item = $scope.CartData.Items[i];
3854            if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
3855                continue;
3856            }
3857
3858            if (isDiscountable(item)) {
3859                discountable += item.total;
3860                if (item.type === 'class' && item.package_items && Array.isArray(item.package_items) && item.package_items.length > 0) {
3861                    for (var j = 0; j < item.package_items.length; j++) {
3862                        if ($scope.knobby['module_fix_coupon_min_purchase_class_addon']) {
3863                            discountable += item.package_items[j]['price'] * item.qty;
3864                        } else {
3865                            discountable += item.package_items[j]['price'];
3866                        }
3867                    }
3868                }
3869            }
3870        }
3871
3872        return discountable;
3873    }
3874
3875    $scope.getCouponEligibilityType = function (coupon) {
3876        if (coupon.pslrIdArray && coupon.pslrIdArray.length) {
3877            return 'item';
3878        } else if (coupon.classIdArray && coupon.classIdArray.length) {
3879            return 'item';
3880        } else if (coupon.departmentIdArray && coupon.departmentIdArray.length) {
3881            return 'department';
3882        } else if (coupon.categoryIdArray && coupon.categoryIdArray.length) {
3883            return 'category';
3884        } else if (coupon.itemTypesArray && coupon.itemTypesArray.length) {
3885            return 'itemType';
3886        } else if (coupon.vendorIdArray && coupon.vendorIdArray.length) {
3887            return 'vendor';
3888        } else if (coupon.manufacturerNameArray && coupon.manufacturerNameArray.length) {
3889            return 'manufacturer';
3890        }
3891
3892        return null;
3893    }
3894
3895    $scope.isEligibleByItems = function (coupon) {
3896        const itemsIds = $scope.CartData.Items.map(function (item) {
3897            return parseInt(item.id);
3898        });
3899        var hasEligiblePslrs = coupon.pslrIdArray.some(function (pslrId) {
3900            return itemsIds.includes(parseInt(pslrId));
3901        });
3902
3903        var hasEligibleClasses = coupon.classIdArray.some(function (classId) {
3904            if (!!$scope.knobby && $scope.knobby['module_fix_web_class_coupon_id_matching']) {
3905                return $scope.CartData.Items.some(function (item) {
3906                    return item.type === 'class' && parseInt(item.quiltClassId) === parseInt(classId);
3907                });
3908            }
3909            return itemsIds.includes(parseInt(classId));
3910        });
3911
3912        return hasEligiblePslrs || hasEligibleClasses;
3913    }
3914
3915    $scope.isEligibleByDepartments = function (coupon) {
3916        const departmentsIds = $scope.CartData.Items.map(function (item) {
3917            return item.departmentId;
3918        });
3919        return coupon.departmentIdArray.some(function (departmentId) {
3920            return departmentsIds.includes(departmentId);
3921        });
3922    }
3923
3924    $scope.isEligibleByCategories = function (coupon) {
3925        const categoriesIds = $scope.CartData.Items.reduce(function (acc, item) {
3926            return acc.concat(item.categoryIdArray);
3927        }, []);
3928        return coupon.categoryIdArray.some(function (categoryId) {
3929            return categoriesIds.includes(categoryId);
3930        });
3931    }
3932
3933    $scope.isEligibleByItemTypes = function (coupon) {
3934        const itemsTypes = $scope.CartData.Items.map(function (item) {
3935            if (item.product_id) {
3936                return 'products';
3937            } else if (item.type === 'class') {
3938                return 'classes';
3939            }
3940
3941            return item.type;
3942        });
3943        return coupon.itemTypesArray.some(function (itemType) {
3944            return itemsTypes.includes(itemType);
3945        });
3946    }
3947
3948    $scope.isEligibleByVendors = function (coupon) {
3949        const vendorsIds = $scope.CartData.Items.map(function (item) {
3950            return item.pos_vendor_id;
3951        });
3952        return coupon.vendorIdArray.some(function (vendorId) {
3953            return vendorsIds.includes(vendorId);
3954        });
3955    }
3956
3957    $scope.isEligibleByManufacturers = function (coupon) {
3958        const manufacturersNames = $scope.CartData.Items.map(function (item) {
3959            return item.manufacturer_name;
3960        });
3961        return coupon.manufacturerNameArray.some(function (manufacturerName) {
3962            return manufacturersNames.includes(manufacturerName);
3963        });
3964    }
3965
3966    $scope.isEligibleCoupon = function (coupon) {
3967        switch($scope.getCouponEligibilityType(coupon)) {
3968            case 'item':
3969                return $scope.isEligibleByItems(coupon);
3970            case 'department':
3971                return $scope.isEligibleByDepartments(coupon);
3972            case 'category':
3973                return $scope.isEligibleByCategories(coupon);
3974            case 'itemType':
3975                return $scope.isEligibleByItemTypes(coupon);
3976            case 'vendor':
3977                return $scope.isEligibleByVendors(coupon);
3978            case 'manufacturer':
3979                return $scope.isEligibleByManufacturers(coupon);
3980            default:
3981                return true;
3982        }
3983    }
3984
3985    $scope.applyHighestSavingCouponsMark = function() {
3986        if ($scope.CartData?.Coupons?.length > 0) {
3987            var getCouponDiscountAbsValue = function (coupon) {
3988                var discountable = 0;
3989
3990                if ($scope.knobby['module_better_coupons']) {
3991                    discountable = $scope.getDiscountableTotalForDiscountableLines();
3992                }
3993
3994                if (coupon.couponType === 'percent_off') {
3995                    if ($scope.knobby['module_better_coupons'] && coupon.couponBaseType === CouponConstants.coupon_bogo) {
3996                        return coupon.dollarOffAmount;
3997                    }
3998
3999                    var discountableItem;
4000
4001                    if (!$scope.knobby['module_better_coupons']) {
4002                        discountable = $scope.SubTotal;
4003                    }
4004
4005                    if (coupon.pslrIdArray && coupon.pslrIdArray.length) {
4006                        if ($scope.isDollarOffFiltersAllowed()) {
4007                            discountableItem = $scope.getDiscountableTotalForPslrLimitedCoupon(coupon);
4008                            discountable = discountableItem.discountable;
4009                        } else {
4010                            discountable = $scope.getDiscountableTotalForPslrLimitedCoupon(coupon);
4011                        }
4012                    } else if (coupon.classIdArray && coupon.classIdArray.length) {
4013                        if ($scope.isDollarOffFiltersAllowed()) {
4014                            discountableItem = $scope.getDiscountableTotalForClassLimitedCoupon(coupon);
4015                            discountable = discountableItem.discountable;
4016                        } else {
4017                            discountable = $scope.getDiscountableTotalForClassLimitedCoupon(coupon);
4018                        }
4019                    } else if (coupon.departmentIdArray && coupon.departmentIdArray.length) {
4020                        if ($scope.isDollarOffFiltersAllowed()) {
4021                            discountableItem = $scope.getDiscountableTotalForDeptLimitedCoupon(coupon);
4022                            discountable = discountableItem.discountable;
4023                        } else {
4024                            discountable = $scope.getDiscountableTotalForDeptLimitedCoupon(coupon);
4025                        }
4026                    } else if (coupon.categoryIdArray && coupon.categoryIdArray.length) {
4027                        if ($scope.isDollarOffFiltersAllowed()) {
4028                            discountableItem = $scope.getDiscountableTotalForCategoryLimitedCoupon(coupon);
4029                            discountable = discountableItem.discountable;
4030                        } else {
4031                            discountable = $scope.getDiscountableTotalForCategoryLimitedCoupon(coupon);
4032                        }
4033                    } else if ($scope.knobby['module_better_coupons'] && coupon.itemTypesArray && coupon.itemTypesArray.length) {
4034                        if ($scope.isDollarOffFiltersAllowed()) {
4035                            discountableItem = $scope.getDiscountableTotalForItemTypesLimitedCoupon(coupon);
4036                            discountable = discountableItem.discountable;
4037                        } else {
4038                            discountable = $scope.getDiscountableTotalForItemTypesLimitedCoupon(coupon);
4039                        }
4040                    } else if (coupon.vendorIdArray && coupon.vendorIdArray.length) {
4041                        if ($scope.isDollarOffFiltersAllowed()) {
4042                            discountable = discountableItem.discountable;
4043                        } else {
4044                            discountable = $scope.getDiscountableTotalForVendorLimitedCoupon(coupon);
4045                        }
4046                    } else if (coupon.manufacturerNameArray && coupon.manufacturerNameArray.length) {
4047                        if ($scope.isDollarOffFiltersAllowed()) {
4048                            discountableItem = $scope.getDiscountableTotalForMnfLimitedCoupon(coupon);
4049                            discountable = discountableItem.discountable;
4050                        } else {
4051                            discountable = $scope.getDiscountableTotalForMnfLimitedCoupon(coupon);
4052                        }
4053                    }
4054
4055                    return $scope.RoundTo2Dec(discountable * coupon.percentOff / 100);
4056                } else if (coupon.couponType === 'dollar_off') {
4057                    if ($scope.knobby['module_better_coupons']) {
4058                        if ($scope.isEligibleCoupon(coupon) && $scope.SubTotal >= coupon.minDollarAmount) {
4059                            return coupon.dollarOffAmount;
4060                        }
4061                    } else {
4062                        if ($scope.SubTotal >= coupon.minDollarAmount) {
4063                            return coupon.dollarOffAmount;
4064                        }
4065                    }
4066                } else if (coupon.couponType === 'free_shipping_off') {
4067                    if ($scope.ShippingInfo.Option) {
4068                        return $scope.getShippingDiscount();
4069                    }
4070                }
4071
4072                return 0;
4073            }
4074
4075            if ($scope.knobby['module_better_coupons']) {
4076                var couponsForProcessing = $scope.CartData.Coupons;
4077
4078                if ($scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyAll') {
4079                    var couponsForProcessing = $scope.CartData.Coupons.filter(function (coupon) {
4080                        return coupon.couponBaseType === CouponConstants.coupon_bogo;
4081                    });
4082                }
4083
4084                couponsForProcessing = couponsForProcessing.filter(function(coupon) {
4085                    return $scope.couponWithCustomerGroupsIsAvailable(coupon);
4086                });
4087
4088                if (couponsForProcessing.length <= 0) {
4089                    return;
4090                }
4091
4092                let maxCoupon = couponsForProcessing.reduce((max, coupon) => {
4093                    return (Number(getCouponDiscountAbsValue(coupon)) > Number(getCouponDiscountAbsValue(max))) ? coupon : max;
4094                }, couponsForProcessing[0]);
4095
4096                var maxCouponValue = Number(getCouponDiscountAbsValue(maxCoupon));
4097                var customerDiscountValue = Number($scope.CustomerDiscounts) || 0;
4098                var isApplyHighestSaving = $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving';
4099                var customerDiscountWins = isApplyHighestSaving && customerDiscountValue >= maxCouponValue && customerDiscountValue > 0;
4100
4101                if (customerDiscountWins) {
4102                    couponsForProcessing.forEach(coupon => {
4103                        if (coupon.couponType === 'free_shipping_off') {
4104                            coupon.notEligible = true;
4105                        } else {
4106                            coupon.notApplied = true;
4107                            if (coupon.couponBaseType === CouponConstants.coupon_bogo) {
4108                                coupon.resetBogoLinesAndSuitableTiers();
4109                            }
4110                        }
4111                    });
4112                } else {
4113                    maxCoupon.notApplied = false;
4114                    maxCoupon.notEligible = false;
4115
4116                    couponsForProcessing.forEach(coupon => {
4117                        if (coupon !== maxCoupon) {
4118                            if (coupon.couponType === 'free_shipping_off') {
4119                                coupon.notEligible = true;
4120                            } else {
4121                                coupon.notApplied = true;
4122                                if (coupon.couponBaseType === CouponConstants.coupon_bogo) {
4123                                    coupon.resetBogoLinesAndSuitableTiers();
4124                                }
4125                            }
4126                        }
4127                    });
4128                    if (isApplyHighestSaving && customerDiscountValue > 0 && maxCouponValue > 0) {
4129                        $scope.CartData.Items.forEach(function (item) {
4130                            if (isDiscountable(item)) {
4131                                item.customer_discount = 0;
4132                                item.discount = 0;
4133                                item.discounted_total = $scope.getItemTotal(item);
4134                            }
4135                        });
4136                        $scope.CustomerDiscounts = 0;
4137                    }
4138                }
4139            } else {
4140                let maxCoupon = $scope.CartData.Coupons.reduce((max, coupon) => {
4141                    return (Number(getCouponDiscountAbsValue(coupon)) > Number(getCouponDiscountAbsValue(max))) ? coupon : max;
4142                }, $scope.CartData.Coupons[0]);
4143
4144                maxCoupon.notApplied = false;
4145                maxCoupon.notEligible = false;
4146
4147                $scope.CartData.Coupons.forEach(coupon => {
4148                    if (coupon !== maxCoupon) {
4149                        if (coupon.couponType === 'free_shipping_off') {
4150                            coupon.notEligible = true;
4151                        } else {
4152                            coupon.notApplied = true;
4153                        }
4154                    }
4155                });
4156            }
4157        }
4158    }
4159
4160    $scope.isCouponAvailable = function (coupon) {
4161        return !coupon.notApplied && !coupon.notEligible && coupon.discount > 0;
4162    }
4163
4164    $scope.isShippingCoupon = function (coupon) {
4165        return coupon.couponType === 'free_shipping_off';
4166    }
4167
4168    $scope.couponUnavailabilityMessage = function (coupon) {
4169        if (coupon.notEligible) {
4170            return 'Not Eligible';
4171        } else if (coupon.notApplied || coupon.discount === 0) {
4172            return 'Not Applied';
4173        }
4174
4175        return '';
4176    }
4177
4178    $scope.CalculateItemBetterDiscounts = function () {
4179        if (!$scope.CartData.Items || $scope.CartData.Items.length == 0) {
4180            $scope.CartData.Coupons = [];
4181            return 0.00;
4182        }
4183
4184        var allowStacking = $scope.InitInfo?.CheckoutSettings?.AllowDiscountStacking;
4185        var hasCoupons = $scope.CartData.Coupons && $scope.CartData.Coupons.length > 0;
4186        var shouldResetItems = $scope.CustomerDiscounts <= 0 || (!allowStacking && $scope.CustomerDiscounts > 0 && hasCoupons);
4187
4188        $scope.CartData.Items.forEach(function (item) {
4189            if (isDiscountable(item) && shouldResetItems) {
4190                item.discount = 0;
4191                item.discounted_total = $scope.getItemTotal(item);
4192                item.customer_discount = 0;
4193            }
4194        });
4195
4196        $scope.CartData.Coupons.forEach(function (coupon) {
4197            coupon.discount = 0;
4198        });
4199
4200        var discountable = 0;
4201        var itemTotal = 0;
4202        var discountableQty = 0;
4203        var discountableItem = {};
4204
4205        for (var i = 0; i < $scope.CartData.Items.length; i++) {
4206            var item = $scope.CartData.Items[i];
4207            if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
4208                continue;
4209            }
4210            if (item.type != 'gc') {
4211                if ($scope.knobby['module_fix_coupon_min_purchase_class_addon'] && item.type === 'class' && item.package_items && item.package_items.length) {
4212                    itemTotal += $scope.RoundTo2Dec($scope.getItemTotalForWeb(item));
4213                } else {
4214                    itemTotal += item.total;
4215                }
4216            }
4217            if (isDiscountable(item)) {
4218                discountable += item.total;
4219                if (item.type === 'class' && item.package_items && Array.isArray(item.package_items) && item.package_items.length > 0) {
4220                    for (var j = 0; j < item.package_items.length; j++) {
4221                        if ($scope.knobby['module_fix_coupon_min_purchase_class_addon']) {
4222                            discountable += item.package_items[j]['price'] * item.qty;
4223                        } else {
4224                            discountable += item.package_items[j]['price'];
4225                        }
4226                    }
4227                }
4228                item.discount_applied = true;
4229            }
4230        }
4231        var discountableTotalAllItems = discountable;
4232
4233        $scope.CartData.Coupons.sort(function(a, b) {
4234            function getPriority(coupon) {
4235                if (coupon.couponType === CouponConstants.coupon_dollar_off && coupon.couponBaseType !== 'bogo') {
4236                    return 0;
4237                }
4238                if (coupon.couponBaseType === 'bogo') {
4239                    return 1;
4240                }
4241                if (coupon.couponType === CouponConstants.coupon_percent && coupon.couponBaseType !== 'bogo') {
4242                    return 2;
4243                }
4244                return 3;
4245            }
4246
4247            if (a.couponBaseType === 'bogo' && b.couponBaseType === 'bogo') {
4248                if (a.notApplied === b.notApplied) {
4249                    return getPriority(a) - getPriority(b);
4250                }
4251                return a.notApplied ? 1 : -1;
4252            }
4253
4254            return getPriority(a) - getPriority(b);
4255        });
4256
4257        // Calculate the discounts off the total
4258        var customerDiscountWonInHighestSaving = $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving' &&
4259            $scope.CustomerDiscounts > 0 && $scope.CartData.Coupons && $scope.CartData.Coupons.length > 0 &&
4260            $scope.CartData.Coupons.every(function (c) { return c.notApplied || c.notEligible; });
4261
4262        if ($scope.CustomerDiscounts > 0 && (allowStacking || !$scope.CartData.Coupons || $scope.CartData.Coupons.length === 0 || customerDiscountWonInHighestSaving)) {
4263            $scope.Discounts = $scope.CustomerDiscounts;
4264        } else {
4265            $scope.Discounts = 0;
4266        }
4267
4268        if ($scope.CartData.Coupons) {
4269            $scope.StatusInfo.IsCouponEntered = $scope.StatusInfo.IsCouponEntered || $scope.CartData.Coupons.length > 0;
4270
4271            for (var c = 0; c < $scope.CartData.Coupons.length; c++) {
4272                var coupon = $scope.CartData.Coupons[c];
4273
4274                if (coupon.couponBaseType === 'bogo') {
4275                    continue;
4276                }
4277
4278                if (coupon.itemTypesArray.length === 1 && coupon.itemTypesArray.indexOf('packages') !== -1) {
4279                    continue;
4280                } else if (coupon.itemTypesArray.indexOf('packages') !== -1) {
4281                    coupon.itemTypesArray.splice(coupon.itemTypesArray.indexOf('packages'), 1);
4282                }
4283
4284                // this coupon only applies to some items
4285                if (coupon.pslrIdArray && coupon.pslrIdArray.length) {
4286                    if ($scope.isDollarOffFiltersAllowed()) {
4287                        discountableItem = $scope.getDiscountableTotalForPslrLimitedCoupon(coupon);
4288
4289                        discountable = discountableItem.discountable;
4290                        discountableQty = discountableItem.discountableQty;
4291                    } else {
4292                        discountable = $scope.getDiscountableTotalForPslrLimitedCoupon(coupon);
4293                    }
4294
4295                    if (!discountable) {
4296                        discountable = discountableTotalAllItems;
4297                        if (!coupon.disabledForSalePriceItem) {
4298                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4299                            coupon.notEligible = true;
4300                        } else {
4301                            coupon.notApplied = true;
4302                        }
4303                        continue;
4304                    }
4305                } else if (coupon.classIdArray && coupon.classIdArray.length) {
4306                    if ($scope.isDollarOffFiltersAllowed()) {
4307                        discountableItem = $scope.getDiscountableTotalForClassLimitedCoupon(coupon);
4308
4309                        discountable = discountableItem.discountable;
4310                        discountableQty = discountableItem.discountableQty;
4311                    } else {
4312                        discountable = $scope.getDiscountableTotalForClassLimitedCoupon(coupon);
4313                    }
4314
4315                    if (!discountable) {
4316                        discountable = discountableTotalAllItems;
4317                        if (!coupon.disabledForSalePriceItem) {
4318                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4319                            coupon.notEligible = true;
4320                        } else {
4321                            coupon.notApplied = true;
4322                        }
4323                        continue;
4324                    }
4325                } else if (coupon.departmentIdArray && coupon.departmentIdArray.length) {
4326                    if ($scope.isDollarOffFiltersAllowed()) {
4327                        discountableItem = $scope.getDiscountableTotalForDeptLimitedCoupon(coupon);
4328                        discountable = discountableItem.discountable;
4329                        discountableQty = discountableItem.discountableQty;
4330                    } else {
4331                        discountable = $scope.getDiscountableTotalForDeptLimitedCoupon(coupon);
4332                    }
4333
4334                    if (!discountable) {
4335                        discountable = discountableTotalAllItems;
4336                        if (!coupon.disabledForSalePriceItem) {
4337                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4338                            coupon.notEligible = true;
4339                        } else {
4340                            coupon.notApplied = true;
4341                        }
4342                        continue;
4343                    }
4344                } else if (coupon.categoryIdArray && coupon.categoryIdArray.length) {
4345                    if ($scope.isDollarOffFiltersAllowed()) {
4346                        discountableItem = $scope.getDiscountableTotalForCategoryLimitedCoupon(coupon);
4347                        discountable = discountableItem.discountable;
4348                        discountableQty = discountableItem.discountableQty;
4349                    } else {
4350                        discountable = $scope.getDiscountableTotalForCategoryLimitedCoupon(coupon);
4351                    }
4352
4353                    if (!discountable) {
4354                        discountable = discountableTotalAllItems;
4355                        if (!coupon.disabledForSalePriceItem) {
4356                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4357                            coupon.notEligible = true;
4358                        } else {
4359                            coupon.notApplied = true;
4360                        }
4361                        continue;
4362                    }
4363                } else if (
4364                    $scope.knobby['module_better_coupons'] &&
4365                    coupon.itemTypesArray && coupon.itemTypesArray.length
4366                ) {
4367                    if ($scope.isDollarOffFiltersAllowed()) {
4368                        discountableItem = $scope.getDiscountableTotalForItemTypesLimitedCoupon(coupon);
4369                        discountable = discountableItem.discountable;
4370                        discountableQty = discountableItem.discountableQty;
4371                    } else {
4372                        discountable = $scope.getDiscountableTotalForItemTypesLimitedCoupon(coupon);
4373                    }
4374
4375                    if (!discountable) {
4376                        discountable = discountableTotalAllItems;
4377                        if (!coupon.disabledForSalePriceItem) {
4378                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4379                            coupon.notEligible = true;
4380                        } else {
4381                            coupon.notApplied = true;
4382                        }
4383                        continue;
4384                    }
4385                } else if (coupon.vendorIdArray && coupon.vendorIdArray.length) {
4386                    if ($scope.isDollarOffFiltersAllowed()) {
4387                        discountableItem = $scope.getDiscountableTotalForVendorLimitedCoupon(coupon);
4388                        discountable = discountableItem.discountable;
4389                        discountableQty = discountableItem.discountableQty;
4390                    } else {
4391                        discountable = $scope.getDiscountableTotalForVendorLimitedCoupon(coupon);
4392                    }
4393
4394                    if (!discountable) {
4395                        discountable = discountableTotalAllItems;
4396                        if (!coupon.disabledForSalePriceItem) {
4397                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4398                            coupon.notEligible = true;
4399                        } else {
4400                            coupon.notApplied = true;
4401                        }
4402                        continue;
4403                    }
4404                } else if (coupon.manufacturerNameArray && coupon.manufacturerNameArray.length) {
4405                    if ($scope.isDollarOffFiltersAllowed()) {
4406                        discountableItem = $scope.getDiscountableTotalForMnfLimitedCoupon(coupon);
4407                        discountable = discountableItem.discountable;
4408                        discountableQty = discountableItem.discountableQty;
4409                    } else {
4410                        discountable = $scope.getDiscountableTotalForMnfLimitedCoupon(coupon);
4411                    }
4412
4413                    if (!discountable) {
4414                        discountable = discountableTotalAllItems;
4415                        if (!coupon.disabledForSalePriceItem) {
4416                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4417                            coupon.notEligible = true;
4418                        } else {
4419                            coupon.notApplied = true;
4420                        }
4421                        continue;
4422                    }
4423                }
4424
4425                coupon.totalDiscountableItemsValue = discountable;
4426            }
4427        }
4428
4429        for (var c = 0; c < $scope.CartData.Coupons.length; c++) {
4430            var coupon = $scope.CartData.Coupons[c];
4431
4432            var disc = 0.00;
4433            var maxDiscount = null;
4434            var minDollarAmount = null;
4435
4436            if ($scope.knobby['module_fix_web_coupon_customer_group_restriction'] && !$scope.couponWithCustomerGroupsIsAvailable(coupon)) {
4437                coupon.discount = 0;
4438                coupon.notEligible = true;
4439                continue;
4440            }
4441
4442            if (
4443                (
4444                    $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving' ||
4445                    ($scope.knobby['module_better_coupons'] && $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyAll' && coupon.couponBaseType === CouponConstants.coupon_bogo)
4446                ) &&
4447                coupon.notApplied
4448            ) {
4449                coupon.discount = 0;
4450                continue;
4451            }
4452
4453            if (coupon.maxDiscount > 0) {
4454                maxDiscount = $scope.RoundTo2Dec(coupon.maxDiscount);
4455            }
4456
4457            if (coupon.minDollarAmount > 0) {
4458                minDollarAmount = $scope.RoundTo2Dec(coupon.minDollarAmount);
4459                if (minDollarAmount > itemTotal) {
4460                    $scope.removeCoupon(coupon.couponId, "Min. Purchase of " + $scope.InitInfo.CurrencySymbol2 + minDollarAmount + ' Required. Excludes Gift Card Purchases.');
4461                    continue;
4462                }
4463            }
4464
4465            if (
4466                (coupon.couponBaseType !== 'bogo' && !coupon.totalDiscountableItemsValue) ||
4467                (coupon.couponBaseType === 'bogo' && !coupon.dollarOffAmount)
4468            ) {
4469                if (!coupon.disabledForSalePriceItem) {
4470                    $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4471                    coupon.notEligible = true;
4472                } else {
4473                    coupon.notApplied = true;
4474                }
4475
4476                continue;
4477            }
4478
4479            if (coupon.couponBaseType === 'bogo') {
4480                for (var uuid in coupon.limitedBogoLines) {
4481                    if (coupon.limitedBogoLines.hasOwnProperty(uuid)) {
4482                        var limitedBogoLine = coupon.limitedBogoLines[uuid];
4483
4484                        var lineItem = $scope.CartData.Items.find(function (item) {
4485                            return item.uuid === uuid;
4486                        }) || null;
4487
4488                        if (lineItem) {
4489                            var baseAmountForDiscount = lineItem.discounted_total / lineItem.qty;
4490
4491                            if (coupon.couponType === CouponConstants.coupon_percent) {
4492                                disc = baseAmountForDiscount * limitedBogoLine.discountableItemsAmount * (limitedBogoLine.bogoLinePercentOff / 100);
4493                            } else if (coupon.couponType === CouponConstants.coupon_dollar_off) {
4494                                if (baseAmountForDiscount > limitedBogoLine.bogoLineAmountOff) {
4495                                    disc = limitedBogoLine.bogoLineAmountOff * limitedBogoLine.discountableItemsAmount;
4496                                } else {
4497                                    disc = baseAmountForDiscount * limitedBogoLine.discountableItemsAmount;
4498                                }
4499                            }
4500
4501                            if (maxDiscount !== null && maxDiscount < disc) {
4502                                disc = maxDiscount;
4503                            }
4504
4505                            disc = Math.round(disc * 100) / 100;
4506                            if ((coupon.discount + disc) > maxDiscount && maxDiscount > 0) {
4507                                disc = maxDiscount - coupon.discount;
4508                            }
4509
4510                            lineItem.discount += disc;
4511                            lineItem.discounted_total = $scope.roundNumber($scope.getItemTotal(lineItem) - lineItem.discount, 3);
4512                            coupon.discount += disc;
4513                            $scope.Discounts += disc;
4514                        }
4515                    }
4516                }
4517            } else {
4518                var percentOffDiscountableTotalBeforeApply = 0;
4519                if (
4520                    coupon.couponType === 'percent_off' &&
4521                    !!window.knobby && window.knobby['module_fix_checkout_apply_all_coupon_stacking'] &&
4522                    $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyAll'
4523                ) {
4524                    coupon.appliedLineUuid.forEach(function (appliedLineUuid) {
4525                        var appliedLineItem = $scope.CartData.Items.find(function (item) {
4526                            return item.uuid === appliedLineUuid;
4527                        });
4528
4529                        if (appliedLineItem) {
4530                            percentOffDiscountableTotalBeforeApply += appliedLineItem.discounted_total;
4531                        }
4532                    });
4533                }
4534
4535                coupon.appliedLineUuid.forEach(function (lineUuid, index, array) {
4536                    var lineItem = $scope.CartData.Items.find(function (item) {
4537                        return item.uuid === lineUuid;
4538                    });
4539
4540                    if (!lineItem) {
4541                        return;
4542                    }
4543
4544                    if (coupon.couponType === 'dollar_off') {
4545                        if (discountable >= coupon.minDollarAmount) {
4546                            if (index === array.length - 1) {
4547                                disc = coupon.dollarOffAmount - coupon.discount;
4548                                if (disc < 0) {
4549                                    disc = 0;
4550                                }
4551                            } else {
4552                                var itemPercent = lineItem.total / coupon.totalDiscountableItemsValue;
4553                                disc = (coupon.dollarOffAmount * itemPercent);
4554                            }
4555
4556                            if ((lineItem.discount + disc) > $scope.getItemTotal(lineItem)) {
4557                                disc = ($scope.getItemTotal(lineItem) + disc) - (lineItem.discount + disc);
4558                            }
4559                        }
4560                    } else if (coupon.couponType === 'percent_off') {
4561                        if (index === array.length - 1) {
4562                            var totalPercentOffDiscount;
4563                            if (
4564                                !!window.knobby && window.knobby['module_fix_checkout_apply_all_c
4564oupon_stacking'] &&
4565                                $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyAll'
4566                            ) {
4567                                totalPercentOffDiscount = Math.round(percentOffDiscountableTotalBeforeApply * (coupon.percentOff / 100) * 100) / 100;
4568                            } else {
4569                                totalPercentOffDiscount = Math.round(coupon.totalDiscountableItemsValue * (coupon.percentOff / 100) * 100) / 100;
4570                            }
4571                            disc = totalPercentOffDiscount - coupon.discount;
4572                            if (disc < 0) { disc = 0; }
4573                        } else {
4574                            disc = lineItem.discounted_total * (coupon.percentOff / 100);
4575                        }
4576                    }
4577
4578                    if (maxDiscount !== null && maxDiscount < disc) {
4579                        disc = maxDiscount;
4580                    }
4581
4582                    disc = Math.round(disc * 100) / 100;
4583
4584                    if ((coupon.discount + disc) > maxDiscount && maxDiscount > 0) {
4585                        disc = maxDiscount - coupon.discount;
4586                    }
4587
4588                    if (disc < 0) {
4589                        disc = 0;
4590                    }
4591
4592                    lineItem.discount += disc;
4593                    lineItem.discounted_total = $scope.roundNumber($scope.getItemTotal(lineItem) - lineItem.discount, 3);
4594                    coupon.discount += disc;
4595                    $scope.Discounts += disc;
4596                });
4597            }
4598        }
4599
4600        if (!allowStacking && $scope.CustomerDiscounts > 0 && $scope.Discounts > 0) {
4601            if ($scope.CustomerDiscounts > $scope.Discounts) {
4602                $scope.CartData.Items.forEach(function (item) {
4603                    if (isDiscountable(item)) {
4604                        item.discount = 0;
4605                        item.discounted_total = $scope.getItemTotal(item);
4606                    }
4607                });
4608                if ($scope.CartData.Coupons) {
4609                    for (var k = 0; k < $scope.CartData.Coupons.length; k++) {
4610                        $scope.CartData.Coupons[k].discount = 0;
4611                        $scope.CartData.Coupons[k].notApplied = true;
4612                    }
4613                }
4614                $scope.Discounts = $scope.CustomerDiscounts;
4615                var customerDiscountPercent = $scope.InitInfo.customer.discount;
4616                $scope.CartData.Items.forEach(function (item) {
4617                    if ($scope.InitInfo.customer?.discount > 0 && isDiscountable(item)) {
4618                        var extPrice = item.price * item.qty;
4619                        var itemDiscount = (extPrice * customerDiscountPercent) / 100;
4620                        if (itemDiscount > extPrice) {
4621                            itemDiscount = extPrice;
4622                        }
4623                        item.discount = itemDiscount;
4624                        item.discounted_total = extPrice - itemDiscount;
4625                    }
4626                });
4627            }
4628        }
4629
4630        if ($scope.Discounts > $scope.SubTotal) {
4631            $scope.Discounts = $scope.SubTotal;
4632        }
4633    };
4634
4635    // We need to divide the discount between all discountable entities
4636    // So we can then calculate the taxes
4637    // keep in sync with Cart.model CalculateItemDiscounts
4638    $scope.CalculateItemDiscounts = function () {
4639        var allowStacking = $scope.InitInfo && $scope.InitInfo.CheckoutSettings && $scope.InitInfo.CheckoutSettings.AllowDiscountStacking;
4640
4641        if (!$scope.CartData.Items || $scope.CartData.Items.length == 0) {
4642            // no items - clear the coupons
4643            $scope.CartData.Coupons = [];
4644            return 0.00;
4645        }
4646
4647        // Calculate the discountable total
4648        var discountable = 0;
4649        var itemTotal = 0;
4650        var discountableQty = 0;
4651        var discountableItem = {};
4652
4653        for (var i = 0; i < $scope.CartData.Items.length; i++) {
4654            var item = $scope.CartData.Items[i];
4655            if ($scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems === true && item.on_sale) {
4656                continue;
4657            }
4658            if (item.type != 'gc') {
4659                if ($scope.knobby['module_fix_coupon_min_purchase_class_addon'] && item.type === 'class' && item.package_items && item.package_items.length) {
4660                    itemTotal += $scope.RoundTo2Dec($scope.getItemTotalForWeb(item));
4661                } else {
4662                    itemTotal += item.total;
4663                }
4664            }
4665            if (isDiscountable(item)) {
4666                discountable += item.total;
4667                if (item.type === 'class' && item.package_items && Array.isArray(item.package_items) && item.package_items.length > 0) {
4668                    for (var j = 0; j < item.package_items.length; j++) {
4669                        if ($scope.knobby['module_fix_coupon_min_purchase_class_addon']) {
4670                            discountable += item.package_items[j]['price'] * item.qty;
4671                        } else {
4672                            discountable += item.package_items[j]['price'];
4673                        }
4674                    }
4675                }
4676                item.discount_applied = true;
4677            }
4678        }
4679        var discountableTotalAllItems = discountable;
4680
4681        // Calculate the discounts off the total
4682        var discounts = 0;
4683        if (allowStacking || !$scope.CartData.Coupons || $scope.CartData.Coupons.length === 0) {
4684            if ($scope.CustomerDiscounts > 0) {
4685                discounts = $scope.CustomerDiscounts;
4686            }
4687        }
4688
4689        if ($scope.CartData.Coupons) {
4690            $scope.StatusInfo.IsCouponEntered = $scope.StatusInfo.IsCouponEntered || $scope.CartData.Coupons.length > 0;
4691
4692            for (var c = 0; c < $scope.CartData.Coupons.length; c++) {
4693                var coupon = $scope.CartData.Coupons[c];
4694
4695                if ($scope.knobby['module_better_coupons']) {
4696                    if (coupon.itemTypesArray.length === 1 && coupon.itemTypesArray.indexOf('packages') !== -1) {
4697                        continue;
4698                    } else if (coupon.itemTypesArray.indexOf('packages') !== -1) {
4699                        coupon.itemTypesArray.splice(coupon.itemTypesArray.indexOf('packages'), 1);
4700                    }
4701                }
4702
4703                // reset - a pslr specific coupon may have changed discountable
4704                discountable = discountableTotalAllItems;
4705                coupon.discount = 0;
4706                var maxDiscount = null;
4707                var minDollarAmount = null;
4708                var discount = 0;
4709                var newDiscount = 0;
4710
4711                // this coupon only applies to some items
4712                if (coupon.pslrIdArray && coupon.pslrIdArray.length) {
4713                    if ($scope.isDollarOffFiltersAllowed()) {
4714                        discountableItem = $scope.getDiscountableTotalForPslrLimitedCoupon(coupon);
4715                        discountable = discountableItem.discountable;
4716                        discountableQty = discountableItem.discountableQty;
4717                    } else {
4718                        discountable = $scope.getDiscountableTotalForPslrLimitedCoupon(coupon);
4719                    }
4720
4721                    if (!discountable) {
4722                        discountable = discountableTotalAllItems;
4723                        if (!coupon.disabledForSalePriceItem) {
4724                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4725                            coupon.notEligible = true;
4726                        } else {
4727                            coupon.notApplied = true;
4728                        }
4729                        continue;
4730                    }
4731                } else if (coupon.classIdArray && coupon.classIdArray.length) {
4732                    if ($scope.isDollarOffFiltersAllowed()) {
4733                        discountableItem = $scope.getDiscountableTotalForClassLimitedCoupon(coupon);
4734                        discountable = discountableItem.discountable;
4735                        discountableQty = discountableItem.discountableQty;
4736                    } else {
4737                        discountable = $scope.getDiscountableTotalForClassLimitedCoupon(coupon);
4738                    }
4739
4740                    if (!discountable) {
4741                        discountable = discountableTotalAllItems;
4742                        if (!coupon.disabledForSalePriceItem) {
4743                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4744                            coupon.notEligible = true;
4745                        } else {
4746                            coupon.notApplied = true;
4747                        }
4748                        continue;
4749                    }
4750                } else if (coupon.departmentIdArray && coupon.departmentIdArray.length) {
4751                    if ($scope.isDollarOffFiltersAllowed()) {
4752                        discountableItem = $scope.getDiscountableTotalForDeptLimitedCoupon(coupon);
4753                        discountable = discountableItem.discountable;
4754                        discountableQty = discountableItem.discountableQty;
4755                    } else {
4756                        discountable = $scope.getDiscountableTotalForDeptLimitedCoupon(coupon);
4757                    }
4758
4759                    if (!discountable) {
4760                        discountable = discountableTotalAllItems;
4761                        if (!coupon.disabledForSalePriceItem) {
4762                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4763                            coupon.notEligible = true;
4764                        } else {
4765                            coupon.notApplied = true;
4766                        }
4767                        continue;
4768                    }
4769                } else if (coupon.categoryIdArray && coupon.categoryIdArray.length) {
4770                    if ($scope.isDollarOffFiltersAllowed()) {
4771                        discountableItem = $scope.getDiscountableTotalForCategoryLimitedCoupon(coupon);
4772                        discountable = discountableItem.discountable;
4773                        discountableQty = discountableItem.discountableQty;
4774                    } else {
4775                        discountable = $scope.getDiscountableTotalForCategoryLimitedCoupon(coupon);
4776                    }
4777
4778
4779                    if (!discountable) {
4780                        discountable = discountableTotalAllItems;
4781                        if (!coupon.disabledForSalePriceItem) {
4782                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4783                            coupon.notEligible = true;
4784                        } else {
4785                            coupon.notApplied = true;
4786                        }
4787                        continue;
4788                    }
4789                } else if (
4790                    $scope.knobby['module_better_coupons'] &&
4791                    coupon.itemTypesArray && coupon.itemTypesArray.length
4792                ) {
4793                    if ($scope.isDollarOffFiltersAllowed()) {
4794                        discountableItem = $scope.getDiscountableTotalForItemTypesLimitedCoupon(coupon);
4795                        discountable = discountableItem.discountable;
4796                        discountableQty = discountableItem.discountableQty;
4797                    } else {
4798                        discountable = $scope.getDiscountableTotalForItemTypesLimitedCoupon(coupon);
4799                    }
4800
4801                    if (!discountable) {
4802                        discountable = discountableTotalAllItems;
4803                        if (!coupon.disabledForSalePriceItem) {
4804                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4805                            coupon.notEligible = true;
4806                        } else {
4807                            coupon.notApplied = true;
4808                        }
4809                        continue;
4810                    }
4811                } else if (coupon.vendorIdArray && coupon.vendorIdArray.length) {
4812                    if ($scope.isDollarOffFiltersAllowed()) {
4813                        discountableItem = $scope.getDiscountableTotalForVendorLimitedCoupon(coupon);
4814                        discountable = discountableItem.discountable;
4815                        discountableQty = discountableItem.discountableQty;
4816                    } else {
4817                        discountable = $scope.getDiscountableTotalForVendorLimitedCoupon(coupon);
4818                    }
4819
4820                    if (!discountable) {
4821                        discountable = discountableTotalAllItems;
4822                        if (!coupon.disabledForSalePriceItem) {
4823                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4824                            coupon.notEligible = true;
4825                        } else {
4826                            coupon.notApplied = true;
4827                        }
4828                        continue;
4829                    }
4830                } else if (coupon.manufacturerNameArray && coupon.manufacturerNameArray.length) {
4831                    if ($scope.isDollarOffFiltersAllowed()) {
4832                        discountableItem = $scope.getDiscountableTotalForMnfLimitedCoupon(coupon);
4833                        discountable = discountableItem.discountable;
4834                        discountableQty = discountableItem.discountableQty;
4835                    } else {
4836                        discountable = $scope.getDiscountableTotalForMnfLimitedCoupon(coupon);
4837                    }
4838
4839                    if (!discountable) {
4840                        discountable = discountableTotalAllItems;
4841                        if (!coupon.disabledForSalePriceItem) {
4842                            $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4843                            coupon.notEligible = true;
4844                        } else {
4845                            coupon.notApplied = true;
4846                        }
4847                        continue;
4848                    }
4849                }
4850
4851                if (coupon.maxDiscount > 0) {
4852                    maxDiscount = $scope.RoundTo2Dec(coupon.maxDiscount);
4853                }
4854
4855                if (coupon.minDollarAmount > 0) {
4856                    minDollarAmount = $scope.RoundTo2Dec(coupon.minDollarAmount);
4857                    if (minDollarAmount > itemTotal) {
4858                        // this coupon can't be used - the total is too low.
4859                        $scope.removeCoupon(coupon.couponId, "Min. Purchase of " + $scope.InitInfo.CurrencySymbol2 + minDollarAmount + ' Required. Excludes Gift Card Purchases.');
4860                        continue;
4861                    }
4862                }
4863
4864                if (discountable > 0) {
4865                    discountable -= (allowStacking || !$scope.CartData.Coupons || $scope.CartData.Coupons.length === 0) ? (Number($scope.CustomerDiscounts) || 0) : 0;
4866
4867                    if (coupon.couponType === 'percent_off') {
4868                        if ($scope.knobby['module_better_coupons'] && coupon.couponBaseType === CouponConstants.coupon_bogo) {
4869                            discount = coupon.dollarOffAmount;
4870                        } else {
4871                            discount = $scope.RoundTo2Dec(discountable * coupon.percentOff / 100);
4872                        }
4873
4874                        if (maxDiscount !== null && maxDiscount < discount) {
4875                            discount = maxDiscount;
4876                        }
4877
4878                        if (
4879                            (
4880                                $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving' ||
4881                                ($scope.knobby['module_better_coupons'] && $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyAll' && coupon.couponBaseType === CouponConstants.coupon_bogo)
4882                            ) &&
4883                            coupon.notApplied
4884                        ) {
4885                            coupon.discount = 0;
4886                        } else {
4887                            coupon.discount = discount;
4888                            discounts += discount;
4889                        }
4890                    } else if (coupon.couponType === 'dollar_off') {
4891                        if (discountable >= coupon.minDollarAmount) {
4892                            newDiscount = coupon.dollarOffAmount;
4893
4894                            if ($scope.knobby['module_better_coupons']) {
4895                                if ($scope.isDollarOffFiltersAllowed() && discountableQty > 0 && coupon.couponBaseType !== CouponConstants.coupon_bogo) {
4896                                    newDiscount *= discountableQty;
4897                                }
4898                            } else {
4899                                if ($scope.isDollarOffFiltersAllowed() && discountableQty > 0) {
4900                                    newDiscount *= discountableQty;
4901                                }
4902                            }
4903
4904                            discount = $scope.RoundTo2Dec(newDiscount);
4905
4906                            if (maxDiscount !== null && maxDiscount < discount) {
4907                                discount = maxDiscount;
4908                            }
4909
4910                            if (discount > $scope.SubTotal) {
4911                                discount = $scope.SubTotal;
4912                            }
4913
4914                            if (
4915                                (
4916                                    $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving' ||
4917                                    ($scope.knobby['module_better_coupons'] && $scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyAll' && coupon.couponBaseType === CouponConstants.coupon_bogo)
4918                                ) &&
4919                                coupon.notApplied
4920                            ) {
4921                                 coupon.discount = 0;
4922                            } else {
4923                                coupon.discount = discount;
4924                                discounts += discount;
4925                            }
4926                        }
4927                    }
4928                } else {
4929                    if (!coupon.disabledForSalePriceItem) {
4930                        $scope.showGiftCardCouponMessage('Coupon Not Applied. No Eligible Items in Cart');
4931                        coupon.notEligible = true;
4932                    } else {
4933                        coupon.notApplied = true;
4934                    }
4935                }
4936            }
4937        }
4938
4939        if (!allowStacking && $scope.CustomerDiscounts > 0 && discounts > 0) {
4940            if ($scope.CustomerDiscounts > discounts) {
4941                if ($scope.CartData.Coupons) {
4942                    for (var k = 0; k < $scope.CartData.Coupons.length; k++) {
4943                        $scope.CartData.Coupons[k].discount = 0;
4944                        $scope.CartData.Coupons[k].notApplied = true;
4945                    }
4946                }
4947                discounts = $scope.CustomerDiscounts;
4948            }
4949        }
4950
4951        $scope.Discounts = $scope.RoundTo2Dec(discounts);
4952
4953        // How much of total discounts is from customer discount (for transactionLineDiscount in line_totals)
4954        var useCustomerDiscount = $scope.CustomerDiscounts > 0 && discounts > 0 &&
4955            (allowStacking || !$scope.CartData.Coupons?.length || discounts === $scope.CustomerDiscounts);
4956        var customerDiscountAmount = useCustomerDiscount ? $scope.CustomerDiscounts : 0;
4957
4958        // Assign each item it's share of the discount
4959        var discount_left = discounts;
4960        for (var i = 0; i < $scope.CartData.Items.length; i++) {
4961            var disc = 0.00;
4962            var item = $scope.CartData.Items[i];
4963
4964            if (isDiscountable(item) && item.discount_applied) {
4965                var itemPercent = item.total / discountableTotalAllItems;
4966                disc = (discounts * itemPercent);
4967                disc = Math.round(disc * 100) / 100;
4968                discount_left -= disc;
4969                disc = Math.round(disc * 100) / 100;
4970                item.discount = disc;
4971                item.discounted_total = $scope.roundNumber($scope.getItemTotal(item) - item.discount, 3);
4972
4973                if (customerDiscountAmount > 0 && discounts > 0) {
4974                    item.customer_discount = $scope.roundNumber(disc * (customerDiscountAmount / discounts), 2);
4975                } else {
4976                    item.customer_discount = 0;
4977                }
4978
4979                //Calculate discount for each package item
4980                if (item.type === 'class' && item.package_items && Array.isArray(item.package_items) && item.package_items.length > 0) {
4981                    for (var j = 0; j < item.package_items.length; j++) {
4982                        var pItem = item.package_items[j];
4983                        var piTotal = pItem.price * item.qty;
4984
4985                        //calculation for total discount;
4986                        var pItemPercent = piTotal / discountableTotalAllItems;
4987                        var piDisc = (discounts * pItemPercent);
4988                        piDisc = Math.round(piDisc * 100) / 100;
4989                        discount_left -= piDisc;
4990                        piDisc = Math.round(piDisc * 100) / 100;
4991
4992                        item.package_items[j]['discount'] = piDisc;
4993                        item.package_items[j]['discounted_total'] = $scope.roundNumber(piTotal - piDisc, 3);
4994                        if (customerDiscountAmount > 0 && discounts > 0) {
4995                            item.package_items[j]['customer_discount'] = $scope.roundNumber(piDisc * (customerDiscountAmount / discounts), 2);
4996                        } else {
4997                            item.package_items[j]['customer_discount'] = 0;
4998                        }
4999                    }
5000                }
5001            } else if (!isDiscountable(item)) {
5002                item.customer_discount = 0;
5003            }
5004        }
5005        discount_left = $scope.RoundTo2Dec(discount_left);
5006        if (discount_left != 0) {
5007            $scope.MagicPennyDiscount(discount_left);
5008        }
5009
5010    };
5011
5012    // Keep in sync with CartModel TotalsCalculate
5013    $scope.TotalsCalculate = function () {
5014        // Total items
5015        var total = 0;
5016        var totalBeingShipped = 0;
5017        var gst_total = 0;
5018        var requiresShipping = false;
5019        var qty = 0;
5020        var weight = 0;
5021        if ($scope.CartData.Items && $scope.CartData.Items.length > 0) {
5022            for (var i = 0; i < $scope.CartData.Items.length; i++) {
5023                var item = $scope.CartData.Items[i];
5024                var item_total = $scope.getItemTotal(item);
5025                var item_gst_total = $scope.getItemGstTotal(item);
5026
5027                if (item.type === 'class') {
5028                    total += $scope.RoundTo2Dec(fractionalMultiplication(item.qty, $scope.getItemPriceForWeb(item)));
5029                } else {
5030                    total += $scope.RoundTo2Dec(item_total);
5031                }
5032
5033                gst_total += $scope.RoundTo2Dec(item_gst_total);
5034
5035                // these types are not shipped, or don't count towards shipping costs
5036
5037                //Rto items should have shipping options
5038                if (item.type != 'class' && item.type != 'gc' && item.type != 'subscription' && !item.downloadable) {
5039                    totalBeingShipped += $scope.RoundTo2Dec(item_total);
5040                    weight += Number(item.weight) * item.qty;
5041                }
5042
5043                qty += Number(item.qty);
5044
5045                // Shipping Required
5046                if (item.type == 'pslr' || item.type == 'product' || item.type == 'rto') {
5047                    if (!item.downloadable) {
5048                        requiresShipping = true;
5049                    }
5050                }
5051
5052                if (
5053                    item.type === 'subscription' &&
5054                    typeof item.subscriptionData !== "undefined" &&
5055                    typeof item.subscriptionData.previous_items_cnt !== "undefined" &&
5056                    item.subscriptionData.previous_items_cnt > 0 &&
5057                    item.subscriptionData.delivery_type === 'ship'
5058                ) {
5059                    requiresShipping = true;
5060                    weight += Number(item.subscriptionData.catch_up_total_weight || 0);
5061                    qty += Number(item.subscriptionData.catch_up_total_qty || 0);
5062                    totalBeingShipped += Number(item.subscriptionData.catch_up_sub_total || 0);
5063                }
5064            }
5065        } else {
5066            requiresShipping = true;
5067        }
5068
5069        if ($scope.checkFlagBugClassesCantCheckOutEcommerce()) {
5070            // If cart contains only classes, gift cards, subscriptions, and/or digital downloads, shipping is not required
5071            if ($scope.isGCClassSubscriptionCombinationOnly()) {
5072                requiresShipping = false;
5073            }
5074        }
5075
5076        $scope.updateDigitalOrder();
5077
5078        $scope.ShippingStatus.Required = requiresShipping;
5079
5080        //If we don't show shipping form anymore, let's clean shipping data. Because they will be used during dynamic taxes calculation
5081        //And dynamic taxes depend on shipping data that can't be filled from this point
5082        if ($scope.HasDynamicTaxes && !$scope.showShippingAddressForm()) {
5083            //Need to clean shipping data as well
5084            $scope.ShippingInfo.Address1 = null;
5085            $scope.ShippingInfo.Address2 = null;
5086            $scope.ShippingInfo.City = null;
5087            $scope.ShippingInfo.State = null;
5088            $scope.ShippingInfo.Zip = null;
5089            if (!$scope.digitalOrder && !$scope.calculateWaTaxesBasedOnStoreLocationAddress()) {
5090                $scope.dynamicRateCache = null;
5091                $scope.dynamicRateCacheKey = null;
5092            }
5093            $scope.SessionStorageSet();
5094        }
5095
5096        $scope.Weight = weight;
5097        $scope.SubTotal = $scope.RoundTo2Dec(total);
5098        $scope.ShippingSubtotal = $scope.RoundTo2Dec(totalBeingShipped);
5099        $scope.GST_SubTotal = $scope.RoundTo2Dec(gst_total);
5100
5101        if ($scope.knobby['module_fix_tiered_coupon_on_website'] && $scope.knobby['module_better_coupons'] && $scope.CartData.Coupons) {
5102            $scope.CartData.Coupons.forEach(function (coupon) {
5103                if (typeof coupon.setAmountsBasedOnTiers !== 'function') {
5104                    return;
5105                }
5106                if (coupon.couponBaseType === CouponConstants.coupon_bogo) {
5107                    coupon.setAmountsBasedOnTiers({lines: $scope.CartData.Items, disableCouponsForSalePriceItems: $scope.InitInfo?.CheckoutSettings?.DisableCouponsForSalePriceItems});
5108                } else {
5109                    coupon.setAmountsBasedOnTiers($scope.SubTotal);
5110                }
5111            });
5112        }
5113
5114        if ($scope.knobby['module_better_coupons']) {
5115            // Shipping
5116            var shipping = 0;
5117            if (requiresShipping && $scope.ShippingInfo.Option) {
5118                var t = Number($scope.ShippingInfo.Option.price) || 0;
5119
5120                shipping += Number(t);
5121                total += shipping;
5122                gst_total += shipping;
5123            }
5124            $scope.ShippingTotal = shipping;
5125        }
5126
5127        if ($scope.CartData.Items) {
5128            var customerDiscountTotal = 0;
5129            for (var i = 0, ilen = $scope.CartData.Items.length; i < ilen; i++) {
5130                var item = $scope.CartData.Items[i];
5131                if ($scope.InitInfo.customer?.discount > 0 && isDiscountable(item)) {
5132                    var customerDiscountPercent = $scope.InitInfo.customer.discount;
5133                    var extPrice = item.price * item.qty;
5134                    var itemDiscount = (extPrice * customerDiscountPercent) / 100
5135                    if (itemDiscount > extPrice) {
5136                        itemDiscount = extPrice;
5137                    }
5138
5139                    var discountedTotal = extPrice - itemDiscount;
5140                    item.discounted_total = discountedTotal;
5141                    item.discount_applied = true;
5142                    item.discount = itemDiscount;
5143                    item.customer_discount = itemDiscount;
5144                    customerDiscountTotal += itemDiscount
5145                } else {
5146                    item.customer_discount = 0;
5147                }
5148            }
5149
5150            $scope.CustomerDiscounts = customerDiscountTotal;
5151        }
5152
5153        if ($scope.knobby['module_better_coupons']) {
5154            $scope.applyHighestSavingCouponsMark();
5155        } else {
5156            if ($scope.InitInfo?.CheckoutSettings?.CouponStacking === 'applyHighestSaving') {
5157                $scope.applyHighestSavingCouponsMark();
5158            }
5159        }
5160
5161        if ($scope.knobby['module_better_coupons']) {
5162            $scope.CalculateItemBetterDiscounts();
5163        } else {
5164            $scope.CalculateItemDiscounts();
5165        }
5166
5167        // Taxes (must be after discounts)
5168        $scope.CalculateItemTaxes();
5169
5170        // Discounts
5171        total -= $scope.Discounts;
5172        gst_total -= $scope.Discounts;
5173
5174        if (!$scope.showAsGST() || $scope.isShippingCountryGST()) {
5175            total += $scope.Taxes;
5176        }
5177
5178        if (!$scope.knobby['module_better_coupons']) {
5179            // Shipping
5180            var shipping = 0;
5181            if (requiresShipping && $scope.ShippingInfo.Option) {
5182                var t = Number($scope.ShippingInfo.Option.price) || 0;
5183
5184                shipping += Number(t);
5185                total += shipping;
5186                gst_total += shipping;
5187            }
5188            $scope.ShippingTotal = shipping;
5189        }
5190
5191        $scope.ShippingDiscount = $scope.getShippingDiscount();
5192
5193        $scope.checkShippingCoupon();
5194
5195        // Shipping Taxes
5196        if ($scope.gstToNon()) {
5197            $scope.ShippingTaxes = 0;
5198        } else {
5199            $scope.ShippingTaxes = $scope.calculateShippingTax(shipping);
5200        }
5201
5202        total += $scope.ShippingTaxes;
5203        gst_total += $scope.ShippingTaxes;
5204
5205        total -= $scope.ShippingDiscount;
5206        gst_total -= $scope.ShippingDiscount;
5207
5208        $scope.CartQty = qty;
5209
5210        if ($scope.checkFlagRetailDeliveryFee()) {
5211            //Retail delivery Fee
5212            if ($scope.ShippingInfo.State === 'CO' && $scope.hasNonSubscriptionItemsInCart() && $scope.tax_service !== 'avalara') {
5213                if ($scope.isRetailDeliveryFeeApplied()) {
5214                    $scope.retailDeliveryFee = $scope.RETAIL_DELIVERY_FEE;
5215                } else {
5216                    $scope.retailDeliveryFee = 0.00;
5217                }
5218                total += $scope.retailDeliveryFee;
5219            } else {
5220                $scope.retailDeliveryFee = 0.00;
5221            }
5222        }
5223
5224        // Gift Cards
5225        $scope.GCTotal = 0;
5226        if ($scope.CartData.GiftCards && !$scope.isPayAtPickup()) {
5227            for (var g = 0; g < $scope.CartData.GiftCards.length; g++) {
5228                var giftCard = $scope.CartData.GiftCards[g];
5229                if (giftCard.maxGiftCardAmount > total) {
5230                    giftCard.Amount = total;
5231                    $scope.GCTotal += total;
5232                    total = 0;
5233                    gst_total = 0;
5234                } else {
5235                    var max_amount = Number(giftCard.maxGiftCardAmount);
5236                    giftCard.Amount = max_amount;
5237                    $scope.GCTotal += max_amount;
5238                    total -= max_amount;
5239                    gst_total -= max_amount;
5240                }
5241            }
5242        }
5243
5244        if ($scope.showAsGST()) {
5245            $scope.CartTotal = $scope.RoundTo2Dec(gst_total);
5246        } else {
5247            $scope.CartTotal = $scope.RoundTo2Dec(total);
5248        }
5249        $scope.BillingStatus.Required = (total != 0 || $scope.isCardOnFileNeeded());
5250
5251        let newTotal = $scope.RoundTo2Dec(total);
5252        let totalIsDirty = (newTotal != $scope.Due);
5253        $scope.Due = newTotal;
5254
5255        if (totalIsDirty) {
5256            // The BNPL message element needs to be update if the total changes
5257            $scope.updateBNPLMessage();
5258        }
5259
5260        $scope.GST_Due = $scope.RoundTo2Dec(gst_total);
5261        $scope.updateProcessor();
5262        $scope.checkMinAmountMet();
5263        $scope.makeTaxInclusive();
5264    };
5265
5266    $scope.hasNonSubscriptionItemsInCart = function () {
5267        return $scope.CartData.Items.some(function (item) {
5268            return item.type !== 'subscription';
5269        });
5270    }
5271
5272    $scope.isRetailDeliveryFeeApplied = function () {
5273        return $scope.isStoreHasLocationForRetailFee() && $scope.Taxes > 0 && ((!$scope.ShippingInfo.Option && $scope.isWholesaleEnabled()) || ($scope.ShippingInfo.Option && $scope.ShippingInfo.Option.name !== 'Store Pickup' && $scope.ShippingInfo.Option.name !== 'Curbside Pickup'));
5274    }
5275
5276    $scope.InitToolTip = function (event) {
5277        var id = event.target.id;
5278        $('#' + id).tooltip();
5279    };
5280
5281    $scope.couponWithCustomerGroupsIsAvailable = function(coupon) {
5282        if (coupon.limitedCustomerGroupList.length) {
5283            if ($scope.BillingInfo && $scope.BillingInfo.customerGroups && $scope.BillingInfo.customerGroups.length) {
5284                var intersection = $scope.BillingInfo.customerGroups.filter(function (customerGroup) {
5285                        return coupon.limitedCustomerGroupList.some(function (couponGroup) {
5286                            return parseInt(customerGroup) === parseInt(couponGroup);
5287                        });
5288                    }
5289                );
5290            }
5291
5292            if (!$scope.BillingInfo || !intersection || (intersection && !intersection.length)) {
5293                return false;
5294            }
5295        }
5296
5297        return true;
5298    };
5299
5300    $scope.updateCouponsByCustomerGroups = function () {
5301        if ($scope.knobby['module_better_coupons']) {
5302            var pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
5303
5304            if (!pattern.test($scope.BillingInfo.Email)) {
5305                return;
5306            }
5307
5308            var params = {
5309                customerEmail: $scope.BillingInfo.Email,
5310            };
5311
5312            $http.get($scope.BuildUrl('/checkout/update-coupons-by-customer-groups'), {params: params, withCredentials: true})
5313                .then(function (result) {
5314                    $scope.BillingInfo.customerGroups = result.data.customerGroups || [];
5315                    $scope.SessionStorageSet();
5316
5317                    $scope.CartDataAdapt(result.data);
5318                });
5319        }
5320    };
5321
5322    // endregion
5323
5324    // ******************************************************************************************
5325    // region Change and Validation
5326    // ******************************************************************************************
5327
5328    $scope.FormFieldErrorClass = function (formName, fieldId, form) {
5329        if ($scope.FormFieldError(formName, fieldId, form)) {
5330            return 'has-error';
5331        }
5332        return '';
5333    };
5334
5335    $scope.FormFieldError = function (formName, fieldId, form) {
5336        if (!form) {
5337            form = $scope[formName];
5338        } else {
5339            if (formName === 'shippingForm') {
5340                $scope.setShippingForm(form);
5341            }
5342            if (formName === 'billingForm') {
5343                $scope.setBillingForm(form);
5344            }
5345            if (formName === 'contactForm') {
5346                $scope.setContactForm(form);
5347            }
5348        }
5349
5350        if (form) {
5351            if (form[fieldId] && form[fieldId].$invalid) {
5352                if (form[fieldId].$dirty || form[fieldId].$touched || form.$submitted) {
5353                    return true;
5354                }
5355            }
5356            return false;
5357        }
5358        return true;
5359    };
5360
5361    $scope.setShippingForm = function(shippingForm) {
5362        if (!$scope.shippingForm && shippingForm) {
5363            $scope.shippingForm = shippingForm;
5364        }
5365    };
5366
5367    $scope.setBillingForm = function(billingForm) {
5368        if (!$scope.billingForm && billingForm) {
5369            $scope.billingForm = billingForm;
5370        }
5371    };
5372
5373    $scope.setContactForm = function(contactForm) {
5374        if (!$scope.contactForm && contactForm) {
5375            $scope.contactForm = contactForm;
5376        }
5377    };
5378
5379    // Dead code - remove
5380    $scope.TextBlur = function (id, value) {
5381        if (!id) {
5382            return;
5383        }
5384        $scope.Class[id] = value ? 'text-success' : 'text-danger';
5385    };
5386
5387    $scope.ExpBlur = function (id) {
5388        if (!$scope.CardInfo.ExpMonth || !$scope.CardInfo.ExpYear) {
5389            $scope.Class[id] = 'text-danger';
5390        } else {
5391            $scope.Class[id] = 'text-success';
5392        }
5393        $scope.Validate();
5394    };
5395
5396    $scope.CCBlur = function (id) {
5397        if ($scope.CCValidate() === null) {
5398            $scope.Class[id] = 'text-success';
5399        } else {
5400            $scope.Class[id] = 'text-danger';
5401        }
5402        $scope.Class['visa'] = $scope.CardInfo.CCType == 'VISA' ? 'card-visa' : 'card-default';
5403        $scope.Class['mastercard'] = $scope.CardInfo.CCType == 'MC' ? 'card-mastercard' : 'card-default';
5404        $scope.Class['amex'] = $scope.CardInfo.CCType == 'AMEX' ? 'card-amex' : 'card-default';
5405        $scope.Class['discover'] = $scope.CardInfo.CCType == 'DISC' ? 'card-discover' : 'card-default';
5406
5407        $scope.Validate();
5408    };
5409
5410    $scope.AddressBlur = function () {
5411        // If we have a dynamic tax, it may need to be updated too
5412        return $scope.updateDynamicTaxesOnce();
5413    };
5414
5415    $scope.EmailValidate = function (id, email) {
5416        var reg = /^([A-Za-z0-9_\-\.\+])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
5417        if (reg.test(email) == false) {
5418            $scope.Class[id] = 'text-success';
5419        } else {
5420            $scope.Class[id] = 'text-danger';
5421        }
5422        $scope.Validate();
5423    };
5424
5425    $scope.Validate = function () {
5426        $scope.TotalsCalculate();
5427        $scope.CartVerify();
5428        $scope.ContactVerify();
5429        $scope.ShippingVerify();
5430        $scope.BillingVerify();
5431        $scope.TotalsCalculate();
5432    };
5433
5434    // endregion
5435
5436    // ******************************************************************************************
5437    // region Cart
5438    // ******************************************************************************************
5439
5440    $scope.getCCProcessor = function() {
5441        if ($scope.InitInfo.Processors && $scope.InitInfo.Processors.cc) {
5442            return $scope.InitInfo.Processors.cc;
5443        }
5444        return null;
5445    };
5446
5447    $scope.isProcessingEnabled = function() {
5448        return $scope.hasCardProcessor() || $scope.isPayPalEnabled();
5449    };
5450
5451    $scope.isCardProcessorVantiv = function() {
5452        return ($scope.InitInfo.Processors && $scope.InitInfo.Processors.cc === $scope.PROCESSOR_VANTIV);
5453    };
5454
5455    $scope.isCardProcessorNMI = function() {
5456        return ($scope.InitInfo.Processors && $scope.InitInfo.Processors.cc === $scope.PROCESSOR_NMI);
5457    };
5458
5459    $scope.isCardProcessorRain = function() {
5460        return ($scope.InitInfo.Processors && $scope.InitInfo.Processors.cc === $scope.PROCESSOR_RAIN);
5461    };
5462
5463    $scope.isCardProcessorQuiltAdyen = function() {
5464        return ($scope.InitInfo.Processors && $scope.InitInfo.Processors.cc === $scope.PROCESSOR_QUILT_ADYEN);
5465    };
5466
5467    $scope.isResponsive = function() {
5468        if (window.isResponsive === false) {
5469            return false;
5470        }
5471        return true;
5472    }
5473
5474    $scope.hasCardProcessor = function() {
5475        return ($scope.isCardProcessorVantiv() || $scope.isCardProcessorNMI() || $scope.isCardProcessorRain() || $scope.isCardProcessorQuiltAdyen());
5476    };
5477
5478    $scope.isPayPalEnabled = function() {
5479        return ($scope.InitInfo.Processors && $scope.InitInfo.Processors.paypal === 'yes');
5480    };
5481
5482    $scope.isVantivHosted = function() {
5483        return $scope.StatusInfo.Processor === $scope.PROCESSOR_VANTIV;
5484    };
5485
5486    $scope.isRainProcessor = function() {
5487        return $scope.StatusInfo.Processor === $scope.PROCESSOR_RAIN;
5488    };
5489
5490    $scope.isQuiltAdyen = function() {
5491        return $scope.StatusInfo.Processor === $scope.PROCESSOR_QUILT_ADYEN;
5492    };
5493
5494    $scope.isPayPal = function() {
5495        return $scope.StatusInfo.Processor === $scope.PROCESSOR_PAYPAL;
5496    };
5497
5498    $scope.isExtendPayPalShippingInfo = function () {
5499        return $scope.isShippingRequired();
5500    };
5501
5502    $scope.isFree = function() {
5503        return $scope.StatusInfo.Processor === $scope.PROCESSOR_FREE;
5504    };
5505
5506    $scope.isCaptchaEnabled = function() {
5507        return ($scope.InitInfo && $scope.InitInfo.captcha);
5508    };
5509
5510    $scope.isFullyPaidByGiftCard = function() {
5511        return $scope.Due === 0 && $scope.GCTotal > 0;
5512    };
5513
5514    $scope.checkMinAmountMet = function() {
5515        if ($scope.StatusInfo.minAmountTriggered) {
5516            $scope.isMinimumAmountMet();
5517        }
5518    };
5519
5520    $scope.isMinimumAmountMet = function() {
5521        if ($scope.isCardProcessorRain() && $scope.isRainProcessor() && !$scope.isCardOnFileNeeded()) {
5522            if ($scope.getAmountDue() < $scope.STRIPE_MIN) {
5523                $scope.StatusInfo.ErrorMessageBottom = "The total purchase amount must be at least $.50 USD, please add more items to continue and checkout.";
5524                $scope.StatusInfo.minAmountTriggered = true;
5525                return false;
5526            } else {
5527                $scope.StatusInfo.ErrorMessageBottom = null;
5528                $scope.StatusInfo.minAmountTriggered = false;
5529            }
5530        }
5531        return true;
5532    }
5533
5534    $scope.isPayAtPickupEnabled = function() {
5535        return ($scope.isResponsive()
5536            && $scope.InitInfo
5537            && $scope.InitInfo.CheckoutSettings
5538            && $scope.InitInfo.CheckoutSettings.PayAtPickup
5539        );
5540    };
5541
5542    $scope.skipBilling = function() {
5543        return (($scope.isPayPal() || $scope.isFree())
5544            && ($scope.BillingStatus.Editing || !$scope.isPayAtPickupEnabled() || !$scope.isPickup())
5545        );
5546    }
5547
5548    $scope.isTextMessagingSupported = function() {
5549        return $scope.InitInfo.storeTextMsgSendingEnabled;
5550    };
5551
5552    /** @return {number} */
5553    $scope.ItemsQuantity = function () {
5554        var totalQuantity = 0;
5555        if ($scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
5556            for (var i = 0; i < $scope.CartData.Items.length; i++) {
5557                var item = $scope.CartData.Items[i];
5558                totalQuantity += Number(item.qty);
5559            }
5560        }
5561        return totalQuantity;
5562    };
5563
5564    $scope.CartVerify = function () {
5565        var totalQuantity = $scope.ItemsQuantity();
5566        if (totalQuantity > 0) {
5567            $scope.CartStatus.Valid = true;
5568            $scope.CartStatus.NotValidMessages = null;
5569        } else {
5570            $scope.CartStatus.Valid = false;
5571            $scope.CartStatus.NotValidMessages = "No items are in your cart.";
5572        }
5573    };
5574
5575    /**
5576     * NOTE: This method has become confusing, the cart can still be free and this will return false because shipping is required
5577     * We should refactor this functionality with the skipBilling method at some point.
5578     * @return {boolean}
5579     */
5580    $scope.CartIsFree = function () {
5581        if ($scope.isShippingRequired() && !$scope.ShippingInfo.Option && !$scope.isPickup()) {
5582            return false;
5583        }
5584        var shipping = $scope.ShippingTotal + $scope.ShippingTaxes;
5585
5586        var total = 0;
5587        if ($scope.CartStatus.Valid && $scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
5588            for (var i = 0; i < $scope.CartData.Items.length; i++) {
5589                var item = $scope.CartData.Items[i];
5590                total += $scope.RoundTo2Dec(item.qty * $scope.getItemPrice(item));
5591            }
5592        } else {
5593            return false;
5594        }
5595
5596        if ((total + shipping) === 0) {
5597            return true;
5598        }
5599
5600        if ($scope.knobby['module_better_coupons']) {
5601            if ($scope.CartData.Coupons && $scope.CartData.Coupons.length > 0 && !$scope.Due && !shipping) { //Discounted to 0 means free
5602                return true;
5603            }
5604        } else {
5605            if ($scope.CartData.Coupons && $scope.CartData.Coupons.length > 0 && !$scope.Due) { //Discounted to 0 means free
5606                return true;
5607            }
5608        }
5609
5610        return false;
5611    };
5612
5613    $scope.CartEdit = function () {
5614        $scope.CartStatus.Entered = true;
5615        $scope.CartStatus.Editing = true;
5616    };
5617
5618    $scope.trackEvent = function() {
5619        if ($scope.InitInfo.facebookPixelId || $scope.InitInfo.googleAnalyticsId) {
5620            var fbContentIds = [];
5621            var fbContents = [];
5622            var gaItems = [];
5623            var gaValue = 0;
5624            if ($scope.CartData.Items) {
5625                for (var i = 0; i < $scope.CartData.Items.length; i++) {
5626                    var item = $scope.CartData.Items[i];
5627                    if (item.sku) {
5628                        fbContentIds.push(item.sku);
5629                        fbContents.push({
5630                            'id': item.sku,
5631                            'quantity': item.qty
5632                        });
5633                    }
5634                    gaItems.push({
5635                        id: item.sku,
5636                        name: item.title,
5637                        quantity: item.qty,
5638                        price: $scope.getItemPrice(item),
5639                    });
5640
5641                    if(window.knobby && window.knobby['flag_google_analytics_v4']) {
5642                        gaValue += $scope.getItemPrice(item);
5643                    }
5644                }
5645            }
5646            if ($scope.InitInfo.facebookPixelId) {
5647                fbq('track', 'InitiateCheckout', {
5648                    currency: $scope.InitInfo.Currency,
5649                    value: $scope.SubTotal,
5650                    num_items: $scope.ItemsQuantity(),
5651                    content_type: 'product',
5652                    content_ids: fbContentIds,
5653                    contents: fbContents
5654                });
5655            }
5656            if (window.knobby && window.knobby['flag_google_analytics_v4']) {
5657                if ($scope.InitInfo.googleAnalyticsId) {
5658                    gtag('event', 'begin_checkout', {
5659                        "currency": $scope.InitInfo.Currency,
5660                        "value": gaValue,
5661                        "items": gaItems
5662                    });
5663                }
5664            } else {
5665                if ($scope.InitInfo.googleAnalyticsId) {
5666                    gtag('event', 'begin_checkout', {
5667                        "currency": $scope.InitInfo.Currency,
5668                        "items": gaItems
5669                    });
5670                }
5671            }
5672        }
5673    };
5674
5675    $scope.CartDone = function (processor) {
5676        // set the "same as" checkbox to false, for wholesale it is hidden and we require the billing info only.
5677        if ($scope.isWholesaleEnabled()) {
5678            $scope.BillingInfo.AddressSameAsShipping = false;
5679        }
5680
5681        $scope.CartStatus.Editing = false;
5682        $scope.setProcessor(processor);
5683
5684        $timeout(function() {
5685            $scope.setShippingOption();
5686        }, 100);
5687
5688        $scope.trackEvent();
5689    };
5690
5691    $scope.CartSync = function () {
5692        $http.get('/checkout/sync.php').then(function () {
5693
5694        });
5695    };
5696
5697    // Determines if we redirect to secure site for CC processing
5698    $scope.allowCreditCardCheckout = function () {
5699        if (($scope.getAmountDue() || $scope.requirePayment()) && $scope.hasCardProcessor()) {
5700            return !$scope.isWholesaleNoCCEnabled();
5701        }
5702        return false;
5703    };
5704
5705    $scope.allowPayPalCheckout = function () {
5706        if (($scope.Due || $scope.requirePayment()) && $scope.InitInfo && $scope.isPayPalEnabled()) {
5707            return !$scope.isWholesaleNoCCEnabled();
5708        }
5709        return false;
5710    };
5711
5712    $scope.isCardOnFileNeeded = function() {
5713        return ($scope.isRtoItemExist() || $scope.isSubscriptionExist() || $scope.isEnrollAutoPay() || $scope.isAddCardOnly());
5714    };
5715
5716    $scope.isRtoItemExist = function () {
5717        if ($scope.CartStatus.Valid && $scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
5718            for (var i = 0; i < $scope.CartData.Items.length; i++) {
5719                var item = $scope.CartData.Items[i];
5720                if (item.type === 'rto') {
5721                    return true;
5722                }
5723            }
5724        }
5725
5726        return false;
5727    };
5728
5729    $scope.isSubscriptionExist = function () {
5730        if ($scope.CartStatus.Valid && $scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
5731            return $scope.CartData.Items.some(function (cartItem) {
5732                return cartItem.type === 'subscription'
5733            });
5734        }
5735        return false;
5736    };
5737
5738    $scope.isSubscriptionOnly = function () {
5739        if ($scope.CartStatus.Valid && $scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
5740            return !$scope.CartData.Items.some(function (cartItem) {
5741                return cartItem.type !== 'subscription'
5742            });
5743        }
5744        return true;
5745    };
5746
5747    $scope.isGCClassSubscriptionCombinationOnly = function () {
5748        if ($scope.CartStatus.Valid && $scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
5749            const allowedTypes = ['class', 'gc', 'subscription'];
5750
5751            var hasSubscriptionCatchUpShipping = $scope.CartData.Items.some(function (cartItem) {
5752                return cartItem.type === 'subscription'
5753                    && cartItem.subscriptionData
5754                    && cartItem.subscriptionData.previous_items_cnt > 0
5755                    && cartItem.subscriptionData.delivery_type === 'ship';
5756            });
5757
5758            if (hasSubscriptionCatchUpShipping) {
5759                return false;
5760            }
5761
5762            return $scope.CartData.Items.every(function (cartItem) {
5763                if (!$scope.checkFlagBugDigitalDownloadWithClass()) {
5764                    return allowedTypes.includes(cartItem.type);
5765                } else {
5766                    return allowedTypes.includes(cartItem.type) || cartItem.downloadable;
5767                }
5768            });
5769        }
5770        return true;
5771    };
5772
5773    $scope.checkFlagForSubscriptionOnly = function () {
5774        if ($scope.knobby && $scope.knobby.module_paypal_subscription_only) {
5775            return $scope.isSubscriptionOnly();
5776        }
5777
5778        return false;
5779    }
5780
5781    $scope.checkFlagStripeSendLevel3Data = function () {
5782        return $scope.knobby && $scope.knobby.module_stripe_send_level_3_data;
5783    }
5784
5785    $scope.checkFlagRestoreSchoolDeliveryShippingOption = function () {
5786        return $scope.knobby && $scope.knobby.module_restore_school_delivery_shipping_option;
5787    };
5788
5789    $scope.checkFlagAdyenSendLevel3Data = function () {
5790        return $scope.knobby && $scope.knobby.module_adyen_send_level_3_data;
5791    }
5792
5793    $scope.checkFlagBugDigitalDownloadWithClass = function () {
5794        return $scope.knobby && $scope.knobby.module_bug_digital_download_with_class;
5795    }
5796
5797    $scope.checkFlagAdyenSendLevel3DataAuthCapture = function () {
5798        return $scope.knobby && $scope.knobby.module_adyen_send_level_3_data_auth_capture;
5799    }
5800
5801    $scope.checkFlagDisableExpressCheckout = function () {
5802        return $scope.knobby && $scope.knobby.module_disable_express_checkout;
5803    }
5804
5805    $scope.checkFlagAvsEcommerce = function () {
5806        return $scope.knobby && $scope.knobby.module_avs_ecommerce;
5807    };
5808
5809    $scope.checkFlagFixAdyenBadShipping = function () {
5810        return $scope.knobby && $scope.knobby.module_fix_adyen_bad_shipping;
5811    };
5812
5813    $scope.getEffectiveShippingInfo = function () {
5814        if ($scope.checkFlagFixAdyenBadShipping() && !$scope.isShippingRequired()) {
5815            return {
5816                FirstName: '',
5817                LastName: '',
5818                Address1: '',
5819                Address2: '',
5820                City: '',
5821                State: '',
5822                Zip: '',
5823                Country: '',
5824                CountryKey: null,
5825                company_name: ''
5826            };
5827        }
5828
5829        return $scope.ShippingInfo;
5830    };
5831
5832    $scope.checkFlagCarePlans = function () {
5833        return $scope.knobby && $scope.knobby.module_care_plans;
5834    }
5835
5836    $scope.checkFlagRetailDeliveryFee = function () {
5837        return $scope.knobby && $scope.knobby.module_colorado_retail_delivery_fee;
5838    }
5839
5840    $scope.checkFlagSendMixPanelEventZipCodeChange = function () {
5841        return $scope.knobby && $scope.knobby.module_send_mix_panel_event_on_zipcode_change_web_checkout;
5842    }
5843
5844    $scope.isDollarOffFiltersAllowed = function () {
5845        return $scope.knobby && $scope.knobby.module_item_filters_dollar_off_coupons;
5846    };
5847
5848    $scope.checkFlagAvalaraTaxCodeIssue = function () {
5849        return $scope.knobby && $scope.knobby.module_avalara_tax_code_issue;
5850    }
5851
5852    $scope.checkFlagFixCheckoutJurisdictionTaxPrecision = function () {
5853        return $scope.knobby && $scope.knobby.module_fix_checkout_jurisdiction_tax_precision;
5854    }
5855
5856    $scope.checkFlagCannotViewTermsConditionsCheckoutScreenAddressInfo = function () {
5857        return $scope.knobby && $scope.knobby.module_cannot_view_terms_conditions_checkout_screen_address_info;
5858    }
5859
5860    $scope.canShowShipToSchool = function () {
5861        return $scope.knobby
5862            && $scope.knobby.module_ship_to_school_for_non_rto_or_osr
5863            && window.isMusicStore
5864            && !$scope.isRtoOrOsr()
5865            && $scope.districts
5866            && $scope.districts.length !== 0
5867            && $scope.InitInfo
5868            && $scope.InitInfo.ShipToSchoolsEnabled;
5869    };
5870
5871    $scope.isWholesaleTaxExempt = function() {
5872        if ($scope.InitInfo.WholesaleSettings && $scope.InitInfo.WholesaleSettings.TaxWholesaleOrders) {
5873            return false;
5874        }
5875
5876      return ($scope.isWholesaleEnabled() && !$scope.isTaxInclusive());
5877    };
5878
5879    $scope.isWholesaleEnabled = function () {
5880        return ($scope.InitInfo.wholesale && $scope.InitInfo.wholesale.checkout_enabled);
5881    };
5882
5883    $scope.isWholesaleNoCCEnabled = function () {
5884        return ($scope.InitInfo.wholesale && $scope.InitInfo.wholesale.checkout_enabled && $scope.InitInfo.wholesale.no_cc_enabled);
5885    };
5886
5887    $scope.disableWholesaleShipping = function () {
5888        if ($scope.isWholesaleNoCCEnabled()) {
5889            return true;
5890        }
5891
5892        return ($scope.InitInfo.wholesale && $scope.InitInfo.wholesale.checkout_enabled && !$scope.InitInfo.wholesale.shipping_enabled);
5893    };
5894
5895    $scope.setWholesaleUserInfo = function () {
5896        if ($scope.isWholesaleEnabled() && $scope.InitInfo.wholesale_user) {
5897            var u = $scope.InitInfo.wholesale_user;
5898            var b = $scope.BillingInfo;
5899            var s = $scope.ShippingInfo;
5900            if (u.email && !b.Email) {
5901                b.Email = u.email;
5902            }
5903            if (u.phone && !b.Phone) {
5904                b.Phone = u.phone;
5905            }
5906            if (u.first_name && !s.FirstName) {
5907                s.FirstName = u.first_name;
5908            }
5909            if (u.last_name && !s.LastName) {
5910                s.LastName = u.last_name;
5911            }
5912        }
5913    };
5914
5915    $scope.setCustomerInfo = function () {
5916        if (!$scope.InitInfo.customer) {
5917            return;
5918        }
5919
5920        var customer = $scope.InitInfo.customer;
5921
5922        $scope.BillingInfo.Email = customer.email;
5923        $scope.BillingInfo.Phone = customer.phone_mobile || customer.phone;
5924
5925        $scope.ShippingInfo.FirstName = customer.name_first;
5926        $scope.ShippingInfo.LastName = customer.name_last;
5927
5928        $scope.BillingInfo.FirstName = customer.name_first;
5929        $scope.BillingInfo.LastName = customer.name_last;
5930
5931
5932        $scope.BillingInfo.Address1 = customer.address;
5933        $scope.BillingInfo.Address2 = customer.address2;
5934        $scope.BillingInfo.City = customer.city;
5935        $scope.BillingInfo.State = customer.state;
5936        $scope.BillingInfo.Zip = customer.zip;
5937        $scope.BillingInfo.Country = customer.country;
5938
5939        if (customer.shipping_address) {
5940            $scope.ShippingInfo.Address1 = customer.shipping_address.address;
5941            $scope.ShippingInfo.Address2 = customer.shipping_address.address2;
5942            $scope.ShippingInfo.City = customer.shipping_address.city;
5943            $scope.ShippingInfo.State = customer.shipping_address.state;
5944            $scope.ShippingInfo.Zip = customer.shipping_address.zip;
5945            $scope.ShippingInfo.Country = customer.shipping_address.country;
5946            $scope.BillingInfo.AddressSameAsShipping = customer.shipping_address.is_billing_default;
5947        }
5948
5949    };
5950
5951    $scope.editRental = function (item) {
5952        $window.location.href = '/rentals.htm#/sync/' + item.id;
5953    };
5954
5955    $scope.showContinueShopping = function() {
5956        return !$scope.isWebPayment();
5957    };
5958
5959    $scope.showItemSummary = function() {
5960        if ($scope.isWebPayment()) {
5961            return false;
5962        }
5963        return ($scope.CartStatus.Valid && !$scope.StatusInfo.Initializing);
5964    };
5965
5966    $scope.showMainCheckout = function() {
5967        return ($scope.requireAddressInfo() || $scope.requireContactInfo() || $scope.RequireBillingInfo())
5968    }
5969
5970    // Should this really be isNMI? Determines if we show NMI option
5971    $scope.showSecureOrPaypal = function () {
5972        return $scope.CartStatus.Valid
5973            && !$scope.isVantivHosted()
5974            && !$scope.isRainProcessor()
5975            && !$scope.isQuiltAdyen()
5976            && !$scope.isPayPal()
5977            && $scope.StatusInfo.Processor !== 'gc'
5978            && !$scope.isWholesaleNoCCEnabled()
5979            && !$scope.isCartTotalExceeded();
5980    };
5981
5982    $scope.getCartSettings = function () {
5983        if (!$scope.InitInfo || !$scope.InitInfo.CartSettings) {
5984            return null;
5985        }
5986
5987        return $scope.InitInfo.CartSettings;
5988    };
5989
5990    $scope.getMaxCartTotal = function () {
5991        var cartSettings = $scope.getCartSettings();
5992
5993        if (!cartSettings || !cartSettings.hasOwnProperty('MaxCartTotal')) {
5994            return null;
5995        }
5996
5997        return +cartSettings.MaxCartTotal || null;
5998    };
5999
6000    $scope.isCartTotalExceeded = function () {
6001        var maxTotal = $scope.getMaxCartTotal();
6002
6003        if (!maxTotal) {
6004            return false;
6005        }
6006
6007        return $scope.SubTotal > maxTotal;
6008    };
6009
6010    // Determines if we show pre-checkout summary (Not NMI)
6011    $scope.showHostedCheckout = function () {
6012        if($scope.domainDisabled === true) {
6013            return false;
6014        }
6015        if ($scope.isWebPayment()) {
6016            return true;
6017        }
6018
6019        return $scope.isVantivHosted()
6020            || $scope.isRainProcessor()
6021            || $scope.isQuiltAdyen()
6022            || $scope.isPayPal()
6023            || $scope.StatusInfo.Processor === 'gc'
6024            || !$scope.requirePayment();
6025    };
6026
6027    $scope.showContactInfo = function() {
6028        return ($scope.CartStatus.Valid && !$scope.isWebPayment());
6029    };
6030
6031    $scope.showSummaries = function() {
6032        return ($scope.CartStatus.Valid && $scope.ShippingStatus.Entered && !$scope.ShippingStatus.Editing && !$scope.isWebPayment());
6033    };
6034
6035    $scope.showEnrollAutoPay = function() {
6036        return $scope.isWebPayment()
6037            && !$scope.isInvoicePayment()
6038            && !$scope.isInvoicesPayment()
6039            && !$scope.isAddCardOnly()
6040            && !$scope.isMmsPayment()
6041            && !$scope.isRtoPayment();
6042    };
6043
6044    // endregion
6045
6046    // ******************************************************************************************
6047    // region Shipping
6048    // ******************************************************************************************
6049
6050    $scope.shippingCountryHasRegions = function () {
6051        try {
6052            if ($scope.ShippingInfo.RegionsCount > 0) {
6053                return true;
6054            }
6055        } catch (e) {
6056            // Consume exception, return default of false
6057        }
6058
6059        return false;
6060    };
6061
6062    $scope.getShippingCountryKey = function () {
6063        var countryKey = null;
6064        if ($scope.ShippingInfo && $scope.ShippingInfo.CountryKey) {
6065            countryKey = $scope.ShippingInfo.CountryKey;
6066        }
6067        if ($scope.isPickup()) {
6068            countryKey = $scope.getStoreCountryKey();
6069        }
6070        return countryKey;
6071    };
6072
6073    $scope.hasShippingState = function() {
6074        return $scope.ShippingInfo.State;
6075    };
6076
6077    $scope.hasShippingZip = function() {
6078        return $scope.ShippingInfo.Zip;
6079    };
6080
6081    $scope.getShippingState = function() {
6082        if ($scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.address && $scope.ShippingInfo.Option.address.state) {
6083            return $scope.InitInfo.Store.state;
6084        }
6085        if ($scope.ShippingInfo && $scope.ShippingInfo.State) {
6086            return $scope.ShippingInfo.State;
6087        }
6088        return null;
6089    }
6090
6091    $scope.ShippingCountrySet = function (country) {
6092        $scope.setCountry($scope.ShippingInfo, country);
6093    };
6094
6095    $scope.getStoreCountryKey = function () {
6096        return $scope.CountryKeyGet($scope.InitInfo.DomesticCountry);
6097    };
6098
6099    $scope.getStoreState = function() {
6100        if ($scope.InitInfo && $scope.InitInfo.Store && $scope.InitInfo.Store.state) {
6101            return $scope.InitInfo.Store.state;
6102        }
6103        return null;
6104    };
6105
6106    $scope.setCountry = function (info, country) {
6107        // Set Country
6108        if (!country) {
6109            country = $scope.InitInfo.DomesticCountry;
6110        }
6111
6112
6113
6114        info.Country = country;
6115
6116        // Get Country Key
6117        info.CountryKey = $scope.CountryKeyGet(country);
6118
6119        if($scope.isExtendPayPalShippingInfo()) {
6120            info.CountryIso2Key = $scope.CountryIso2KeyGet(country);
6121        }
6122
6123        // Determine State or Province
6124        info.RegionsCount = 0;
6125        if (info.CountryKey) {
6126            switch (info.CountryKey) {
6127                case 'USA':
6128                case 'AUS':
6129                    info.StateOrProvince = 'State';
6130                    break;
6131                default:
6132                    info.StateOrProvince = 'Province';
6133                    break;
6134            }
6135
6136            info.ZipOrPostal = 'Postal Code';
6137            if (info.CountryKey === 'USA') {
6138                info.ZipOrPostal = 'Zip Code';
6139            }
6140
6141            // Regions Count
6142            info.RegionsCount = 0;
6143            if ($scope.InitInfo.Regions[info.CountryKey]) {
6144                info.RegionsCount = Object.keys($scope.InitInfo.Regions[info.CountryKey]).length;
6145            }
6146
6147            if (info.Country === $scope.InitInfo.DomesticCountry && info.RegionsCount == 0) {
6148                info.State = 'N/A';
6149            }
6150        } else {
6151            info.StateOrProvince = '';
6152        }
6153    };
6154
6155    $scope.isStorePickupEnabled = function() {
6156        if (!$scope.InitInfo || !$scope.InitInfo.StorePickupEnabled) {
6157            return false;
6158        }
6159
6160        if ($scope.hasOsrItem() && !$scope.isOsrStorePickupEnabled()) {
6161            return false;
6162        }
6163
6164        return true;
6165    };
6166
6167    $scope.isLocalDeliveryEnabled = function () {
6168        return $scope.knobby && $scope.knobby.module_add_local_delivery_tab;
6169    }
6170
6171    $scope.isStorePickup = function () {
6172        return ($scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.name === 'Store Pickup');
6173    };
6174
6175    $scope.isSchoolPickup = function () {
6176        return ($scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.name === 'School Delivery');
6177    };
6178
6179    $scope.isCurbsidePickupEnabled = function() {
6180        if (!$scope.InitInfo || !$scope.InitInfo.CurbsidePickupEnabled) {
6181            return false;
6182        }
6183
6184        if ($scope.hasOsrItem()) {
6185            return false;
6186        }
6187
6188        return true;
6189    };
6190
6191    $scope.isLocalDelivery = function() {
6192        return $scope.ShippingInfo?.Option?.name === 'Local Delivery';
6193    };
6194
6195    $scope.isCurbsidePickup = function() {
6196        return ($scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.name === 'Curbside Pickup');
6197    };
6198
6199    $scope.isPickup = function() {
6200        return $scope.isStorePickup() || $scope.isCurbsidePickup() || $scope.isSchoolPickup();
6201    };
6202
6203    $scope.isDelivery = function() {
6204        return false;
6205    };
6206
6207    $scope.isCartValid = function() {
6208        if ($scope.isWebPayment()) {
6209            return true;
6210        }
6211        return $scope.CartStatus.Valid;
6212    }
6213
6214    $scope.isContacStatusValid = function() {
6215        if ($scope.isWebPayment()) {
6216            return true;
6217        }
6218        return $scope.ContactStatus.Valid;
6219    }
6220
6221    $scope.getISO2digitCountryCode = function(country) {
6222        var iso3digit = $scope.CountryKeyGet(country);
6223        if (iso3digit === 'USA') {
6224            return 'US';
6225        }
6226    }
6227
6228    $scope.getIso2CountryCodeFromIso3 = function (country) {
6229        let isoMap = {
6230            "AFG":"AF","ALA":"AX","ALB":"AL","DZA":"DZ","ASM":"AS","AND":"AD","AGO":"AO","AIA":"AI","ATG":"AG","ARG":"AR","ARM":"AM","ABW":"AW","AUS":"AU","AUT":"AT","AZE":"AZ","BHS":"BS","BHR":"BH","BGD":"BD","BRB":"BB","BLR":"BY","BEL":"BE","BLZ":"BZ","BEN":"BJ","BMU":"BM","BTN":"BT","BOL":"BO","BIH":"BA","BWA":"BW","BRA":"BR","VGB":"VG","IOT":"IO","BRN":"BN","BGR":"BG","BFA":"BF","BDI":"BI","KHM":"KH","CMR":"CM","CAN":"CA","CPV":"CV","CYM":"KY","CAF":"CF","TCD":"TD","CHL":"CL","CHN":"CN","HKG":"HK","MAC":"MO","CXR":"CX","CCK":"CC","COL":"CO","COM":"KM","COG":"CG","COD":"CD","COK":"CK","CRI":"CR","CIV":"CI","HRV":"HR","CUB":"CU","CYP":"CY","CZE":"CZ","DNK":"DK","DJI":"DJ","DMA":"DM","DOM":"DO","ECU":"EC","EGY":"EG","SLV":"SV","GNQ":"GQ","ERI":"ER","EST":"EE","ETH":"ET","FLK":"FK","FRO":"FO","FJI":"FJ","FIN":"FI","FRA":"FR","GUF":"GF","PYF":"PF","GAB":"GA","GMB":"GM","GEO":"GE","DEU":"DE","GHA":"GH","GIB":"GI","GRC":"GR","GRL":"GL","GRD":"GD","GLP":"GP","GUM":"GU","GTM":"GT","GGY":"GG","GIN":"GN","GNB":"GW","GUY":"GY","HTI":"HT","VAT":"VA","HND":"HN","HUN":"HU","ISL":"IS","IND":"IN","IDN":"ID","IRN":"IR","IRQ":"IQ","IRL":"IE","IMN":"IM","ISR":"IL","ITA":"IT","JAM":"JM","JPN":"JP","JEY":"JE","JOR":"JO","KAZ":"KZ","KEN":"KE","KIR":"KI","PRK":"KP","KOR":"KR","KWT":"KW","KGZ":"KG","LAO":"LA","LVA":"LV","LBN":"LB","LSO":"LS","LBR":"LR","LBY":"LY","LIE":"LI","LTU":"LT","LUX":"LU","MKD":"MK","MDG":"MG","MWI":"MW","MYS":"MY","MDV":"MV","MLI":"ML","MLT":"MT","MHL":"MH","MTQ":"MQ","MRT":"MR","MUS":"MU","MYT":"YT","MEX":"MX","FSM":"FM","MDA":"MD","MCO":"MC","MNG":"MN","MNE":"ME","MSR":"MS","MAR":"MA","MOZ":"MZ","MMR":"MM","NAM":"NA","NRU":"NR","NPL":"NP","NLD":"NL","NCL":"NC","NZL":"NZ","NIC":"NI","NER":"NE","NGA":"NG","NIU":"NU","NFK":"NF","MNP":"MP","NOR":"NO","OMN":"OM","PAK":"PK","PLW":"PW","PSE":"PS","PAN":"PA","PNG":"PG","PRY":"PY","PER":"PE","PHL":"PH","POL":"PL","PRT":"PT","PRI":"PR","QAT":"QA","REU":"RE","ROU":"RO","RUS":"RU","RWA":"RW","BLM":"BL","SHN":"SH","KNA":"KN","LCA":"LC","MAF":"MF","SPM":"PM","VCT":"VC","WSM":"WS","SMR":"SM","STP":"ST","SAU":"SA","SEN":"SN","SRB":"RS","SYC":"SC","SLE":"SL","SGP":"SG","SVK":"SK","SVN":"SI","SLB":"SB","SOM":"SO","ZAF":"ZA","SSD":"SS","ESP":"ES","
6230LKA":"LK","SDN":"SD","SUR":"SR","SJM":"SJ","SWZ":"SZ","SWE":"SE","CHE":"CH","SYR":"SY","TWN":"TW","TJK":"TJ","TZA":"TZ","THA":"TH","TLS":"TL","TGO":"TG","TKL":"TK","TON":"TO","TTO":"TT","TUN":"TN","TUR":"TR","TKM":"TM","TCA":"TC","TUV":"TV","UGA":"UG","UKR":"UA","ARE":"AE","GBR":"GB","USA":"US","URY":"UY","UZB":"UZ","VUT":"VU","VEN":"VE","VNM":"VN","VIR":"VI","WLF":"WF","ESH":"EH","YEM":"YE","ZMB":"ZM","ZWE":"ZW"
6231        };
6232
6233        return isoMap[country] || null;
6234    };
6235
6236    /**  @return {string} */
6237    $scope.CountryKeyGet = function (country) {
6238        if (!country || !$scope.InitInfo.Countries) {
6239            return null;
6240        }
6241
6242        var countries = $scope.InitInfo.Countries;
6243        for (var property in countries) {
6244            if (countries.hasOwnProperty(property)) {
6245                if (countries[property] === country) {
6246                    return property;
6247                }
6248            }
6249        }
6250
6251        return null;
6252    };
6253
6254    /**  @return {string} */
6255    $scope.CountryIso2KeyGet = function (country) {
6256        if (!country || !$scope.InitInfo.CountriesIso2) {
6257            return null;
6258        }
6259
6260        var countries = $scope.InitInfo.CountriesIso2;
6261        for (var property in countries) {
6262            if (countries.hasOwnProperty(property)) {
6263                if (countries[property] === country) {
6264                    return property;
6265                }
6266            }
6267        }
6268
6269        return null;
6270    };
6271
6272    $scope.changeShippingCountry = function () {
6273        $scope.ShippingInfo.State = null;
6274        $scope.ShippingInfo.Zip = null;
6275        $scope.ShippingStatus.DirtyZip = null; // Force options
6276        $scope.ShippingStatus.DirtyState = null; // Force options
6277        $scope.ShippingOptionsGet();
6278        $scope.ProcessItems();
6279    };
6280
6281    $scope.shippingOptionsRequired = function () {
6282        return ($scope.isShippingRequired() && !$scope.InitInfo.PayPalShippingEnabled);
6283    };
6284
6285    $scope.setShippingOption = function() {
6286        let element = null;
6287        if ($scope.isStorePickup()) {
6288            element = $('#inStorePickupTab');
6289        } else if ($scope.isCurbsidePickup()) {
6290            element = $('#curbsidePickupTab');
6291        } else if ($scope.isLocalDeliveryEnabled() && $scope.isLocalDelivery()) {
6292            element = $('#localDeliveryTab');
6293        } else if ($scope.isSchoolPickup()) {
6294            element = $('#schoolPickupTab');
6295        } else if (!$scope.isShippingEnabled()) {
6296            if ($scope.isStorePickupEnabled()) {
6297                element = $('#inStorePickupTab');
6298            } else if ($scope.isCurbsidePickupEnabled()) {
6299                element = $('#curbsidePickupTab');
6300            } else if ($scope.isOsrSchoolPickupEnabled()) {
6301                element = $('#schoolPickupTab');
6302            }
6303        }
6304
6305        if (element && typeof element.tab !== "undefined") {
6306            element.tab('show');
6307        }
6308    };
6309
6310    $scope.otherPickupOptionsEnabled = function () {
6311        return $scope.InitInfo.StorePickupEnabled || $scope.InitInfo.CurbsidePickupEnabled;
6312    };
6313
6314    $scope.ShippingOptionsGet = function (forceUpdate) {
6315        // Handle shipping calc data based on geo location data
6316        if (!$scope.ShippingInfo.State) {
6317            $scope.ShippingInfo.State = $scope.shippingCalcState;
6318            if (
6319                $scope.InitInfo.DomesticCountry === $scope.ShippingInfo.Country &&
6320                !$scope.ShippingInfo.State &&
6321                !$scope.otherPickupOptionsEnabled()
6322            ) {
6323                $scope.ShippingStatus.OptionsValid = false;
6324                $scope.enhancedShippingInfo.shippingOptions = {};
6325                $scope.enhancedShippingInfo.localDeliveryOptions = {};
6326                return $scope.NoOpPromise();
6327            }
6328        }
6329        if ($scope.ShippingInfo.Zip) {
6330            $scope.shippingCalcZipCode = $scope.ShippingInfo.Zip;
6331            $scope.geoLocationZipCode = '';
6332        }
6333
6334        if ($scope.geoLocationZipCode !== '' && $scope.checkFlagSendMixPanelEventZipCodeChange()) {
6335            sendMixpanelEvent('Zipcode- changed', {'button-location': 'Website Checkout - Shipping Options'});
6336        }
6337
6338        $scope.Validate();
6339        if (!$scope.isShippingRequired()) {
6340            // Zero-out the shipping options
6341            $scope.ShippingInfo.Option = null;
6342            $scope.ShippingInfo.OptionId = null;
6343            $scope.ShippingSubtotal = null;
6344            return $scope.NoOpPromise();
6345        }
6346
6347        if ($scope.InitInfo.ShipStationEnabled) {
6348            // When pickup-without-zip flag is ON and pickup options are enabled, we skip zip validation –
6349            // we still want to call the endpoint so that pickup rates are returned, even if ShipStation will error.
6350            var skipZipValidation = $scope.otherPickupOptionsEnabled() && !$scope.shippingCalcZipCode;
6351
6352            if (!skipZipValidation) {
6353                var postalInvalid;
6354                if ($scope.knobby && $scope.knobby['module_validate_shipping_postal_format']) {
6355                    postalInvalid = !isValidPostalCode($scope.shippingCalcZipCode, $scope.ShippingInfo.CountryKey);
6356                } else {
6357                    postalInvalid = !$scope.shippingCalcZipCode || ($scope.ShippingInfo.CountryKey === 'USA' && $scope.shippingCalcZipCode.length < 5) || ($scope.ShippingInfo.CountryKey && $scope.shippingCalcZipCode.length < 3)
6358                }
6359
6360                if (postalInvalid) {
6361                    $scope.ShippingStatus.OptionsValid = false;
6362                    $scope.ShippingStatus.Message = "Invalid zip/postal code";
6363                    if ($scope.isNewUIFlagSet()) {
6364                        $scope.enhancedShippingInfo.shippingOptions = {};
6365                        $scope.enhancedShippingInfo.localDeliveryOptions = {};
6366                    } else {
6367                        $scope.ShippingInfo.Options = null;
6368                        return $scope.NoOpPromise();
6369                    }
6370                } else {
6371                    $scope.ShippingStatus.OptionsValid = true;
6372                    $scope.ShippingStatus.Message = "";
6373                }
6374            }
6375
6376            if (!forceUpdate && $scope.ShippingStatus.DirtyZip === $scope.shippingCalcZipCode && $scope.ShippingStatus.DirtyState === $scope.ShippingInfo.State && $scope.ShippingInfo.Options) {
6377                // Don't waste our time, nothing has changed
6378                return $scope.NoOpPromise();
6379            }
6380            $scope.ShippingStatus.DirtyZip = $scope.shippingCalcZipCode;
6381            $scope.ShippingStatus.DirtyState = $scope.ShippingInfo.State;
6382        }
6383
6384        var local = $scope.ShippingInfo.Country === $scope.InitInfo.DomesticCountry;
6385
6386        // Check if we should allow pickup options without zip code
6387        var allowPickupWithoutZip = $scope.otherPickupOptionsEnabled();
6388
6389        // For regular shipping validation (when no pickup options available)
6390        var stateMissingForRegularShipping = $scope.ShippingInfo.RegionsCount > 0 && !$scope.ShippingInfo.State;
6391
6392        // Block API call only if:
6393        // 1. No country info, OR
6394        // 2. No pickup options available AND regular shipping requirements not met
6395        if (
6396            (!$scope.ShippingInfo.Country && !local) ||
6397            (!$scope.otherPickupOptionsEnabled() && stateMissingForRegularShipping && local && !$scope.InitInfo.ShipStationEnabled && !allowPickupWithoutZip)
6398        ) {
6399            $scope.ShippingStatus.OptionsValid = false;
6400            if ($scope.isNewUIFlagSet()) {
6401                $scope.enhancedShippingInfo.shippingOptions = {};
6402                $scope.enhancedShippingInfo.localDeliveryOptions = {};
6403                $scope.updateGstTotals();
6404            } else {
6405                $scope.ShippingInfo.Options = null;
6406                $scope.updateGstTotals();
6407                return $scope.NoOpPromise();
6408            }
6409        }
6410
6411        // Update dynamic taxes based on new shipping info
6412        return $scope.updateDynamicTaxesOnce().then(function () {
6413            $scope.ShippingStatus.OptionsUpdating++;
6414            var params = {
6415                shipping_state: $scope.ShippingInfo.State,
6416                shipping_country: $scope.ShippingInfo.Country,
6417                shipping_postal: $scope.shippingCalcZipCode,
6418                qty: $scope.CartQty,
6419                subtotal: $scope.ShippingSubtotal,
6420                discount: $scope.Discounts,
6421                weight: $scope.Weight,
6422                for_web: 1,
6423                dimensions: $scope.getLargestDimensions()
6424            };
6425
6426            var osrItem = $scope.getOsrItem();
6427            if (osrItem) {
6428                params.osr_rto_contract_id = +osrItem.id;
6429            }
6430            return $http.get($scope.BuildUrl('/checkout/shippingoptions'), {
6431                params: params,
6432                withCredentials: true
6433            }).then(function ($result) {
6434                var options = $result.data.shipping;
6435                var isErrorResponse = options && ((!options.length && options.error_message) || options.primary_provider_error);
6436
6437                if (isErrorResponse) {
6438                    // Preserve error information so the UI can alert the customer
6439                    $scope.ShippingStatus.OptionsValid = false;
6440                    $scope.ShippingInfo.Options = options;
6441                    $scope.ShippingStatus.Message = options.error_message;
6442
6443                    if (options.primary_provider_error && options.primary_provider_error.error) {
6444                        $scope.ShippingStatus.Message = options.primary_provider_error.error;
6445                    }
6446
6447                    // Still build the enhanced option lists so that pickup choices render
6448                    $scope.ShippingVerify();
6449                    $scope.Validate();
6450
6451                    // If pickup options are indeed present, mark options as valid so UI shows tabs
6452                    if ($scope.hasStorePickupOptions() || $scope.hasCurbsidePickupOptions() || $scope.hasSchoolPickupOptions() || $scope.hasLocalDeliveryOptions()) {
6453                        $scope.ShippingStatus.OptionsValid = true;
6454                    }
6455                } else {
6456                    // Normal success path (no carrier errors)
6457                    if (!$scope.isNewUIFlagSet()) {
6458                        $scope.ShippingStatus.OptionsValid = true;
6459                    }
6460                    $scope.ShippingInfo.Options = options;
6461                    $scope.ShippingVerify();
6462                    $scope.Validate();
6463                }
6464                $scope.ShippingStatus.OptionsUpdating--;
6465                console.log($scope.ShippingInfo);
6466
6467                if ($scope.knobby && $scope.knobby.module_hide_shipping_tab) {
6468                    const allowedNames = ['Store Pickup', 'Curbside Pickup', 'School Delivery', 'Local Delivery'];
6469                    const optionList = Object.values(options || {});
6470
6471                    $scope.isShippingOptionsProvided = optionList.some(o => !allowedNames.includes(o.name));
6472                    $scope.isLocalDeliveryOptionsProvided = optionList.some(o => o.name === 'Local Delivery');
6473                }
6474            });
6475        });
6476    };
6477
6478    $scope.updateDigitalOrder = function() {
6479        if ($scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
6480            var downloadableItems = $scope.CartData.Items.filter(function (item) { return item.downloadable });
6481            $scope.digitalOrder = downloadableItems.length === $scope.CartData.Items.length
6482            if ($scope.digitalOrder) {
6483                $scope.ShippingInfo.Address1 = null;
6484                $scope.ShippingInfo.Address2 = null;
6485                $scope.ShippingInfo.City = null;
6486                $scope.ShippingInfo.State = null;
6487                $scope.ShippingInfo.Zip = null;
6488                if (!$scope.digitalOrder) {
6489                    $scope.dynamicRateCache = null;
6490                    $scope.dynamicRateCacheKey = null;
6491                }
6492                $scope.SessionStorageSet();
6493            }
6494        } else {
6495            $scope.digitalOrder = false;
6496        }
6497    }
6498
6499    $scope.BillingOptionsGet = function() {
6500        // Update dynamic taxes flag
6501        if ($scope.digitalOrder) {
6502            $scope.Validate();
6503        } else {
6504            $scope.getTaxTable();
6505        }
6506    };
6507
6508    $scope.updateGstTotals = function () {
6509
6510        $scope.TotalsCalculate();
6511        $scope.GST_Due -= ($scope.ShippingTotal + $scope.ShippingTaxes);
6512        $scope.ShippingTaxes = 0;
6513    };
6514
6515    $scope.ShippingVerify = function () {
6516        // If feature flag is enabled and cart contains only classes/gift cards/subscriptions/digital downloads, skip all shipping validation
6517        if ($scope.checkFlagBugClassesCantCheckOutEcommerce() && $scope.isGCClassSubscriptionCombinationOnly()) {
6518            $scope.ShippingStatus.Valid = true;
6519            $scope.ShippingStatus.NotValidMessages = null;
6520            $scope.SessionStorageSet();
6521            return Promise.resolve();
6522        }
6523
6524        if ((!$scope.isShippingRequired() || $scope.InitInfo.PayPalShippingEnabled) && !$scope.InitInfo.StorePickupEnabled && !$scope.InitInfo.CurbsidePickupEnabled) {
6525            $scope.ShippingStatus.Valid = true;
6526            $scope.ShippingStatus.NotValidMessages = null;
6527            $scope.SessionStorageSet();
6528            return Promise.resolve();
6529        }
6530
6531        if ($scope.digitalOrder) {
6532            $scope.ShippingStatus.Valid = true;
6533            $scope.ShippingStatus.NotValidMessages = null;
6534            $scope.SessionStorageSet();
6535            if ($scope.digitalOrder) {
6536                // Update taxes since things may have changed
6537                return $scope.updateDynamicTaxesOnce().then(function () {
6538                    $scope.TotalsCalculate();
6539                    if ($scope.BillingStatus.Entered) {
6540                        $scope.BillingVerify();
6541                    }
6542                    $scope.safeApply();
6543                });
6544            } else {
6545                return Promise.resolve();
6546            }
6547        }
6548
6549        // Default Country
6550
6551        $scope.ShippingInfo.Country = null;
6552        if ($scope.ShippingInfo.CountryKey && $scope.InitInfo.Countries) {
6553            if ($scope.InitInfo.InternationalEnabled === false) {
6554                $scope.ShippingCountrySet($scope.InitInfo.DomesticCountry);
6555            } else {
6556                $scope.ShippingCountrySet($scope.InitInfo.Countries[$scope.ShippingInfo.CountryKey]);
6557            }
6558        }
6559
6560        // Show currency
6561        $scope.InitInfo.CurrencyCode = '';
6562        if ($scope.ShippingInfo.Country && $scope.InitInfo.DomesticCountry && $scope.ShippingInfo.Country !== $scope.InitInfo.DomesticCountry) {
6563            $scope.InitInfo.CurrencyCode = $scope.InitInfo.Currency;
6564        }
6565
6566        // Shipping Option
6567        if (!$scope.ShippingInfo.State && !$scope.ShippingInfo.Country) {
6568            $scope.ShippingInfo.OptionId = 0;
6569        }
6570
6571        if ($scope.ShippingInfo.OptionId && $scope.ShippingInfo.Options) {
6572            $scope.ShippingInfo.Option = $scope.ShippingInfo.Options[$scope.ShippingInfo.OptionId];
6573        } else {
6574            $scope.ShippingInfo.Option = null;
6575        }
6576
6577        // Separate Options
6578        try {
6579            var allOptions = $scope.ShippingInfo.Options;
6580            $scope.enhancedShippingInfo.shippingOptions = {};
6581            $scope.enhancedShippingInfo.storePickupOptions = {};
6582            $scope.enhancedShippingInfo.curbsidePickupOptions = {};
6583            $scope.enhancedShippingInfo.schoolPickupOptions = {};
6584            $scope.enhancedShippingInfo.localDeliveryOptions = {};
6585
6586            if (allOptions) {
6587                Object.keys(allOptions).forEach(
6588                    function (key) {
6589                        var option = this[key];
6590                        if (!option.hasOwnProperty('name') || ($scope.checkFlagRestoreSchoolDeliveryShippingOption() && option.name === '')) {
6591                            return;
6592                        }
6593                        var optionName = option.name;
6594
6595                        if (optionName === 'Store Pickup') {
6596                            $scope.enhancedShippingInfo.storePickupOptions[key] = option;
6597                        } else if (optionName === 'Curbside Pickup') {
6598                            $scope.enhancedShippingInfo.curbsidePickupOptions[key] = option;
6599                        } else if (optionName === 'School Delivery' && ($scope.canShowShipToSchool() && $scope.ShippingInfo.shipToSchool && $scope.ShippingInfo.selectedSchool)) {
6600                            $scope.enhancedShippingInfo.shippingOptions[key] = option;
6601                        } else if (optionName === 'School Delivery') {
6602                            $scope.enhancedShippingInfo.schoolPickupOptions[key] = option;
6603                        } else {
6604                            if ($scope.isNewUIFlagSet()) {
6605                                $scope.ShippingStatus.OptionsValid = true;
6606                            }
6607                            // Only include regular shipping options if address requirements are met and no validation errors
6608                            var zipProvided = $scope.hasShippingZip();
6609                            var stateValid = $scope.ShippingInfo.RegionsCount > 0 ? $scope.hasShippingState() : true;
6610                            //If zip is not provided, it will fail for other reasons, If zip is provided make sure there are no error messages
6611                            var hasOtherValidationErrors = zipProvided && $scope.showShippingErrorMessage();
6612
6613                            if (zipProvided && stateValid && !hasOtherValidationErrors) {
6614                                if ($scope.isLocalDeliveryEnabled() && optionName === 'Local Delivery') {
6615                                    $scope.enhancedShippingInfo.localDeliveryOptions[key] = option;
6616                                } else {
6617                                    $scope.enhancedShippingInfo.shippingOptions[key] = option;
6618                                }
6619                            }
6620                        }
6621                    },
6622                    allOptions
6623                );
6624            }
6625
6626            if ($scope.isPickup() && !$scope.isSchoolPickup()) {
6627                $scope.BillingInfo.AddressSameAsShipping = true;
6628                $scope.BillingAddressVerify();
6629            } else if ($scope.StatusInfo.payAtPickup) {
6630                $scope.resetPayAtPickup();
6631            }
6632        } catch (err) {
6633            // Consume and log, don't let errors affect existing behavior
6634            if (window.console) {
6635                console.error('Error Separating Shipping Options: ' + err);
6636            }
6637        }
6638
6639        // Update Local Storage
6640        $scope.ShippingStatus.Valid = ($scope.shippingForm && $scope.shippingForm.$valid);
6641        $scope.SessionStorageSet();
6642
6643        if (!$scope.ShippingInfo.Option && $scope.areShippingOptionsRequired()) {
6644            $scope.ShippingStatus.Valid = false;
6645        }
6646
6647        // Update taxes since things may have changed
6648        return $scope.updateDynamicTaxesOnce().then(function () {
6649            $scope.TotalsCalculate();
6650            if ($scope.BillingStatus.Entered) {
6651                $scope.BillingVerify();
6652            }
6653            $scope.safeApply();
6654        });
6655    };
6656
6657    $scope.checkShippingCoupon = function () {
6658        var coupon = $scope.getFreeShippingCoupon();
6659
6660        $scope.couponStatusMessage = '';
6661
6662        if (!coupon) {
6663            return;
6664        }
6665
6666        coupon.discount = 0;
6667
6668        if (!$scope.ShippingInfo.Option) {
6669            $scope.couponStatusMessage = 'Shipping Selection Required';
6670            return;
6671        }
6672
6673        var shippingAmount = $scope.getShippingTotal();
6674
6675        if (shippingAmount > coupon.maxDiscount && coupon.maxDiscount > 0) {
6676            $scope.couponStatusMessage ='Shipping max limit of '+$scope.InitInfo.CurrencySymbol2+parseFloat(coupon.maxDiscount).toFixed(2);
6677        }
6678
6679        coupon.discount = $scope.getShippingDiscount();
6680    };
6681
6682    $scope.ShippingEdit = function () {
6683        $scope.Validate();
6684        $scope.ShippingStatus.Entered = true;
6685        $scope.ShippingStatus.Editing = true;
6686        $scope.BillingStatus.Editing = false;
6687        $scope.TotalsCalculate();
6688    };
6689
6690    $scope.showShippingForm = function() {
6691        if ($scope.isFixCheckoutForClassesFlagEnabled()) {
6692            return $scope.CartStatus.Valid && ($scope.ShippingStatus.Editing || !$scope.ShippingStatus.Entered);
6693        }
6694
6695        if (!$scope.checkFlagBugClassesCantCheckOutEcommerce()) {
6696            return ($scope.CartStatus.Valid && ($scope.ShippingStatus.Editing || !$scope.ShippingStatus.Entered));
6697        } else {
6698            return ($scope.CartStatus.Valid && $scope.isShippingRequired() && ($scope.ShippingStatus.Editing || !$scope.ShippingStatus.Entered));
6699        }
6700    };
6701
6702    $scope.showShippingOptionsForm = function () {
6703        return ($scope.CartStatus.Valid && $scope.requirePayment() && $scope.isPayPal() && !$scope.disableWholesaleShipping());
6704    };
6705
6706    $scope.showShippingAddressForm = function() {
6707        return $scope.isShippingRequired();
6708    };
6709
6710    $scope.isShippingEnabled = function() {
6711        if (!$scope.InitInfo.ShippingEnabled && !$scope.InitInfo.ShipStationEnabled) {
6712            // Not sure this is correct
6713            return !$scope.isStorePickupEnabled() && !$scope.isCurbsidePickupEnabled();
6714        }
6715
6716        if ($scope.hasOsrItem() && !$scope.isOsrShippingEnabled()) {
6717            return !$scope.isOsrStorePickupEnabled() && !$scope.isOsrSchoolPickupAvailable();
6718        }
6719
6720        return true;
6721    };
6722
6723    $scope.isShippingTabVisible = function () {
6724        if ($scope.knobby && $scope.knobby.module_hide_shipping_tab && !$scope.isShippingOptionsProvided) {
6725            return false;
6726        }
6727
6728        return $scope.isShippingEnabled();
6729    };
6730
6731    $scope.isShippingFormVisible = function () {
6732        return $scope.isShippingEnabled() || $scope.isLocalDeliveryEnabled();
6733    };
6734
6735    $scope.showNoShippingRequired = function() {
6736        return ($scope.CartStatus.Valid && !$scope.isShippingRequired() && !$scope.isPayPal());
6737    };
6738
6739    $scope.isShippingRequired = function() {
6740        if ($scope.isWebPayment()) {
6741            return false;
6742        }
6743
6744        return $scope.ShippingStatus.Required;
6745    };
6746
6747    $scope.isShippingNeeded = function() {
6748        return (!$scope.ShippingStatus.Valid
6749            && $scope.isShippingRequired()
6750            && !$scope.isPickup()
6751            && !$scope.isPayAtPickup()
6752        );
6753    };
6754
6755    $scope.isShippingEntered = function() {
6756        if ($scope.isWebPayment()) {
6757            return true;
6758        }
6759        return $scope.ShippingStatus.Entered;
6760    }
6761
6762    $scope.areShippingOptionsRequired = function() {
6763        // If Wholesale is enabled, they also need to enable wholesale shipping in order to see shipping options
6764        if ($scope.disableWholesaleShipping()) {
6765            return false;
6766        }
6767
6768        // Always show shipping options if pickup is enabled
6769        if ($scope.isGCClassSubscriptionCombinationOnly()) {
6770            return false;
6771        }
6772
6773        if (($scope.hasOsrItem() && $scope.isOsrStorePickupEnabled()) || $scope.otherPickupOptionsEnabled()) {
6774            return true;
6775        }
6776
6777        // For regular shipping, require zip and state only if country has regions
6778        var stateMissingForRegularShipping = $scope.ShippingInfo.RegionsCount > 0 && !$scope.ShippingInfo.State;
6779        if (!$scope.ShippingInfo.Zip || stateMissingForRegularShipping) {
6780            return $scope.otherPickupOptionsEnabled();
6781        }
6782
6783        // NOTE: Store can disable shipping and then collect at PayPal
6784        if ($scope.isWholesaleNoCCEnabled()) {
6785            return false;
6786        }
6787
6788        return $scope.isShippingRequired();
6789    };
6790
6791
6792    $scope.isShippingOptionsValid = function() {
6793        return ($scope.ShippingStatus && $scope.ShippingStatus.OptionsValid);
6794    };
6795
6796    $scope.showShippingErrorMessage = function() {
6797        return ($scope.ShippingStatus && $scope.ShippingStatus.Message);
6798    };
6799
6800    $scope.showShippingStateRequired = function() {
6801        return (!$scope.showShippingErrorMessage() && !$scope.isShippingOptionsValid() && !$scope.hasShippingState());
6802    };
6803
6804
6805    $scope.showNoShippingOptions = function() {
6806        if ($scope.showShippingErrorMessage()) {
6807            return false;
6808        }
6809        if ($scope.showShippingStateRequired()) {
6810            return false;
6811        }
6812        if (!$scope.isShippingOptionsValid() && $scope.hasShippingState()) {
6813            return true;
6814        }
6815        if ($scope.isShippingOptionsValid() && !$scope.hasShippingOptions()) {
6816            if (!$scope.hasShippingState() || !$scope.hasShippingZip()) {
6817                return false;
6818            }
6819
6820            return true;
6821        }
6822        return false;
6823    };
6824
6825    $scope.hasShippingOptions = function () {
6826        try {
6827            var hasRegularShipping = false;
6828            var hasPickupOptions = false;
6829
6830            // Check for regular shipping options - always require proper validation
6831            // State is only required if country has regions
6832            var zipProvided = $scope.hasShippingZip();
6833            var stateValid = $scope.ShippingInfo.RegionsCount > 0 ? $scope.hasShippingState() : true;
6834
6835            if (zipProvided && stateValid) {
6836                var options = $scope.enhancedShippingInfo.shippingOptions;
6837                var keys = Object.keys(options);
6838                hasRegularShipping = (keys.length > 0);
6839            }
6840
6841            hasPickupOptions = $scope.hasStorePickupOptions() || $scope.hasCurbsidePickupOptions() || $scope.hasSchoolPickupOptions() || $scope.hasLocalDeliveryOptions();
6842
6843            return hasRegularShipping || hasPickupOptions;
6844        } catch (err) {
6845            return false;
6846        }
6847    };
6848
6849    $scope.hasStorePickupOptions = function () {
6850        try {
6851            var options = $scope.enhancedShippingInfo.storePickupOptions;
6852            var keys = Object.keys(options);
6853            return (keys.length > 0);
6854        } catch (err) {
6855            return false;
6856        }
6857    };
6858
6859    $scope.hasCurbsidePickupOptions = function () {
6860        try {
6861            var options = $scope.enhancedShippingInfo.curbsidePickupOptions;
6862            var keys = Object.keys(options);
6863            return (keys.length > 0);
6864        } catch (err) {
6865            return false;
6866        }
6867    };
6868
6869    $scope.hasLocalDeliveryOptions = function () {
6870        var info = $scope.enhancedShippingInfo || {};
6871
6872        function hasAny(opt) {
6873            if (!opt) {
6874                return false;
6875            }
6876
6877            return Array.isArray(opt) ? opt.length > 0 : Object.keys(opt).length > 0;
6878        }
6879
6880        var stateValid = $scope.ShippingInfo.RegionsCount > 0 ? !!$scope.hasShippingState() : true;
6881        var addressGate = $scope.hasShippingZip() && stateValid;
6882        var hasRegularShipping = hasAny(info.shippingOptions) && addressGate;
6883        var hasLocalDelivery = addressGate && hasAny(info.localDeliveryOptions);
6884
6885        var hasPickupOptions = (
6886            $scope.hasStorePickupOptions() ||
6887            $scope.hasCurbsidePickupOptions() ||
6888            $scope.hasSchoolPickupOptions() ||
6889            hasLocalDelivery
6890        );
6891
6892        return !!(hasRegularShipping || hasPickupOptions);
6893    };
6894
6895    $scope.hasSchoolPickupOptions = function () {
6896        try {
6897            return Object.keys($scope.enhancedShippingInfo.schoolPickupOptions).length > 0;
6898        } catch (err) {
6899            return false;
6900        }
6901    };
6902
6903    $scope.focusShippingCalcZipCode = function () {
6904        $('#shipping_calc_zip').select();
6905    };
6906
6907    $scope.changeShippingCalcZipCode = function () {
6908        var zipCode = $('#shipping_calc_zip').val();
6909        $scope.ShippingInfo.Zip = zipCode;
6910        $scope.ShippingOptionsGet();
6911    };
6912
6913    if ($scope.knobby && $scope.knobby['module_glitch_online_orders_canadian_tax_missing']) {
6914        $scope.$watch('ShippingInfo.State', function (newValue) {
6915            if (newValue && $scope.InitInfo?.Regions && $scope.InitInfo.Regions[$scope.ShippingInfo?.CountryKey]) {
6916                const validKeys = Object.keys($scope.InitInfo.Regions[$scope.ShippingInfo.CountryKey]);
6917                if (!validKeys.includes(newValue)) {
6918                    $scope.ShippingInfo.State = '';
6919                }
6920            }
6921        });
6922    }
6923
6924    $scope.checkCaptcha = function (skipBilling) {
6925        if ($scope.knobby && $scope.knobby.module_update_to_captcha_v3) {
6926            if (!$scope.isMinimumAmountMet()) {
6927                return;
6928            }
6929
6930            if (!$scope.isCaptchaEnabled()) {
6931                if ($scope.skipBilling() || skipBilling) {
6932                    $scope.ShippingDone(false);
6933                } else {
6934                    $scope.ShippingDone(true, 'BillingEdit');
6935                }
6936                return Promise.resolve();
6937            }
6938
6939            var authToken = $scope.InitInfo.store_token;
6940            var url = $scope.RainApiUrl + 'store/captcha/validate';
6941            var headers = {};
6942            if (authToken) {
6943                headers.Authorization = 'Bearer ' + authToken;
6944            }
6945
6946            return new Promise(function(resolve, reject) {
6947                grecaptcha.ready(function() {
6948                    grecaptcha.execute(window.recaptchaSiteKey, {action: 'checkout'})
6949                        .then(function(token) {
6950                            $http.post(url, {token: token}, {headers: headers})
6951                                .then(function() {
6952                                    $scope.showCaptcha = false;
6953
6954                                    if ($scope.skipBilling() || skipBilling) {
6955                                        $scope.ShippingDone(false);
6956                                    } else {
6957                                        $scope.ShippingDone(true, 'BillingEdit');
6958                                    }
6959
6960                                    resolve(token);
6961                                })
6962                                .catch(function(response) {
6963                                    $scope.showCaptcha = true;
6964
6965                                    $.growlUI('Error <i class="fa fa-exclamation text-danger"></i>', response.data.error);
6966
6967                                    reject(response.data.error);
6968                                });
6969                        });
6970                });
6971            });
6972        }
6973
6974        if (!$scope.isMinimumAmountMet()) {
6975            return;
6976        }
6977
6978        if ($scope.showCaptcha !== true) {
6979            if ($scope.skipBilling() || skipBilling) {
6980                $scope.ShippingDone(false);
6981            } else {
6982                $scope.ShippingDone(true, 'BillingEdit');
6983            }
6984            return;
6985        }
6986
6987        return $http.get($scope.BuildUrl('/captcha/check/' + window.captchaResponse), {withCredentials: true}).then(function (result) {
6988            $scope.StatusInfo.ErrorMessageBottom = null;
6989            if (result.data.success === true) {
6990                $scope.showCaptcha = false;
6991
6992                if ($scope.skipBilling() || skipBilling) {
6993                    $scope.ShippingDone(false);
6994                } else {
6995                    $scope.ShippingDone(true, 'BillingEdit');
6996                }
6997            } else {
6998                $scope.StatusInfo.ErrorMessageBottom = "Please click the button that says, \"I'm not a robot\", to continue.";
6999                $scope.showCaptcha = true;
7000                recaptchaOnload();
7001                return false;
7002            }
7003        });
7004    };
7005
7006    $scope.ShippingDone = function (checkContactInfo, next) {
7007        $scope.resetPayAtPickup();
7008
7009        if ($scope.contactForm) {
7010            $scope.contactForm.$setSubmitted();
7011            if ($scope.contactForm.$invalid) {
7012                return;
7013            }
7014        }
7015
7016        $scope.ShippingStatus.Submitted = true;
7017        if ($scope.isShippingRequired() && $scope.shippingForm) {
7018            $scope.shippingForm.$setSubmitted();
7019
7020            if ($scope.shippingForm.$invalid) {
7021                return;
7022            }
7023        }
7024        $scope.Validate();
7025
7026        var contactValid = checkContactInfo ? $scope.ContactStatus.Valid : true;
7027        $scope.ShippingStatus.MessagesShow = false;
7028
7029        if ($scope.ShippingStatus.Valid && contactValid) {
7030
7031            $scope.SessionUpdate();
7032            if ($scope.skipBilling()) {
7033                $scope.CheckoutProcess();
7034            } else if (!$scope.isResponsive() && !$scope.isNewUIFlagSet() && ($scope.isVantivHosted() || $scope.isRainProcessor() || $scope.isQuiltAdyen())) {
7035                $scope.CheckoutProcess();
7036            } else if (next) {
7037                $scope.ShippingStatus.Entered = true;
7038                if (next == 'BillingEdit') {
7039                    $scope.BillingEdit();
7040                }
7041            }
7042        } else {
7043            $scope.ShippingStatus.MessagesShow = !$scope.ShippingStatus.Valid;
7044            $scope.ContactStatus.MessagesShow = !contactValid;
7045        }
7046    };
7047
7048    // endregion
7049
7050    // ******************************************************************************************
7051    // region Billing
7052    // ******************************************************************************************
7053
7054    $scope.isPayAtPickup = function() {
7055        return ($scope.StatusInfo && $scope.StatusInfo.payAtPickup);
7056    };
7057
7058    $scope.payAtPickupSelected = function() {
7059        $scope.StatusInfo.payAtPickup = true;
7060        $scope.updateProcessor();
7061        $scope.Validate();
7062    };
7063
7064    $scope.resetPayAtPickup = function() {
7065        $scope.StatusInfo.payAtPickup = false;
7066        $scope.StatusInfo.Processor = null;
7067        $scope.updateProcessor();
7068    };
7069
7070    $scope.payWithCardSelected = function() {
7071        $scope.resetPayAtPickup();
7072        $scope.Validate();
7073    };
7074
7075    $scope.billingCountryHasRegions = function () {
7076        try {
7077            if ($scope.BillingInfo.RegionsCount > 0) {
7078                return true;
7079            }
7080        } catch (e) {
7081            // Consume exception, return default of false
7082        }
7083
7084        return false;
7085    };
7086
7087    $scope.disableBillingFields = function() {
7088        return ($scope.ShippingStatus
7089            && $scope.isShippingRequired()
7090            && $scope.BillingInfo
7091            && $scope.BillingInfo.AddressSameAsShipping
7092            && !$scope.isPickup()
7093            && !$scope.digitalOrder
7094        );
7095    };
7096
7097    $scope.requirePayment = function () {
7098        $scope.updateProcessor();
7099        if ($scope.isFree() && $scope.isShippingRequired()) {
7100            return !($scope.ShippingInfo.Option && ($scope.ShippingTotal + $scope.ShippingTaxes) === 0);
7101        }
7102
7103        return $scope.isCardOnFileNeeded() || !($scope.Due === 0 || $scope.StatusInfo.Processor === 'gc' || $scope.isFree() || $scope.StatusInfo.Processor === $scope.PROCESSOR_PAY_AT_PICKUP || $scope.isWholesaleNoCCEnabled());
7104    };
7105
7106    $scope.requireTermsWithShipping = function() {
7107        return ($scope.isTermsRequired() && ($scope.skipBilling() || $scope.isBillingDone())  && $scope.isNewUIFlagSet());
7108    };
7109
7110    $scope.isTermsRequired = function() {
7111        return ($scope.InitInfo && $scope.InitInfo.CheckoutSettings && $scope.InitInfo.CheckoutSettings.TermsEnabled && $scope.isNewUIFlagSet());
7112    };
7113
7114    $scope.TermsShow = function () {
7115        $scope.StatusInfo.TermsProcessing = true;
7116        $('#TermModal').modal('show');
7117        if ($scope.InitInfo.terms) {
7118            $('#TermsAndC').html($scope.InitInfo.terms);
7119            $scope.StatusInfo.TermsProcessing = false;
7120        } else {
7121            $http.get($scope.BuildUrl('/checkout/terms'), {withCredentials: true}).then(function (result) {
7122                $scope.InitInfo.terms = result.data.terms;
7123
7124                $('#TermsAndC').html($scope.InitInfo.terms);
7125                $scope.StatusInfo.TermsProcessing = false;
7126            });
7127        }
7128    };
7129
7130    $scope.ExpirationOptionsGen = function () {
7131        // Years
7132        var startYear = new Date().getFullYear();
7133        var years = [];
7134        for (var y = startYear; y < startYear + 11; y++) {
7135            years.push(y);
7136        }
7137        $scope.ExpYears = years;
7138
7139        // Months
7140        var months = [];
7141        for (var m = 1; m < 13; m++) {
7142            months.push(m);
7143        }
7144        $scope.ExpMonths = months;
7145    };
7146
7147    $scope.TermsValidate = function () {
7148        if (!$scope.InitInfo.CheckoutSettings.TermsEnabled) {
7149            return true;
7150        }
7151    };
7152
7153    /**
7154     * For NMI primarily
7155     * @return {string|null}
7156     * */
7157    $scope.CCValidate = function () {
7158        if ($scope.isPayPal() || $scope.isVantivHosted() || $scope.isRainProcessor() || $scope.isQuiltAdyen() || $scope.isWholesaleNoCCEnabled() || $scope.StatusInfo.Processor === 'gc') {
7159            return null;
7160        }
7161
7162        // Check Card type accepted
7163        if (typeof CheckLuhn !== 'undefined') {
7164            var ccType = GetCardType($scope.CardInfo.CC);
7165            if (ccType != 'UNKNOWN') {
7166                $scope.CardInfo.CCType = GetCardType($scope.CardInfo.CC);
7167
7168                if ($scope.InitInfo.CheckoutSettings) {
7169                    if ($scope.CardInfo.CCType == 'VISA' && !$scope.InitInfo.CheckoutSettings.VisaAccepted) {
7170                        return "We're sorry, but we do not accept Visa credit cards";
7171                    }
7172                    if ($scope.CardInfo.CCType == 'MC' && !$scope.InitInfo.CheckoutSettings.MCAccepted) {
7173                        return "We're sorry, but we do not accept MasterCard credit cards";
7174                    }
7175                    if ($scope.CardInfo.CCType == 'AMEX' && !$scope.InitInfo.CheckoutSettings.AmexAccepted) {
7176                        return "We're sorry, but we do not accept American Express credit cards";
7177                    }
7178                    if ($scope.CardInfo.CCType == 'DISC' && !$scope.InitInfo.CheckoutSettings.DiscAccepted) {
7179                        return "We're sorry, but we do not accept Discover credit cards";
7180                    }
7181                }
7182            }
7183        }
7184
7185        // Check LUHN
7186        if (typeof CheckLuhn !== 'undefined' && CheckLuhn($scope.CardInfo.CC)) {
7187            $scope.CardInfo.CCValid = $scope.CardInfo.CC.replace(/[\s+|.|-]/g, '');
7188            $scope.CardInfo.CCMasked = MaskCardNumber($scope.CardInfo.CC);
7189
7190            return null;
7191        } else {
7192            $scope.CardInfo.CCValid = null;
7193            $scope.CardInfo.CCMasked = null;
7194            $scope.CardInfo.CCType = null;
7195        }
7196        return "The credit card you entered does not appear to be a valid credit card number";
7197    };
7198
7199    /** @return {boolean} */
7200    $scope.PhoneValid = function (phone) {
7201        if (!phone) {
7202            return false;
7203        }
7204
7205        // Simple for now, we may care to do better later
7206        var result = phone.match(/\d/g);
7207        if (!result) {
7208            return false;
7209        }
7210        result = result.length >= 10;
7211        if (result) {
7212            var reg = /([A-Za-z])/;
7213            result = reg.test(phone) != true;
7214        }
7215        return result;
7216    };
7217
7218    $scope.requireContactInfo = function() {
7219        return ($scope.CartStatus.Valid && $scope.requirePayment() && $scope.isPayPal());
7220    };
7221
7222    $scope.isFixCheckoutForClassesFlagEnabled = function () {
7223        return $scope.knobby && $scope.knobby.module_fix_checkout_for_classes;
7224    };
7225
7226    $scope.ContactVerify = function () {
7227        var hits = 0;
7228        var messages = [];
7229
7230        if (($scope.isPayPal() || $scope.isVantivHosted() || $scope.isRainProcessor() || $scope.isQuiltAdyen()) && $scope.Due > 0) {
7231            $scope.ContactStatus.Valid = true;
7232            return;
7233        }
7234
7235        if ($scope.isFixCheckoutForClassesFlagEnabled() && $scope.isGCClassSubscriptionCombinationOnly()) {
7236            $scope.ContactStatus.Valid = true;
7237
7238            return;
7239        }
7240
7241        var emailValid = $scope.isEmailAddressValid($scope.BillingInfo.Email);
7242        var phoneValid = $scope.PhoneValid($scope.BillingInfo.Phone);
7243
7244        if ($scope.isPickup() && emailValid && phoneValid) {
7245            $scope.ContactStatus.Valid = true;
7246            return;
7247        }
7248
7249        emailValid ? hits++ : messages.push("Please enter your email address");
7250        hits = hits << 1;
7251
7252        var result = phoneValid;
7253        result ? hits++ : messages.push("Please enter a valid phone number");
7254
7255        // Show message
7256        $scope.ContactStatus.NotValidMessages = messages;
7257        $scope.ContactStatus.Valid = hits == 3;
7258    };
7259
7260    $scope.isEmailAddressValid = function (emailAddress) {
7261        return !!emailAddress;
7262    };
7263
7264    $scope.changeBillingCountry = function () {
7265        $scope.BillingInfo.State = null;
7266        $scope.BillingInfo.Zip = null;
7267        $scope.BillingVerify();
7268    };
7269
7270    $scope.setBillingCountry = function (country) {
7271        $scope.setCountry($scope.BillingInfo, country);
7272    };
7273
7274    $scope.setBillingOption = function() {
7275        let element = $('#payNowTab');
7276        if ($scope.isPayAtPickup()) {
7277            element = $('#payAtPickupTab');
7278        }
7279        if (element && typeof element.tab !== "undefined") {
7280            element.tab('show');
7281        }
7282    };
7283
7284    $scope.RequireBillingInfo = function() {
7285        return $scope.isWebPayment();
7286    };
7287
7288    $scope.BillingAddressVerify = function () {
7289        // Billing same as shipping
7290        if ($scope.BillingInfo.AddressSameAsShipping && $scope.isShippingRequired() && (!$scope.isPickup() || !$scope.enhancedShippingAndStorePickupOptions)) {
7291            $scope.BillingInfo.FirstName = $scope.ShippingInfo.FirstName;
7292            $scope.BillingInfo.LastName = $scope.ShippingInfo.LastName;
7293            $scope.BillingInfo.Address1 = $scope.ShippingInfo.Address1;
7294            $scope.BillingInfo.Address2 = $scope.ShippingInfo.Address2;
7295            $scope.BillingInfo.City = $scope.ShippingInfo.City;
7296            $scope.BillingInfo.State = $scope.ShippingInfo.State;
7297            $scope.BillingInfo.Zip = $scope.ShippingInfo.Zip;
7298            $scope.BillingInfo.Country = $scope.ShippingInfo.Country;
7299            $scope.BillingInfo.CountryKey = $scope.ShippingInfo.CountryKey;
7300        }
7301    };
7302
7303    $scope.BillingVerify = function () {
7304        if ($scope.isWholesaleNoCCEnabled()) {
7305            if ($scope.isNewUIFlagSet()) {
7306                $scope.CardStatus.Valid = true;
7307            } else {
7308                $scope.BillingStatus.Required = false;
7309            }
7310        }
7311
7312        if (!$scope.BillingStatus.Required || ($scope.getAmountDue() == 0 && !$scope.isCardOnFileNeeded()) || $scope.isPayPal() || $scope.isPayAtPickup()) {
7313            $scope.BillingStatus.Valid = true;
7314            $scope.CardStatus.Valid = true;
7315            if (!$scope.digitalOrder) {
7316                return;
7317            }
7318
7319        }
7320
7321        // Default Country
7322        if (!$scope.BillingInfo.CountryKey) {
7323            $scope.BillingInfo.CountryKey = $scope.ShippingInfo.CountryKey;
7324        }
7325        if ($scope.BillingInfo.CountryKey && $scope.InitInfo.Countries) {
7326            $scope.setBillingCountry($scope.InitInfo.Countries[$scope.BillingInfo.CountryKey]);
7327        }
7328        $scope.BillingAddressVerify();
7329
7330        if ($scope.billingForm) {
7331            $scope.BillingStatus.Valid = $scope.billingForm.$valid;
7332            $scope.SessionStorageSet();
7333        } else {
7334            $scope.BillingStatus.Valid = true;
7335        }
7336
7337        // Vantiv
7338        if ($scope.isVantivHosted()) {
7339            $scope.CardStatus.Valid = true; // we don't collect it here.
7340            return;
7341        }
7342
7343        // Rain
7344        if ($scope.isRainProcessor() || $scope.isQuiltAdyen()) {
7345            $scope.CardStatus.Valid = true; // we will check the token
7346            return;
7347        }
7348
7349        // NMI
7350        // Format expiration date
7351        if ($scope.CardInfo.ExpMonth && $scope.CardInfo.ExpYear) {
7352            $scope.CardInfo.ExpFormated = ("0" + $scope.CardInfo.ExpMonth).slice(-2) + ("0" + $scope.CardInfo.ExpYear).slice(-2);
7353        }
7354
7355        // Card Validation
7356        $scope.CardStatus.Message = $scope.CCValidate();
7357        $scope.CardStatus.Valid = ($scope.CardStatus.Message === null);
7358
7359    };
7360
7361    $scope.BillingEdit = function () {
7362        $scope.BillingStatus.Editing = true;
7363        $scope.BillingStatus.Entered = true;
7364        $scope.ShippingStatus.Editing = false;
7365        $scope.Validate();
7366        $scope.TotalsCalculate();
7367        $scope.setBillingOption();
7368        // IE does not autoscroll to the top
7369        window.scrollTo(0, 0);
7370    };
7371
7372    $scope.BillingDone = function (next) {
7373        if ($scope.billingForm) {
7374            $scope.billingForm.$setSubmitted();
7375            if ($scope.billingForm.$invalid) {
7376                return;
7377            }
7378            $scope.Validate();
7379        }
7380
7381        if ($scope.BillingStatus.Valid && $scope.CardStatus.Valid) {
7382            $scope.BillingStatus.Editing = false;
7383            $scope.BillingStatus.Entered = true;
7384            $scope.TotalsCalculate();
7385            $scope.SessionUpdate();
7386            if (next == 'ShippingEdit') {
7387                $scope.ShippingEdit();
7388            }
7389        } else {
7390            $scope.BillingStatus.MessagesShow = !$scope.BillingStatus.Valid;
7391            $scope.CardStatus.MessagesShow = !$scope.CardStatus.Valid;
7392        }
7393    };
7394
7395    // endregion
7396
7397    // ******************************************************************************************
7398    // region Processing
7399    // ******************************************************************************************
7400
7401    // Sets the default processor
7402    $scope.updateProcessor = function () {
7403        var payWithGiftCard = !$scope.Due && $scope.GCTotal > 0;
7404
7405        if (payWithGiftCard && $scope.isRtoItemExist()) {
7406            payWithGiftCard = false;
7407        }
7408
7409        if ($scope.isWholesaleNoCCEnabled()) {
7410            $scope.StatusInfo.Processor = 'wholesaleNoCC';
7411        } else if ($scope.isPayAtPickup()) {
7412            $scope.StatusInfo.Processor = $scope.PROCESSOR_PAY_AT_PICKUP;
7413        } else if (payWithGiftCard) {
7414            $scope.StatusInfo.Processor = 'gc';
7415        } else if (!$scope.Due && $scope.CartIsFree() && !$scope.isCardOnFileNeeded()) {
7416            $scope.StatusInfo.Processor = $scope.PROCESSOR_FREE;
7417        } else if ($scope.StatusInfo.SelectedProcessor) {
7418            $scope.StatusInfo.Processor = $scope.StatusInfo.SelectedProcessor;
7419        } else if (!$scope.StatusInfo.Processor && $scope.InitInfo.Processors) {
7420            if ($scope.InitInfo.Processors.cc) {
7421                if (!$scope.isCardProcessorVantiv() && !$scope.isCardProcessorRain() && !$scope.isCardProcessorQuiltAdyen()) {
7422                    // This will auto select the processor on the secure page to NMI
7423                    // otherwise, we don't auto select if paypal is an option so that they can pick paypal
7424                    $scope.StatusInfo.Processor = $scope.InitInfo.Processors.cc;
7425                }
7426            } else if ($scope.isPayPalEnabled()) {
7427                $scope.StatusInfo.Processor = $scope.PROCESSOR_PAYPAL;
7428            }
7429        } else if (!$scope.StatusInfo.SelectedProcessor && $scope.StatusInfo.Processor === $scope.PROCESSOR_FREE && $scope.InitInfo.Processors) {
7430            if ($scope.isCardProcessorVantiv()) {
7431                $scope.StatusInfo.Processor = $scope.InitInfo.Processors.cc;
7432            } else if ($scope.isPayPalEnabled()) {
7433                $scope.StatusInfo.Processor = $scope.PROCESSOR_PAYPAL;
7434            }
7435        }
7436    };
7437
7438    $scope.setProcessor = function (processor) {
7439        if (processor) {
7440            $scope.StatusInfo.SelectedProcessor = processor;
7441            $scope.StatusInfo.Processor = processor;
7442            switch (processor) {
7443                case $scope.PROCESSOR_VANTIV:
7444                case $scope.PROCESSOR_PAYPAL:
7445                    $scope.ShippingEdit();
7446                    // Disable the summary
7447                    $scope.BillingStatus.Entered = false;
7448                    $scope.ShippingStatus.Entered = false;
7449                    break;
7450                default:
7451                    $scope.ShippingVerify();
7452                    break;
7453            }
7454        } else {
7455            $scope.updateProcessor();
7456        }
7457    };
7458
7459    $scope.toggleSubscribeToMarketingEmails = function () {
7460        $scope.subscribeToMarketingEmails = !$scope.subscribeToMarketingEmails;
7461    };
7462
7463    $scope.CheckoutProcess = function (checkoutOptions) {
7464        checkoutOptions = checkoutOptions || {};
7465        console.log('checkout process');
7466
7467        var bnplMethods = ['klarna', 'affirm', 'afterpay'];
7468
7469        var bnplMethodIndex = bnplMethods.findIndex(function (m) {
7470            return $scope.paymentMethod === m;
7471        });
7472
7473        if (bnplMethodIndex !== -1) {
7474            return $scope.onBNPL($scope.paymentMethod);
7475        }
7476
7477        if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure() && checkoutOptions.taxesVerified) {
7478            if ($scope.HasDynamicTaxes && $scope.isDynamicTaxServiceEnabled()) {
7479                $scope.TotalsCalculate();
7480                $scope.CartVerify();
7481                $scope.BillingVerify();
7482            }
7483        }
7484
7485        if (!($scope.checkFlagBlockCheckoutOnDynamicTaxFailure() && checkoutOptions.taxesVerified)) {
7486            if ($scope.forms.contact) {
7487                $scope.$broadcast('form-check-validity');
7488                if ($scope.forms.contact.$invalid) {
7489                    return Promise.resolve();
7490                }
7491            }
7492
7493            if ($scope.contactForm && $scope.showContactInfo()) {
7494                $scope.contactForm.$setSubmitted();
7495                if ($scope.contactForm.$invalid) {
7496                    return Promise.resolve();
7497                }
7498            }
7499
7500            if ($scope.shippingForm && $scope.isShippingRequired() && !$scope.isPickup()) {
7501                $scope.ShippingStatus.Submitted = true;
7502                $scope.shippingForm.$setSubmitted();
7503                if ($scope.shippingForm.$invalid) {
7504                    return Promise.resolve();
7505                }
7506            }
7507
7508            if ($scope.isVantivHosted() || $scope.isRainProcessor() || $scope.isQuiltAdyen()) {
7509                $scope.BillingAddressVerify();
7510            }
7511
7512            if ($scope.billingForm && $scope.isBillingRequired() && !$scope.isPayAtPickup() && !$scope.isPayPal()) {
7513                $scope.billingForm.$setSubmitted();
7514                if ($scope.billingForm.$invalid) {
7515                    return Promise.resolve();
7516                }
7517            } else if ($scope.billingForm && $scope.isTermsRequired()) {
7518                if (!$scope.BillingInfo.TermsAgree) {
7519                    $scope.billingForm.$setSubmitted();
7520                    return Promise.resolve();
7521                }
7522            }
7523
7524            if ($scope.billingForm && $scope.isBillingRequired() && $scope.digitalOrder) {
7525                $scope.billingForm.$setSubmitted();
7526                if ($scope.billingForm.$invalid) {
7527                    return Promise.resolve();
7528                }
7529            }
7530
7531            // Check Captcha if needed and validate it first
7532            if ($scope.showCaptcha) {
7533                return $scope.checkCaptcha().then(function() {
7534                    if (!$scope.showCaptcha) {
7535                        return $scope.CheckoutProcess(checkoutOptions);
7536                    }
7537                });
7538            }
7539
7540            $scope.Validate();
7541        }
7542
7543        var canPayViaCard = $scope.isCartValid() && $scope.ShippingStatus.Valid && $scope.BillingStatus.Valid && $scope.isContacStatusValid() && $scope.CardStatus.Valid;
7544        var isDigitalOrderAndWeCanPayForItViaPayPal = false;
7545        var isFreeDigitalOrder = false;
7546        var isDigitalOrderFullyPaidByGiftCard = false;
7547        if ($scope.digitalOrder && $scope.BillingStatus.Valid && $scope.isPayPal()) {
7548            isDigitalOrderAndWeCanPayForItViaPayPal = true;
7549        }
7550        if ($scope.digitalOrder && $scope.isFree()) {
7551            isFreeDigitalOrder = true;
7552        }
7553
7554        if ($scope.digitalOrder && $scope.isFullyPaidByGiftCard()) {
7555            isDigitalOrderFullyPaidByGiftCard = true;
7556        }
7557
7558        if (!canPayViaCard && !isDigitalOrderAndWeCanPayForItViaPayPal && !isFreeDigitalOrder && !isDigitalOrderFullyPaidByGiftCard) {
7559            $scope.ShippingStatus.MessagesShow = true;
7560            $scope.ContactStatus.MessagesShow = true;
7561            $scope.BillingStatus.MessagesShow = true;
7562            $scope.CardStatus.MessagesShow = true;
7563
7564            return Promise.resolve();
7565        }
7566
7567        var processCheckoutPayment = function () {
7568            console.log('Processing');
7569            $scope.makeTaxInclusive();
7570
7571            if ($scope.paymentMethod === 'paypal') {
7572                $scope.setProcessor('paypal');
7573            }
7574
7575            const isRainProcessing = $scope.isCardProcessorRain() && $scope.isRainProcessor() && $scope.Rain.card;
7576            const isQuiltAdyenProcessing = $scope.isCardProcessorQuiltAdyen() && $scope.isQuiltAdyen() && $scope.paymentMethod === 'cc';
7577
7578            if (isRainProcessing) {
7579                // Rain Processing Validation
7580                $scope.Rain.processorToken = null;
7581                $scope.clearErrorMessage();
7582
7583                if (!$scope.isMinimumAmountMet()) {
7584                    $scope.setProcessingCheckout(false);
7585                    return Promise.resolve();
7586                }
7587
7588                if ($scope.Rain.error) {
7589                    $scope.setProcessingCheckout(false);
7590                    $scope.StatusInfo.ErrorMessageBottom = $scope.Rain.error.message;
7591                    return Promise.resolve();
7592                }
7593
7594                if (!$scope.isRainPaymentValid()) {
7595                    $scope.setProcessingCheckout(false);
7596                    $scope.StatusInfo.ErrorMessageBottom = "Please complete the credit information before placing your order.";
7597                    return Promise.resolve();
7598                }
7599
7600                if ($scope.paymentMethod !== 'cc') {
7601                    return Promise.reject('Unknown Payment type');
7602                }
7603            }
7604
7605            if (isQuiltAdyenProcessing) {
7606                // Quilt Processing Validation
7607                $scope.clearErrorMessage();
7608
7609                if (!$scope.isMinimumAmountMet()) {
7610                    $scope.setProcessingCheckout(false);
7611                    return Promise.resolve();
7612                }
7613
7614                if (!$scope.isAdyenPaymentValid()) {
7615                    $scope.setProcessingCheckout(false);
7616                    $scope.StatusInfo.ErrorMessageBottom = $scope.getAdyenError();
7617                    return Promise.resolve();
7618                }
7619            }
7620
7621            const submitInfoGetData = $scope.getSubmitInfoGetData();
7622            const authPaymentData = isQuiltAdyenProcessing ? $scope.getAuthPaymentData() : undefined;
7623            const processRainPaymentData = isRainProcessing ? $scope.getProcessRainPaymentData() : undefined;
7624            const postProcessData = $scope.getPostProcessData();
7625
7626            if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
7627                if ((!$scope.isCardOnFileNeeded() && !$scope.getAmountDue()) || $scope.isWholesaleNoCCEnabled() || $scope.isPayAtPickup()) {
7628                    return $scope.PreProcess($scope.buildCheckoutPreProcessData()).then(function () {
7629                        if ($scope.TransactionId) {
7630                            return $scope.PostProcess(postProcessData);
7631                        } else if (!$scope.StatusInfo.ErrorMessage) {
7632                            $scope.StatusInfo.ErrorMessageBottom = "An unknown error occurred, please try again";
7633                            $scope.setProcessingCheckout(false);
7634
7635                            return Promise.resolve();
7636                        }
7637                    });
7638                }
7639
7640                if (isRainProcessing) {
7641                    return $scope.PreProcess($scope.buildCheckoutPreProcessData()).then(function(id) {
7642                        if (!id) {
7643                            return Promise.resolve();
7644                        }
7645
7646                        return $scope.SubmitInfoGet(submitInfoGetData).then(function() {
7647                            if ($scope.Rain.paymentAlreadyPaid) {
7648                                return $scope.PostProcess(postProcessData);
7649                            } else if ($scope.Rain.processorToken && $scope.TransactionId) {
7650                                return $scope.processRainPayment(processRainPaymentData).then(function(result) {
7651                                    $scope.setProcessingCheckout(false);
7652
7653                                    if (result !== 'success') {
7654                                        return $scope.NoOpPromise();
7655                                    }
7656
7657                                    return $scope.PostProcess(postProcessData);
7658                                }).catch(function (error) {
7659                                    console.log(error);
7660                                    $scope.setErrorMessageBottom('An unhandled error has occurred. Please try again. If the error continues, please contact the store');
7661                                    $scope.setProcessingCheckout(false);
7662                                    return $scope.NoOpPromise();
7663                                });
7664                            }
7665
7666                            $scope.setProcessingCheckout(false);
7667
7668                            return Promise.resolve();
7669                        });
7670                    });
7671                }
7672
7673                if (isQuiltAdyenProcessing) {
7674                    return $scope.PreProcess($scope.buildCheckoutPreProcessData()).then(function(id) {
7675                        if (!id) {
7676                            return Promise.resolve();
7677                        }
7678                        return $scope.SubmitInfoGet(submitInfoGetData).then(function() {
7679                            return $scope.authPayment(authPaymentData).then(function() {
7680                                if ($scope.Adyen.authData || $scope.Adyen.paymentAlreadyPaid) {
7681                                    return $scope.PostProcess(postProcessData);
7682                                }
7683                                $scope.setProcessingCheckout(false);
7684                            });
7685                        });
7686                    });
7687                }
7688
7689                if ($scope.StatusInfo.Processor === 'gc') {
7690                    return $scope.PreProcess($scope.buildCheckoutPreProcessData()).then(function () {
7691                        if ($scope.TransactionId) {
7692                            return $scope.PostProcess(postProcessData);
7693                        } else if (!$scope.StatusInfo.ErrorMessage) {
7694                            $scope.StatusInfo.ErrorMessageBottom = "An unknown error occurred, please try again";
7695                            $scope.setProcessingCheckout(false);
7696
7697                            return Promise.resolve();
7698                        }
7699                    });
7700                }
7701
7702                return $scope.SubmitInfoGet(submitInfoGetData).then(function () {
7703                    if (!$scope.SubmitInfo) {
7704                        return Promise.resolve();
7705                    }
7706
7707                    return $scope.PreProcess($scope.buildCheckoutPreProcessData()).then(function () {
7708                        if ($scope.TransactionId) {
7709                            $scope.ProcessorSubmit();
7710                        }
7711                    });
7712                });
7713            }
7714
7715            const preProcessData = $scope.getPreProcessData();
7716
7717            if ((!$scope.isCardOnFileNeeded() && !$scope.getAmountDue()) || $scope.isWholesaleNoCCEnabled() || $scope.isPayAtPickup()) {
7718                return $scope.PreProcess(preProcessData).then(function () {
7719                    if ($scope.TransactionId) {
7720                        return $scope.PostProcess(postProcessData);
7721                    } else if (!$scope.StatusInfo.ErrorMessage) {
7722                        $scope.StatusInfo.ErrorMessageBottom = "An unknown error occurred, please try again";
7723                        $scope.setProcessingCheckout(false);
7724
7725                        return Promise.resolve();
7726                    }
7727                });
7728            }
7729
7730            // Rain Processing
7731            if (isRainProcessing) {
7732                return $scope.PreProcess(preProcessData).then(function(id) {
7733                    if (!id) {
7734                        return Promise.resolve();
7735                    }
7736
7737                    return $scope.SubmitInfoGet(submitInfoGetData).then(function() {
7738                        // NOTE: Payment Server will not return a new intent once we've requested one - an intent is associated with a cart ID
7739                        if ($scope.Rain.paymentAlreadyPaid) {
7740                            return $scope.PostProcess(postProcessData);
7741                        } else if ($scope.Rain.processorToken && $scope.TransactionId) {
7742                            return $scope.processRainPayment(processRainPaymentData).then(function(result) {
7743                                $scope.setProcessingCheckout(false);
7744
7745                                if (result !== 'success') {
7746                                    return $scope.NoOpPromise();
7747                                }
7748
7749                                return $scope.PostProcess(postProcessData);
7750                            }).catch(function (error) {
7751                                console.log(error);
7752                                $scope.setErrorMessageBottom('An unhandled error has occurred. Please try again. If the error continues, please contact the store');
7753                                $scope.setProcessingCheckout(false);
7754                                return $scope.NoOpPromise();
7755                            });
7756                        }
7757
7758                        $scope.setProcessingCheckout(false);
7759
7760                        return Promise.resolve();
7761                    });
7762                });
7763            }
7764
7765            // Quilt Processing
7766            if (isQuiltAdyenProcessing) {
7767                return $scope.PreProcess(preProcessData).then(function(id) {
7768                    if (!id) {
7769                        return Promise.resolve();
7770                    }
7771                    return $scope.SubmitInfoGet(submitInfoGetData).then(function() {
7772                        return $scope.authPayment(authPaymentData).then(function() {
7773                            if ($scope.Adyen.authData || $scope.Adyen.paymentAlreadyPaid) {
7774                                return $scope.PostProcess(postProcessData);
7775                            }
7776                            $scope.setProcessingCheckout(false);
7777                        });
7778                    });
7779                });
7780            }
7781
7782            if ($scope.StatusInfo.Processor === 'gc') {
7783                return $scope.PreProcess(preProcessData).then(function () {
7784                    if ($scope.TransactionId) {
7785                        return $scope.PostProcess(postProcessData);
7786                    } else if (!$scope.StatusInfo.ErrorMessage) {
7787                        $scope.StatusInfo.ErrorMessageBottom = "An unknown error occurred, please try again";
7788                        $scope.setProcessingCheckout(false);
7789
7790                        return Promise.resolve();
7791                    }
7792                });
7793            }
7794
7795            return $scope.SubmitInfoGet(submitInfoGetData).then(function () {
7796                if (!$scope.SubmitInfo) {
7797                    return Promise.resolve();
7798                }
7799
7800                return $scope.PreProcess(preProcessData).then(function () {
7801                    if ($scope.TransactionId) {
7802                        $scope.ProcessorSubmit();
7803                    }
7804                });
7805            });
7806        };
7807
7808        if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
7809            if (!checkoutOptions.taxesVerified) {
7810                var freshTaxesPromise = $scope.ensureFreshDynamicTaxesForCheckout();
7811
7812                if (freshTaxesPromise) {
7813                    $scope.setProcessingCheckout(true);
7814
7815                    return freshTaxesPromise.then(function () {
7816                        if ($scope.hasActiveDynamicTaxFailure()) {
7817                            $scope.setProcessingCheckout(false);
7818                            $scope.safeApply();
7819
7820                            return Promise.resolve();
7821                        }
7822
7823                        checkoutOptions.taxesVerified = true;
7824
7825                        if ($scope.HasDynamicTaxes && $scope.isDynamicTaxServiceEnabled()) {
7826                            $scope.TotalsCalculate();
7827                            $scope.CartVerify();
7828                            $scope.BillingVerify();
7829                        }
7830
7831                        return processCheckoutPayment();
7832                    });
7833                }
7834            }
7835
7836            if ($scope.hasActiveDynamicTaxFailure()) {
7837                return Promise.resolve();
7838            }
7839
7840            $scope.setProcessingCheckout(true);
7841        }
7842
7843        if (!$scope.checkFlagBlockCheckoutOnDynamicTaxFailure()) {
7844            $scope.setProcessingCheckout(true);
7845
7846            if (!$scope.dynamicTaxesReady) {
7847                $scope.setErrorMessageBottom(null);
7848
7849                return $scope.updateDynamicTaxesOnce().then(function () {
7850                    if (!$scope.dynamicTaxesReady) {
7851                        $scope.setProcessingCheckout(false);
7852                        $scope.safeApply();
7853
7854                        return Promise.reject('Tax system is not responding.');
7855                    }
7856
7857                    return $scope.CheckoutProcess();
7858                }).catch(function (error) {
7859                    var phone = $scope.InitInfo.webInventoryLocationPhone || $scope.InitInfo.Store.phone;
7860                    var suggestion = 'Please try your order again later or give us a call at ' + phone + ' to order by phone.';
7861
7862                    $scope.setProcessingCheckout(false);
7863                    $scope.setErrorMessageBottom(error.message + ' ' + suggestion);
7864
7865                    $scope.safeApply();
7866
7867                    return Promise.reject(error);
7868                });
7869            }
7870        }
7871
7872        return processCheckoutPayment();
7873    };
7874
7875    $scope.makeTaxInclusive = function () {
7876        if ($scope.showAsGST()) {
7877            if ($scope.isShippingCountryGST()) {
7878                $scope.Taxes = $scope.GST_Taxes;
7879                $scope.Due = $scope.GST_Due;
7880
7881                if ($scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length) {
7882                    for (var i = 0; i < $scope.CartData.Items.length; i++) {
7883                        var item = $scope.CartData.Items[i];
7884                        $scope.CartData.Items[i].tax = fixFloat(item.gst_tax) * fixFloat(item.qty);
7885                    }
7886                }
7887            } else {
7888                $scope.Taxes = 0;
7889                $scope.ShippingTaxes = 0;
7890                $scope.taxable_sales = 0;
7891                if ($scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length) {
7892                    for (var i = 0; i < $scope.CartData.Items.length; i++) {
7893                        var item = $scope.CartData.Items[i];
7894                        $scope.CartData.Items[i].tax = 0;
7895                        $scope.CartData.Items[i].taxable = 0;
7896                        $scope.CartData.Items[i].total = $scope.CartData.Items[i].discounted_total = (fixFloat(item.price) * fixFloat(item.qty)) - fixFloat(item.discount);
7897                    }
7898                }
7899            }
7900        }
7901    };
7902
7903    $scope.getPreProcessData = function () {
7904        var shippingDiscount = $scope.getShippingDiscount();
7905        var shippingInfo = $scope.getEffectiveShippingInfo();
7906
7907        // Adapter
7908        if ($scope.isScheduledPayment()) {
7909            var data = {
7910                type: 'scheduled_payment',
7911                spid: $scope.scheduledPaymentId,
7912                token: $scope.paymentToken,
7913                payment_info: $scope.InitInfo.payment_info,
7914            };
7915        } else if ($scope.isSubscriptionPayment()) {
7916            var data = {
7917                type: 'subscription_payment',
7918                sid: $scope.subscriptionId,
7919                token: $scope.paymentToken,
7920                payment_info: $scope.InitInfo.payment_info,
7921                subscription_guid: $scope.paymentNonce,
7922            };
7923        } else if ($scope.isInvoicePayment()) {
7924            var data = {
7925                type: 'invoice_payment',
7926                iid: $scope.invoiceId,
7927                token: $scope.paymentToken,
7928                payment_info: $scope.InitInfo.payment_info,
7929            };
7930        } else if ($scope.isInvoicesPayment()) {
7931            var data = {
7932                type: 'invoices_payment',
7933                icid: $scope.invoicesCartId,
7934                token: $scope.paymentToken,
7935                payment_info: $scope.InitInfo.payment_info,
7936            };
7937        } else if ($scope.isRtoPayment()) {
7938            var data = {
7939                type: 'rto_payment',
7940                rpid: $scope.rtoCartId,
7941                token: $scope.paymentToken,
7942                payment_info: $scope.InitInfo.payment_info,
7943            };
7944        } else if ($scope.isAddCardOnly()) {
7945            var data = {
7946                type: 'add_card_only',
7947                cid: $scope.customerId,
7948                token: $scope.paymentToken,
7949            };
7950        } else if ($scope.isMmsPayment()) {
7951            var data = {
7952                type: 'mms_payment',
7953                cid: $scope.customerId,
7954                token: $scope.paymentToken,
7955                payment_info: $scope.InitInfo.payment_info,
7956            };
7957        } else {
7958            var data = {
7959                sub_total: $scope.SubTotal,
7960                discount_amount: $scope.Discounts,
7961                shipping_amount: ($scope.ShippingInfo.Option ? $scope.ShippingInfo.Option.price : 0),
7962                shipping_discount: shippingDiscount,
7963                shipping_tax_amount: $scope.ShippingTaxes,
7964                tax_amount: $scope.Taxes,
7965                tax_jurisdictions: $scope.JurisdictionalTax,
7966                shipping_tax_jurisdictions: JSON.stringify($scope.shipping_tax_jurisdictions),
7967                taxable_sales: $scope.taxable_sales,
7968                non_taxable_sales: $scope.non_taxable_sales,
7969                non_sales: $scope.non_sales,
7970                cart_total: $scope.CartTotal,
7971                gc_amount: $scope.GCTotal,
7972                cc_amount: $scope.getAmountDue(),
7973                billing_same_as_shipping: $scope.BillingInfo.AddressSameAsShipping,
7974                billing_first_name: $scope.BillingInfo.FirstName,
7975                billing_last_name: $scope.BillingInfo.LastName,
7976                billing_address1: $scope.BillingInfo.Address1,
7977                billing_address2: $scope.BillingInfo.Address2,
7978                billing_city: $scope.BillingInfo.City,
7979                billing_state: $scope.BillingInfo.State,
7980                billing_postal: $scope.BillingInfo.Zip,
7981                billing_country: $scope.BillingInfo.Country,
7982                billing_email: $scope.BillingInfo.Email,
7983                billing_phone: $scope.BillingInfo.Phone,
7984                shipping_first_name: shippingInfo.FirstName,
7985                shipping_last_name: shippingInfo.LastName,
7986                shipping_company: shippingInfo.company_name,
7987                shipping_address1: shippingInfo.Address1,
7988                shipping_address2: shippingInfo.Address2,
7989                shipping_city: shippingInfo.City,
7990                shipping_state: shippingInfo.State,
7991                shipping_postal: shippingInfo.Zip,
7992                shipping_country: shippingInfo.Country,
7993                shipping_id: $scope.ShippingInfo.Option ? $scope.ShippingInfo.Option.id : 0,
7994                shipping_service_id: $scope.ShippingInfo.Option ? $scope.ShippingInfo.Option.service_id : 0,
7995                shipping_package_id: $scope.ShippingInfo.Option ? $scope.ShippingInfo.Option.package_id : 0,
7996                shipping_option_name: $scope.ShippingInfo.Option ? $scope.ShippingInfo.Option.name : '',
7997                processor: ($scope.isCardOnFileNeeded() || $scope.Due) ? $scope.StatusInfo.Processor : 'nocc',
7998                gift_message: $scope.GiftMessage.Message,
7999                gift_enabled: $scope.GiftMessage.Checked,
8000                order_notes: $scope.orderNotes.note,
8001                join_email_list: ($scope.ContactInfo.JoinEmailList == true),
8002                terms_agree: $scope.BillingInfo.TermsAgree,
8003                items: $scope.CartData.Items,
8004                coupons: $scope.CartData.Coupons,
8005                gift_cards: $scope.isPayAtPickup() ? [] : $scope.CartData.GiftCards,
8006                send_text_notifications: $scope.CheckoutInfo.sendTextNotifications,
8007                subscribeToNewsletter: $scope.subscribeToNewsletter,
8008                subscribeToMarketingEmails: $scope.subscribeToMarketingEmails,
8009            };
8010
8011            if ($scope.ShippingInfo?.Option?.name === 'School Delivery') {
8012                var osrItem = $scope.getOsrItem();
8013
8014                if (osrItem && osrItem.osrData?.address) {
8015                    data.shipping_address1 = osrItem.osrData.address.street1 || null;
8016                    data.shipping_address2 = osrItem.osrData.address.street2 || null;
8017                    data.shipping_city = osrItem.osrData.address.city || null;
8018                    data.shipping_state = osrItem.osrData.address.state || null;
8019                    data.shipping_postal = osrItem.osrData.address.postalCode || null;
8020                    data.shipping_country = $scope.InitInfo.Countries?.[osrItem.osrData.address.country]
8021                        || osrItem.osrData.address.country
8022                        || null;
8023                }
8024            }
8025        }
8026
8027        if ($scope.isFlagInsuranceEnabled()) {
8028            data.isTransactionEligibleForJmInsurance = $scope.isProductInCart();
8029            data.contactJMInsuranceInfo = $scope.contactJMInsuranceInfo;
8030            data.jMInsuranceChecked = $scope.isInsuranceSwitchedOn || false;
8031        }
8032
8033        if ($scope.isFlagLuxsuranceEnabled()) {
8034            data.contactJMInsuranceInfo = $scope.contactJMInsuranceInfo;
8035            data.isLuxsuranceSwitchedOn = $scope.isLuxsuranceSwitchedOn || false;
8036        }
8037
8038        if ($scope.isFree()) {
8039            data.processor = $scope.PROCESSOR_FREE;
8040        }
8041
8042        if ($scope.knobby && $scope.knobby.module_tax_profiles) {
8043            data.pos_tax_profile_id = $scope.getCurrentTaxProfileId();
8044        }
8045
8046        if ($scope.checkFlagRetailDeliveryFee()) {
8047            data.retail_delivery_fee = $scope.retailDeliveryFee;
8048            data.retail_delivery_fee_avalara_tax_jurisdiction = $scope.retailDeliveryFeeTaxJurisdiction;
8049        }
8050
8051        return data;
8052    };
8053
8054    $scope.PreProcess = function (data) {
8055        if (typeof data === 'undefined') {
8056            data = $scope.getPreProcessData();
8057        }
8058
8059        var noCart = false;
8060        if ($scope.isInvoicePayment()) {
8061            noCart = true;
8062        }
8063
8064        return $http.post($scope.BuildUrl('/checkout/preprocess', noCart), data, {withCredentials: true}).then(function (result) {
8065            if (result.data.error) {
8066                $scope.CartDataGet().then(function () {
8067                    $scope.setProcessingCheckout(false);
8068                    $scope.ShippingOptionsGet();
8069                });
8070                if (result.data.error_type && result.data.error_type.indexOf('coupons_consumed') > -1) {
8071                    if ($scope.isVantivHosted() || $scope.isRainProcessor() || $scope.isQuiltAdyen()) {
8072                        $scope.giftCardCouponMsg = result.data.error;
8073                    } else {
8074                        alert(result.data.error);
8075                    }
8076                    angular.forEach(result.data.coupons_id_consumed, function (id){
8077                        $scope.removeCoupon(id);
8078                    });
8079                } else {
8080                    $scope.StatusInfo.ErrorMessage = result.data.error;
8081                }
8082
8083                if (result.data.error_type && result.data.error_type.indexOf('coupons_expired') > -1) {
8084                    $scope.giftCardCouponMsg = result.data.error;
8085                    angular.forEach(result.data.coupons_id_expired, function(id) {
8086                        $scope.removeCoupon(id);
8087                    });
8088                }
8089
8090                if (result.data.error_type && result.data.error_type.indexOf('coupons_deleted') > -1) {
8091                    $scope.giftCardCouponMsg = result.data.error;
8092                    angular.forEach(result.data.coupons_id_deleted, function(id) {
8093                        $scope.removeCoupon(id);
8094                    });
8095                }
8096
8097                if (result.data.error_type && result.data.error_type == 'inventory') {
8098                    $scope.InventoryMessages = result.data.message_array;
8099                    $scope.StatusInfo.ErrorMessage = null; // This error is handled/displayed separately
8100                }
8101
8102                if (result?.data?.error_type === 'store_available') {
8103                    $scope.AvailabilityMessages = result.data.message_array;
8104                    $scope.StatusInfo.ErrorMessage = null;
8105                }
8106
8107                if ($scope.checkFlagBlockCheckoutOnDynamicTaxFailure() && result?.data?.error_type === 'dynamic_tax_error') {
8108                    $scope.StatusInfo.ErrorMessage = null;
8109                    $scope.StatusInfo.DynamicTaxError = result.data.error;
8110                    $scope.dynamicTaxesReady = false;
8111                }
8112
8113                return 0;
8114            } else {
8115                $scope.TransactionId = result.data.transaction_id;
8116                return result.data.transaction_id;
8117            }
8118        });
8119    };
8120
8121    $scope.getPostProcessData = function () {
8122        var data = {
8123            processor: 'nocc'
8124        };
8125
8126        if ($scope.isWholesaleNoCCEnabled()) {
8127            data.processor = 'wholesaleNoCC';
8128        } else if ($scope.isFree()) {
8129            data.processor = $scope.PROCESSOR_FREE;
8130        } else if ($scope.isPayAtPickup()) {
8131            data.processor = $scope.PROCESSOR_PAY_AT_PICKUP;
8132        } else if ($scope.isRainProcessor()) {
8133            data.processor = $scope.PROCESSOR_RAIN;
8134            data.amount = $scope.getAmountDue();
8135        } else if ($scope.isQuiltAdyen()) {
8136            data.processor = $scope.PROCESSOR_QUILT_ADYEN;
8137            data.amount = $scope.getAmountDue();
8138        }
8139
8140        data.subscribeToMarketingEmails = $scope.subscribeToMarketingEmails;
8141
8142        if ($scope.checkFlagRetailDeliveryFee() && $scope.retailDeliveryFee > 0) {
8143            data.retail_delivery_fee = $scope.retailDeliveryFee;
8144        }
8145
8146        if ($scope.isScheduledPayment()) {
8147            data.type = 'scheduled_payment';
8148            data.spid = $scope.scheduledPaymentId;
8149            data.token = $scope.paymentToken;
8150            data.enroll_autopay = $scope.isEnrollAutoPay() ? 1 : 0;
8151            data.payment_info = $scope.InitInfo.payment_info;
8152        } else if ($scope.isSubscriptionPayment()) {
8153            data.type = 'subscription_payment';
8154            data.sid = $scope.subscriptionId;
8155            data.token = $scope.paymentToken;
8156            data.enroll_autopay = $scope.isEnrollAutoPay() ? 1 : 0;
8157            data.payment_info = $scope.InitInfo.payment_info;
8158        } else if ($scope.isInvoicePayment()) {
8159            data.type = 'invoice_payment';
8160            data.iid = $scope.invoiceId;
8161            data.token = $scope.paymentToken;
8162            data.enroll_autopay = 0;
8163            data.add_card_on_file = $scope.BillingInfo.addCardOnFile ? 1 : 0;
8164            if (window.knobby && window.knobby['module_aim_add_card_on_file_for_invoice_payments']) {
8165                data.billing_email = $scope.BillingInfo.Email;
8166                data.billing_postal = $scope.BillingInfo.Zip;
8167            }
8168            data.payment_info = $scope.InitInfo.payment_info;
8169        } else if ($scope.isInvoicesPayment()) {
8170            data.type = 'invoices_payment';
8171            data.icid = $scope.invoicesCartId;
8172            data.token = $scope.paymentToken;
8173            data.enroll_autopay = 0;
8174            data.add_card_on_file = $scope.BillingInfo.addCardOnFile ? 1 : 0;
8175            if (window.knobby && window.knobby['module_aim_add_card_on_file_for_invoice_payments']) {
8176                data.billing_email = $scope.BillingInfo.Email;
8177                data.billing_postal = $scope.BillingInfo.Zip;
8178            }
8179            data.payment_info = $scope.InitInfo.payment_info;
8180        } else if ($scope.isRtoPayment()) {
8181            data.type = 'rto_payment';
8182            data.rpid = $scope.rtoCartId;
8183            data.token = $scope.paymentToken;
8184            data.payment_info = $scope.InitInfo.payment_info;
8185        } else if ($scope.isAddCardOnly()) {
8186            data.type = 'add_card_only';
8187            data.cid = $scope.customerId;
8188            data.token = $scope.paymentToken;
8189        } else if ($scope.isMmsPayment()) {
8190            data.type = 'mms_payment';
8191            data.cid = $scope.customerId;
8192            data.token = $scope.paymentToken;
8193            data.enroll_autopay = 0;
8194            data.payment_info = $scope.InitInfo.payment_info;
8195        }
8196
8197        if ($scope.checkFlagStripeSendLevel3Data() || $scope.checkFlagAdyenSendLevel3Data()) {
8198            data.level_3_transaction_id = $scope.TransactionId || null;
8199            data.level_3_cart_items = $scope.CartData.Items || [];
8200        }
8201
8202        return data;
8203    };
8204
8205    $scope.PostProcess = function (data) {
8206        if (typeof data === 'undefined') {
8207            data = $scope.getPostProcessData();
8208        }
8209
8210        if (typeof data === 'object') {
8211            const dynamicData = {};
8212
8213            if (data.processor === $scope.PROCESSOR_RAIN) {
8214                dynamicData.gateway_transaction_id = $scope.Rain.intentId;
8215                dynamicData.payment_method_id = $scope.Rain.PaymentMethodId;
8216            } else if (data.processor === $scope.PROCESSOR_QUILT_ADYEN) {
8217                dynamicData.gateway_transaction_id = $scope.Adyen.authData.payment_id;
8218                // card on file
8219                if ($scope.Adyen.authData.payment_method_id) {
8220                    dynamicData.payment_method_id = $scope.Adyen.authData.payment_method_id;
8221                }
8222            } else if (data.processor === $scope.PROCESSOR_FREE) {
8223                dynamicData.gateway_transaction_id = $scope.PROCESSOR_FREE;
8224            }
8225
8226            if (data.processor === $scope.PROCESSOR_QUILT_ADYEN && ['add_card_only', 'mms_payment'].includes(data.type)) {
8227                // Adyen can update customerId on SubmitInfo step
8228                dynamicData.cid = $scope.customerId;
8229            }
8230
8231            data = Object.assign({}, data, dynamicData);
8232        }
8233
8234        var noCart = false;
8235        if ($scope.isInvoicePayment()) {
8236            noCart = true;
8237        }
8238
8239        if ($scope.isSavedCardsFlagEnabled() && $scope.CardInfo.nickname) {
8240            data.nickname = $scope.CardInfo.nickname;
8241        }
8242
8243        $scope.setProcessing(true);
8244
8245        return $http.post($scope.BuildUrl('/checkout/postprocess', noCart), data, {withCredentials: true}).then(function (result) {
8246            if (result.data.error) {
8247                $scope.StatusInfo.ErrorMessage = result.data.error;
8248                $scope.setProcessing(false);
8249            } else if (result.data.redirect_url) {
8250                $window.location.href = result.data.redirect_url;
8251            }
8252        });
8253    };
8254
8255    $scope.GoToCheckoutUrl = function () {
8256        var wsurl = $scope.WsUrl.replace("https://", "").replace("http://", "");
8257        var goto = $scope.InitInfo.CheckoutUrl + "?id=" + $scope.CartId + "&ws=" + wsurl;
8258        if ($scope.showAsGST()) {
8259            $scope.Taxes = 0;
8260            $scope.ShippingTaxes = 0;
8261        }
8262
8263        $scope.trackEvent();
8264        $window.location.href = goto;
8265    };
8266
8267    $scope.ProcessItems = function () {
8268
8269        if ($scope.showAsGST() && !$scope.isShippingCountryGST()) {
8270
8271            $scope.SubTotal += ($scope.Taxes + $scope.ShippingTaxes);
8272            $scope.Taxes = 0;
8273            $scope.ShippingTaxes = 0;
8274
8275            for (var i = 0; i < $scope.CartData.Items.length; i++) {
8276
8277                $scope.CartData.Items[i].price = $scope.getItemGstPrice($scope.CartData.Items[i]);
8278            }
8279        }
8280    };
8281
8282    $scope.getSubmitInfoGetData = function (paymentMethod, paymentMethodTypes) {
8283        if (typeof paymentMethod === 'undefined') {
8284            paymentMethod = null;
8285        }
8286        if (typeof paymentMethodTypes === 'undefined') {
8287            paymentMethodTypes = null;
8288        }
8289
8290        var data = null;
8291        var shippingInfo = $scope.getEffectiveShippingInfo();
8292
8293        if ($scope.StatusInfo.Processor == 'NMI3S') {
8294            data = {
8295                processor: $scope.StatusInfo.Processor,
8296                cart_amount: $scope.getAmountDue(),
8297                shipping_amount: $scope.getShippingWithDiscount(),
8298                tax_amount: $scope.Taxes,
8299                shipping_tax_amount: $scope.ShippingTaxes,
8300                'shipping-first-name': shippingInfo.FirstName,
8301                'shipping-last-name': shippingInfo.LastName,
8302                'shipping-address1': shippingInfo.Address1,
8303                'shipping-address2': shippingInfo.Address2,
8304                'shipping-city': shippingInfo.City,
8305                'shipping-state': shippingInfo.State,
8306                'shipping-postal': shippingInfo.Zip
8307            };
8308        } else if ($scope.isPayPal()) {
8309            data = {
8310                processor: $scope.StatusInfo.Processor
8311            };
8312        } else if ($scope.isVantivHosted()) {
8313            data = {
8314                processor: $scope.StatusInfo.Processor,
8315                cart_amount: $scope.Due,
8316                shipping_amount: $scope.getShippingWithDiscount(),
8317                tax_amount: $scope.Taxes,
8318                'billing-email': $scope.BillingInfo.Email,
8319                'billing-phone': $scope.BillingInfo.Phone,
8320                'shipping-first-name': shippingInfo.FirstName,
8321                'shipping-last-name': shippingInfo.LastName,
8322                'shipping-address1': shippingInfo.Address1,
8323                'shipping-address2': shippingInfo.Address2,
8324                'shipping-city': shippingInfo.City,
8325                'shipping-state': shippingInfo.State,
8326                'shipping-postal': shippingInfo.Zip,
8327                'billing-first-name': $scope.BillingInfo.FirstName,
8328                'billing-last-name': $scope.BillingInfo.LastName,
8329                'billing-address1': $scope.BillingInfo.Address1,
8330                'billing-address2': $scope.BillingInfo.Address2,
8331                'billing-city': $scope.BillingInfo.City,
8332                'billing-state': $scope.BillingInfo.State,
8333                'billing-postal': $scope.BillingInfo.Zip
8334            };
8335        } else if ($scope.isRainProcessor()) {
8336            data = {
8337                processor: $scope.StatusInfo.Processor,
8338                cart_amount: $scope.getAmountDue(),
8339                save_card: $scope.isCardOnFileNeeded(),
8340                customer_email: $scope.BillingInfo.Email,
8341                subscription_only: $scope.isSubscriptionOnly(),
8342                spid: $scope.scheduledPaymentId,
8343                sid: $scope.subscriptionId,
8344                iid: $scope.invoiceId,
8345                icid: $scope.invoicesCartId,
8346                rpid: $scope.rtoCartId,
8347                token: $scope.paymentNonce,
8348                mms_payment: $scope.isMmsPayment(),
8349            };
8350
8351            if (paymentMethod) {
8352                data.payment_method = paymentMethod;
8353            }
8354            if (paymentMethodTypes && paymentMethodTypes.length) {
8355                data.payment_method_types = paymentMethodTypes;
8356            }
8357        }
8357 else if ($scope.isQuiltAdyen()) {
8358            if (!$scope.checkFlagAvsEcommerce()) {
8359                data = {
8360                    processor: $scope.StatusInfo.Processor,
8361                    cart_amount: $scope.getAmountDue(),
8362                    transaction_id: $scope.TransactionId,
8363                    subscription_payment: $scope.isSubscriptionPayment(),
8364                    scheduled_payment: $scope.isScheduledPayment(),
8365                    invoice_payment: $scope.isInvoicePayment(),
8366                    invoices_payment: $scope.isInvoicesPayment(),
8367                    rto_payment: $scope.isRtoPayment(),
8368                    mms_payment: $scope.isMmsPayment(),
8369                    save_card: $scope.isCardOnFileNeeded(),
8370                    spid: $scope.scheduledPaymentId,
8371                    sid: $scope.subscriptionId,
8372                    iid: $scope.invoiceId,
8373                    icid: $scope.invoicesCartId,
8374                    rpid: $scope.rtoCartId,
8375                    token: $scope.paymentNonce,
8376                    'billing-email': $scope.BillingInfo.Email,
8377                    'billing-phone': $scope.BillingInfo.Phone,
8378                    'shipping-first-name': shippingInfo.FirstName,
8379                    'shipping-last-name': shippingInfo.LastName,
8380                    'shipping-address1': shippingInfo.Address1,
8381                    'shipping-address2': shippingInfo.Address2,
8382                    'shipping-city': shippingInfo.City,
8383                    'shipping-state': shippingInfo.State,
8384                    'shipping-postal': shippingInfo.Zip,
8385                    'billing-first-name': $scope.BillingInfo.FirstName,
8386                    'billing-last-name': $scope.BillingInfo.LastName,
8387                    'billing-address1': $scope.BillingInfo.Address1,
8388                    'billing-address2': $scope.BillingInfo.Address2,
8389                    'billing-city': $scope.BillingInfo.City,
8390                    'billing-state': $scope.BillingInfo.State,
8391                    'billing-postal': $scope.BillingInfo.Zip,
8392                };
8393            } else {
8394                data = {
8395                    processor: $scope.StatusInfo.Processor,
8396                    cart_amount: $scope.getAmountDue(),
8397                    transaction_id: $scope.TransactionId,
8398                    subscription_payment: $scope.isSubscriptionPayment(),
8399                    scheduled_payment: $scope.isScheduledPayment(),
8400                    invoice_payment: $scope.isInvoicePayment(),
8401                    invoices_payment: $scope.isInvoicesPayment(),
8402                    rto_payment: $scope.isRtoPayment(),
8403                    mms_payment: $scope.isMmsPayment(),
8404                    save_card: $scope.isCardOnFileNeeded(),
8405                    spid: $scope.scheduledPaymentId,
8406                    sid: $scope.subscriptionId,
8407                    iid: $scope.invoiceId,
8408                    icid: $scope.invoicesCartId,
8409                    rpid: $scope.rtoCartId,
8410                    token: $scope.paymentNonce,
8411                };
8412
8413                if ($scope.isSavedCardsFlagEnabled()) {
8414                    const card = $scope.getCard();
8415
8416                    if (card?.gateway_token) {
8417                        data.payment_method_id = card.gateway_token;
8418                    }
8419                }
8420
8421                if ($scope.InitInfo.user_ip_address || null) {
8422                    data.client_ip_address = $scope.InitInfo.user_ip_address;
8423                }
8424
8425                data.billing_address = $scope.quiltAdyenPrepareBillingAddress();
8426                data.shipping_address = $scope.quiltAdyenPrepareShippingAddress();
8427                data.shopper_datails = $scope.quiltAdyenPrepareShopperDetails();
8428            }
8429
8430            if ($scope.customerId) {
8431                data.customer_id = $scope.customerId;
8432            }
8433
8434            if ($scope.checkFlagAdyenSendLevel3DataAuthCapture()) {
8435                data.level_3_transaction_id = $scope.TransactionId || null;
8436                data.level_3_cart_items = $scope.CartData.Items || []; //This data cannot be passed to adyen. Destroy on backend
8437            }
8438
8439            if ($scope.isScheduledPayment()) {
8440                data.type = 'scheduled_payment';
8441            } else if ($scope.isSubscriptionPayment()) {
8442                data.type = 'subscription_payment';
8443            } else if ($scope.isInvoicePayment()) {
8444                data.type = 'invoice_payment';
8445            } else if ($scope.isInvoicesPayment()) {
8446                data.type = 'invoices_payment';
8447            } else if ($scope.isRtoPayment()) {
8448                data.type = 'rto_payment';
8449            } else if ($scope.isAddCardOnly()) {
8450                data.type = 'add_card_only';
8451            } else if ($scope.isMmsPayment()) {
8452                data.type = 'mms_payment';
8453            }
8454        }
8455
8456        return data;
8457    };
8458
8459    $scope.SubmitInfoGet = function (data) {
8460        if (typeof data === 'undefined') {
8461            data = $scope.getSubmitInfoGetData();
8462        }
8463
8464        $scope.SubmitInfo = null;
8465
8466        if (!data) {
8467            // Should not ever get here, but we did somehow
8468            $scope.StatusInfo.ErrorMessage = "(CH104) Unable to checkout, try refreshing your browser. If that does not help, contact customer service at " + $scope.getStoreCustomerServicePhone();
8469
8470            return $scope.NoOpPromise();
8471        }
8472
8473        if (typeof data === 'object') {
8474            const dynamicData = {};
8475
8476            if (data.processor === $scope.PROCESSOR_QUILT_ADYEN) {
8477                data.transaction_id = $scope.TransactionId;
8478
8479                if ($scope.checkFlagAdyenSendLevel3DataAuthCapture()) {
8480                    data.level_3_transaction_id = $scope.TransactionId || null;
8481                }
8482            }
8483
8484            data = Object.assign({}, data, dynamicData);
8485        }
8486
8487        var noCart = false;
8488        if ($scope.invoiceId) {
8489            noCart = true;
8490        }
8491
8492        var requestQuery = 'u=' + $scope.getStoreUuid();
8493        requestQuery += '&isMobileBrowser=' + $scope.isMobileBrowser;
8494
8495        if ($scope.isSavedCardsFlagEnabled() && $scope.customerId && $scope.formData?.selectedCustomerCardId) {
8496            const card = $scope.getCard();
8497
8498            data.customer_id = $scope.customerId;
8499            data.customer_vault_id = data.external_id = card.external_id;
8500            data.payment_method = card.gateway_token;
8501        }
8502
8503        return $http.post($scope.BuildUrl('/checkout/submitinfo', noCart, requestQuery), data, {withCredentials: true}).then(function ($result) {
8504            if ($result.data.error) {
8505                if ($result.data.error.error_type == 'captcha_expired') {
8506                    $scope.showCaptcha = true;
8507                    return $scope.checkCaptcha();
8508                } else {
8509                    var errorMessage = $result.data.error.error_message;
8510                    var errorType = $result.data.error.error_type;
8511                    if ($result.data.error.error_type) {
8512
8513                        if (errorType === 'VH Timeout') {
8514                            $scope.setErrorMessageBottom("(VH 1500) There was a temporary error while setting up your payment. Please try again.");
8515                        } else if (!errorMessage) {
8516                            $scope.setErrorMessageBottom("(VTV 400) There is a configuration error and we cannot continue processing this order, please contact the store to get this resolved" + $scope.getStoreCustomerServicePhone());
8517                        } else {
8518                            $scope.setErrorMessageBottom('(CH' + errorType + ') ' + errorMessage);
8519                        }
8520                    } else {
8521                        if (errorMessage === 'Invalid AccountToken') {
8522                            $scope.setErrorMessageBottom("(VTV 401) There is a configuration error and we cannot continue processing this order, please contact customer service " + $scope.getStoreCustomerServicePhone());
8523                        } else {
8524                            $scope.setErrorMessageBottom("(CH103) " + errorMessage);
8525                        }
8526                    }
8527                    $scope.setProcessingCheckout(false);
8528                }
8529            } else {
8530                $scope.SubmitInfo = $result.data.info;
8531
8532                if ($scope.isRainProcessor()) {
8533                    // Through the Quilt Stripe payfac an already-paid intent (page reload after auth) carries no
8534                    // client secret; the already_paid path posts the order without confirming a payment in the browser.
8535                    var quiltAlreadyPaid = window.knobby && window.knobby.module_stripe_to_quilt_payfac
8536                        && $result.data && $result.data.info && $result.data.info.already_paid;
8537                    if ($result.data && $result.data.info && ($result.data.info.token || quiltAlreadyPaid)) {
8538                        $scope.Rain.processorToken = $result.data.info.token;
8539                        $scope.Rain.intentId = $result.data.info.intent_id;
8540                        $scope.Rain.paymentAlreadyPaid = $result.data.info.already_paid;
8541                    } else {
8542                        $scope.StatusInfo.ErrorMessage = "(CH100) An unexpected error occurred, please try again.";
8543                    }
8544                } else if ($scope.isQuiltAdyen()) {
8545                    if ($result.data?.info?.customer_id && !$scope.customerId) {
8546                        $scope.customerId = $result.data.info.customer_id;
8547                    }
8548                    if (window.knobby && window.knobby['module_web_order_coming_through_as_abandoned_cart']) {
8549                        $scope.Adyen.paymentAlreadyPaid = ['requires_capture', 'succeeded'].includes($result.data?.info?.status);
8550                    } else {
8551                        $scope.Adyen.paymentAlreadyPaid = $result.data?.info?.status === 'requires_capture';
8552                    }
8553                    if ($scope.Adyen.paymentAlreadyPaid) {
8554                        $scope.Adyen.authData = $result.data.info;
8555                    }
8556                }
8557            }
8558        }, function(error) {
8559            $scope.StatusInfo.ErrorMessage = "(CH101) An unexpected error occurred, please try again.";
8560            $scope.setProcessingCheckout(false);
8561        });
8562    };
8563
8564    $scope.getAuthPaymentData = function () {
8565        const isSavedCard =
8566            $scope.isSavedCardsFlagEnabled() &&
8567            $scope.paymentMode === 'saved' &&
8568            $scope.formData?.selectedCustomerCardId;
8569
8570        let params = {
8571            'processor': $scope.PROCESSOR_QUILT_ADYEN,
8572            'amount': $scope.getAmountDue(),
8573            'billing_address1': $scope.BillingInfo.Address1,
8574            'billing_address2': $scope.BillingInfo.Address2,
8575            'billing_city': $scope.BillingInfo.City,
8576            'billing_state': $scope.BillingInfo.State,
8577            'billing_postal': $scope.BillingInfo.Zip,
8578            'billing_country': $scope.BillingInfo.CountryIso2Key,
8579        };
8580
8581        if (isSavedCard) {
8582            const card = $scope.getCard();
8583
8584            if (card) {
8585                params.pos_customer_card_id = card.pos_customer_card_id;
8586                params.gateway_token = card.gateway_token;
8587                params.pos_customer_id = $scope.customerId;
8588                if (window.knobby && window.knobby['module_fix_external_customer_id_for_migrated_adyen_cards']) {
8589                    params.payfac_customer_id = card.external_id;
8590                }
8591            }
8592        } else {
8593            const method = $scope.Adyen.state.data.paymentMethod;
8594
8595            params.enc_number = method.encryptedCardNumber;
8596            params.enc_month = method.encryptedExpiryMonth;
8597            params.enc_year = method.encryptedExpiryYear;
8598            params.enc_cvv = method.encryptedSecurityCode;
8599
8600            if ($scope.isSavedCardsFlagEnabled()) {
8601                params.nickname = $scope.CardInfo.nickname;
8602                params.pos_customer_id = $scope.customerId;
8603            }
8604        }
8605
8606        return params;
8607    };
8608
8609    $scope.authPayment = function(data) {
8610        if ($scope.Adyen.paymentAlreadyPaid) {
8611            $scope.setProcessingCheckout(false);
8612            return Promise.resolve();
8613        }
8614
8615        const isSavedCard = (
8616            $scope.isSavedCardsFlagEnabled() &&
8617            $scope.paymentMode === 'saved' &&
8618            $scope.formData?.selectedCustomerCardId
8619        );
8620
8621        if (!isSavedCard && !$scope.Adyen.state.data.paymentMethod) {
8622            $scope.setErrorMessageBottom("Payment invalid");
8623            $scope.setProcessingCheckout(false);
8624            return Promise.resolve();
8625        }
8626        if (!$scope.SubmitInfo || !$scope.SubmitInfo.payment_id) {
8627            $scope.setErrorMessageBottom("Payment ID missing");
8628            $scope.setProcessingCheckout(false);
8629            return Promise.resolve();
8630        }
8631
8632        if (typeof data === 'undefined') {
8633            data = $scope.getAuthPaymentData();
8634        }
8635
8636        if (typeof data === 'object') {
8637            data = Object.assign({}, data, {
8638                'transaction_id': $scope.SubmitInfo.transaction_id || $scope.TransactionId,
8639                'payment_id': $scope.SubmitInfo.payment_id,
8640            });
8641        }
8642
8643        return $http.post($scope.BuildUrl('/checkout/auth'), data, {withCredentials: true}).then(function(result) {
8644            if (result.data.error) {
8645                $scope.setErrorMessageBottom(result.data.error.message);
8646                $scope.Adyen.authData = null;
8647            } else {
8648                $scope.Adyen.authData = result.data;
8649            }
8650        }, function(result) {
8651            if ($scope.knobby && $scope.knobby.module_show_card_on_file_decline_reason) {
8652                $scope.setErrorMessageBottom('We were unable to process this payment. Please try again.');
8653                $scope.Adyen.authData = null;
8654                $scope.setProcessingCheckout(false);
8655            }
8656
8657            throw result;
8658        });
8659    };
8660
8661    $scope.requireAddressInfo = function () {
8662        if ($scope.CartStatus.Valid) {
8663            if ($scope.isVantivHosted() || $scope.isRainProcessor() || $scope.isQuiltAdyen()) {
8664                $scope.BillingAddress.Required = true;
8665                return true
8666            } else if (!$scope.requirePayment() || $scope.StatusInfo.Processor === 'gc') {
8667                return true;
8668            }
8669        }
8670        return false;
8671    };
8672
8673    $scope.ProcessorSubmit = function () {
8674        $('#processorform').submit();
8675    };
8676
8677    /** @return {array|null} */
8678    $scope.SubmitFields = function () {
8679        if ($scope.CartData) {
8680            if ($scope.StatusInfo.Processor == 'NMI3S') {
8681                return $scope.NMIFormFields();
8682            } else if ($scope.isPayPal()) {
8683                return $scope.PayPalFields();
8684            }
8685        }
8686        return null;
8687    };
8688
8689    $scope.NMIFormFields = function () {
8690        var shippingInfo = $scope.getEffectiveShippingInfo();
8691
8692        return {
8693            cart_amount: $scope.Due,
8694            shipping_amount: $scope.getShippingWithDiscount(),
8695            tax_amount: $scope.Taxes,
8696            'billing-cc-number': $scope.CardInfo.CCValid,
8697            'billing-cc-exp': $scope.CardInfo.ExpFormated,
8698            'billing-cvv': $scope.CardInfo.CVV,
8699            'billing-first-name': $scope.BillingInfo.FirstName,
8700            'billing-last-name': $scope.BillingInfo.LastName,
8701            'billing-address1': $scope.BillingInfo.Address1,
8702            'billing-address2': $scope.BillingInfo.Address2,
8703            'billing-city': $scope.BillingInfo.City,
8704            'billing-state': $scope.BillingInfo.State,
8705            'billing-postal': $scope.BillingInfo.Zip,
8706            'billing-country': $scope.BillingInfo.Country,
8707            'billing-email': $scope.BillingInfo.Email,
8708            'billing-phone': $scope.BillingInfo.Phone,
8709            'shipping-first-name': shippingInfo.FirstName,
8710            'shipping-last-name': shippingInfo.LastName,
8711            'shipping-address1': shippingInfo.Address1,
8712            'shipping-address2': shippingInfo.Address2,
8713            'shipping-city': shippingInfo.City,
8714            'shipping-state': shippingInfo.State,
8715            'shipping-postal': shippingInfo.Zip,
8716            'shipping-country': shippingInfo.Country,
8717            'shipping-email': $scope.ContactInfo.Email,
8718            'shipping-phone': $scope.ContactInfo.Phone
8719        };
8720    };
8721
8722    $scope.PayPalFields = function () {
8723        var gst_store_but_not_gst_buyer = ($scope.showAsGST() && !$scope.isShippingCountryGST());
8724
8725        //TODO clean up bug##168771561 2019-10-01
8726        var totalDiscount = $scope.RoundTo2Dec($scope.GCTotal + $scope.Discounts);
8727        //We will change HTML form only in a case when discount amount >
8727 subtotal.
8728        //Because Paypal does not support this case
8729        if (totalDiscount >= $scope.SubTotal) {
8730            var fields = {
8731                cmd: '_cart',
8732                paymentaction: $scope.SubmitInfo ? $scope.SubmitInfo.payment_action : null,
8733                business: $scope.SubmitInfo ? $scope.SubmitInfo.business : null,
8734                notify_url: $scope.SubmitInfo ? $scope.SubmitInfo.notify_url : null,
8735                shopping_url: $scope.SubmitInfo ? $scope.SubmitInfo.shopping_url : null,
8736                invoice: $scope.SubmitInfo ? $scope.SubmitInfo.cart_id : null,
8737                weight_unit: $scope.InitInfo.WeightUnit ? $scope.InitInfo.WeightUnit : 'lbs',
8738                currency_code: $scope.InitInfo.Currency ? $scope.InitInfo.Currency : 'USD',
8739                upload: 1,
8740                bn: 'Rain_SP'
8741            };
8742        } else {
8743            var fields = {
8744                cmd: '_cart',
8745                paymentaction: $scope.SubmitInfo ? $scope.SubmitInfo.payment_action : null,
8746                business: $scope.SubmitInfo ? $scope.SubmitInfo.business : null,
8747                notify_url: $scope.SubmitInfo ? $scope.SubmitInfo.notify_url : null,
8748                shopping_url: $scope.SubmitInfo ? $scope.SubmitInfo.shopping_url : null,
8749                invoice: $scope.SubmitInfo ? $scope.SubmitInfo.cart_id : null,
8750                weight_unit: $scope.InitInfo.WeightUnit ? $scope.InitInfo.WeightUnit : 'lbs',
8751                currency_code: $scope.InitInfo.Currency ? $scope.InitInfo.Currency : 'USD',
8752                upload: 1,
8753                discount_amount_cart: totalDiscount,
8754                bn: 'Rain_SP'
8755            };
8756
8757            var tax = $scope.Taxes ? $scope.Taxes : 0;
8758            if ($scope.ShippingTaxes) {
8759                tax += $scope.ShippingTaxes;
8760            }
8761
8762            if (gst_store_but_not_gst_buyer) {
8763
8764                tax = 0;
8765            } else if ($scope.showAsGST()) {
8766
8767                tax = $scope.getGstTaxes();
8768            }
8769
8770            fields['tax_cart'] = $scope.RoundTo2Dec(tax);
8771        }
8772
8773        fields['return'] = $scope.SubmitInfo ? $scope.SubmitInfo['return'] : null;
8774
8775        if ($scope.CartData.Items && $scope.CartData.Items.length) {
8776            //We will change HTML form only in a case when discount amount > subtotal.
8777            //Because Paypal does not support this case
8778            if (totalDiscount >= $scope.SubTotal) {
8779                var price = 0;
8780                var productsNames = [];
8781                $scope.CartData.Items.forEach(function(item) {
8782                    productsNames.push(item.title);
8783                });
8784
8785                fields['amount_1'] = $scope.CartTotal;
8786                fields['shipping_1'] = 0; //We included shipping amount in the $scope.CartTotal;
8787                fields['item_name_1'] = productsNames.join(';');
8788            } else {
8789                var correctorThreeDecimalPrice = 0;
8790
8791                for (var i = 1; i <= $scope.CartData.Items.length; i++) {
8792                    var item = $scope.CartData.Items[i - 1];
8793
8794                    // PayPal cannot handle fractional quantity, so on fractional quantity we always send them a quantity of 1
8795                    var quantity = item.qty;
8796                    var title = item.title;
8797                    var price = $scope.getItemPriceForWeb(item);
8798
8799                    if (gst_store_but_not_gst_buyer) {
8800
8801                        price = $scope.getItemGstPrice(item);
8802                    }
8803
8804                    if (item.qty_type == 'fraction' && item.qty_remainder > 0) {
8805                        if (gst_store_but_not_gst_buyer) {
8806
8807                            price = item.qty * $scope.getItemGstPrice(item);
8808                        } else {
8809                            price = item.qty * $scope.getItemPriceForWeb(item);
8810                        }
8811                        quantity = 1;
8812                        title += ' (' + item.qty + ' ' + item.measure_unit + 's)';
8813                    }
8814
8815                    if (quantity > 1) {
8816                        var differenceBetweenAmountAfterMultiply = $scope.RoundTo2Dec(
8817                            fractionalMultiplication($scope.RoundTo2Dec(price), quantity) - $scope.RoundTo2Dec(fractionalMultiplication(price, quantity))
8818                        );
8819
8820                        if (differenceBetweenAmountAfterMultiply) {
8821                            correctorThreeDecimalPrice += differenceBetweenAmountAfterMultiply;
8822                        }
8823                    }
8824
8825                    fields['amount_' + i] = $scope.RoundTo2Dec(price);
8826                    fields['item_name_' + i] = title;
8827                    fields['quantity_' + i] = quantity;
8828                    fields['item_number_' + i] = item.id;
8829                    fields['weight_' + i] = item.weight;
8830
8831                    if (!$scope.isShippingRequired()) {
8832                        fields['shipping_' + i] = '0'; // If shipping is not required, we have to send an empty shipping field to paypal to prevent them from charging shipping <OldIPN>
8833                    } else {
8834                        // We add all shipping to the first item
8835                        if (i == 1) {
8836                            if ($scope.ShippingInfo.Option && (!$scope.InitInfo.PayPalShippingEnabled || $scope.ShippingInfo.Option.price > 0)) {
8837                                fields['shipping_1'] = $scope.RoundTo2Dec($scope.ShippingInfo.Option.price - $scope.ShippingDiscount);
8838                            }
8839                        } else if (!$scope.InitInfo.PayPalShippingEnabled) {
8840                            fields['shipping_' + i] = '0';
8841                        }
8842                    }
8843                }
8844                if (correctorThreeDecimalPrice) {
8845                    //since our system supports three decimal prices and PayPal does not, we do this calculation. In order to adjust the price which we send to PayPal
8846                    fields.discount_amount_cart = fields.discount_amount_cart ?
8847                        fields.discount_amount_cart + correctorThreeDecimalPrice :
8848                        correctorThreeDecimalPrice;
8849                }
8850            }
8851        }
8852
8853        if ($scope.isExtendPayPalShippingInfo()) {
8854            fields["no_shipping"] = 0;
8855            fields["address_override"] = 1;
8856
8857            if ($scope.isPickup()) {
8858                if ($scope.ShippingInfo.Option.address) {
8859                    var address = $scope.ShippingInfo.Option.address;
8860
8861                    fields["country"] = $scope.CountryIso2KeyGet($scope.InitInfo.Countries[address.country]);
8862                    fields["city"] = address.city;
8863                    fields["state"] = address.state;
8864                    fields["zip"] = address.postalCode;
8865                    fields["address1"] = address.street1;
8866                    if (address.street2) {
8867                        fields["address2"] = address.street2 || '';
8868                    }
8869                }
8870            } else {
8871                fields["first_name"] = $scope.ShippingInfo.FirstName;
8872                fields["last_name"] = $scope.ShippingInfo.LastName;
8873                fields["country"] = $scope.ShippingInfo.CountryIso2Key;
8874                fields["city"] = $scope.ShippingInfo.City;
8875                fields["state"] = $scope.ShippingInfo.State;
8876                fields["zip"] = $scope.ShippingInfo.Zip;
8877                fields["address1"] = $scope.ShippingInfo.Address1;
8878                if ($scope.ShippingInfo.Address2) {
8879                    fields["address2"] = $scope.ShippingInfo.Address2 || '';
8880                }
8881            }
8882        }
8883
8884        return fields;
8885    };
8886
8887    // endregion
8888
8889    // ******************************************************************************************
8890    // region Taxes
8891    // ******************************************************************************************
8892
8893    $scope.getTaxBitmaps = function () {
8894        if ($scope.InitInfo && $scope.InitInfo.tax_bitmaps) {
8895            return $scope.InitInfo.tax_bitmaps;
8896        }
8897        return {};
8898    };
8899
8900    $scope.getProductTaxBitmap = function () {
8901        var bitmaps = $scope.getTaxBitmaps();
8902        if (bitmaps.product >= 0) {
8903            return +bitmaps.product;
8904        }
8905        return null;
8906    };
8907
8908    $scope.getClassTaxBitmap = function () {
8909        var bitmaps = $scope.getTaxBitmaps();
8910        if (bitmaps.class >= 0) {
8911            return +bitmaps.class;
8912        }
8913        return null;
8914    };
8915
8916    $scope.getServiceTaxBitmap = function () {
8917        var bitmaps = $scope.getTaxBitmaps();
8918        if (bitmaps.service >= 0) {
8919            return +bitmaps.service;
8920        }
8921        return null;
8922    };
8923
8924    // endregion
8925
8926    // ******************************************************************************************
8927    // region Items
8928    // ******************************************************************************************
8929
8930    $scope.getItemsWithPackageItems = function () {
8931        var items = [];
8932        if ($scope.CartData && $scope.CartData.Items) {
8933            $scope.CartData.Items.forEach(function (i) {
8934                items.push(i);
8935                if (i.package_items && Array.isArray(i.package_items)) {
8936                    i.package_items.forEach(function (pi) {
8937                        pi.qty = i.qty;
8938                        pi.total = pi.qty * pi.price;
8939                        items.push(pi);
8940                    })
8941                }
8942            });
8943        }
8944        return items;
8945    };
8946
8947    $scope.getItemPrice = function (item) {
8948        var price = +item.price;
8949
8950        if (item.type === 'class') {
8951            return price;
8952        }
8953
8954        if (item.package_items && item.package_items.length) {
8955            item.package_items.forEach(function (i) {
8956                price += i.price;
8957            })
8958        }
8959
8960        return price;
8961    };
8962
8963    $scope.getItemPriceForWeb = function (item) {
8964        var price = +item.price;
8965        if (item.package_items && item.package_items.length) {
8966            item.package_items.forEach(function (i) {
8967                price += i.price;
8968            })
8969        }
8970
8971        return price;
8972    };
8973
8974    $scope.getItemGstPrice = function (item) {
8975        var gst_price = +item.gst_price;
8976
8977        if (item.package_items && item.package_items.length) {
8978            item.package_items.forEach(function (i) {
8979                gst_price += i.gst_price;
8980            })
8981        }
8982
8983        return gst_price;
8984    };
8985
8986    $scope.getItemTotal = function (item) {
8987        return fractionalMultiplication(item.qty, $scope.getItemPrice(item));
8988    };
8989
8990    $scope.getItemTotalForWeb = function (item) {
8991        return fractionalMultiplication(item.qty, $scope.getItemPriceForWeb(item));
8992    };
8993
8994    $scope.getItemGstTotal = function (item) {
8995        return fractionalMultiplication(item.qty, $scope.getItemGstPrice(item));
8996    };
8997
8998    $scope.getGstTaxes = function () {
8999        return (fixFloat($scope.GST_Taxes) + fixFloat($scope.ShippingTaxes));
9000    };
9001
9002    $scope.showAsGST = function () {
9003        return ($scope.isStoreGST() && !$scope.isWholesaleEnabled());
9004    };
9005
9006    $scope.breakDownTaxes = function () {
9007        return Object.keys($scope.JurisdictionalTaxes).length > 0 &&
9008            (
9009                /* Flag turned on OR it is Canadian store */
9010                ($scope.knobby && ($scope.knobby['module_pos_show_tax_breakdown_on_receipt'] || $scope.knobby['pos_show_tax_breakdown_on_receipt'])) || $scope.useCanadaTax()
9011            )
9012        ;
9013    };
9014
9015    $scope.showTaxJurisdiction = function(jurisdiction, taxes, shipping) {
9016        if (!taxes.tax_paid) {
9017            return false;
9018        }
9019        var shippingTax = jurisdiction.indexOf('Shipping') >= 0;
9020        if (shippingTax != shipping) {
9021            return false;
9022        }
9023        return true;
9024    };
9025
9026    $scope.showQuantityAsNumber = function (item) {
9027        return !item.quantity_updating && (item.class_attendees?.length > 0 || item.type === 'subscription');
9028    };
9029
9030    $scope.showQuantityAsSelector = function (item) {
9031        return !item.quantity_updating && item.type !== 'rental' && item.type !== 'subscription' && !(item.class_attendees?.length > 0);
9032    };
9033
9034    $scope.showPaymentOptions = function() {
9035        return ($scope.isPayAtPickupEnabled()
9036            && ($scope.isPickup() || $scope.isDelivery())
9037            && !$scope.isCardOnFileNeeded()
9038        );
9039    };
9040
9041    $scope.showPaymentSummary = function() {
9042        return (!$scope.isVantivHosted()
9043             && !$scope.isPayPal()
9044             && !$scope.isWholesaleNoCCEnabled()
9045        );
9046    };
9047
9048    $scope.showPaymentSection = function() {
9049        if ($scope.isWebPayment() || $scope.digitalOrder) {
9050            return true;
9051        }
9052        return $scope.BillingStatus.Editing;
9053    };
9054
9055    $scope.showBillingSection = function() {
9056        if ($scope.isFullyPaidByGiftCard()) {
9057            return false;
9058        }
9059
9060        if (
9061            $scope.isWebPayment()
9062            || ($scope.CartStatus.Valid && $scope.requireTermsWithShipping())
9063            || $scope.digitalOrder
9064        ) {
9065            return true;
9066        }
9067
9068        return ($scope.CartStatus.Valid
9069            && $scope.ShippingStatus.Entered
9070            && !$scope.ShippingStatus.Editing
9071        );
9072    };
9073
9074    $scope.showBillingForm = function() {
9075        return ($scope.showBillingSection() && $scope.BillingStatus.Required );
9076    }
9077
9078    // Determines if we show the billing address even if it's same as shipping
9079    $scope.showBillingAddress = function() {
9080        return (
9081            (
9082                   !$scope.isShippingRequired()
9083                || !$scope.BillingInfo.AddressSameAsShipping
9084                ||  $scope.isStorePickup()
9085                ||  $scope.isCurbsidePickup()
9086                ||  $scope.isSchoolPickup()
9087                ||  $scope.isVantivHosted()
9088                ||  ($scope.isNewUIFlagSet() && $scope.isWholesaleNoCCEnabled()) // yes we still want to show it for now
9089          )
9090            && !$scope.isPayAtPickup()
9091        );
9092    };
9093
9094    $scope.showBillingSummary = function() {
9095        return (
9096            $scope.ShippingStatus.Editing
9097            && ($scope.BillingStatus.Entered || $scope.ShippingStatus.Entered)
9098            && $scope.BillingStatus.Required
9099            && !$scope.isWebPayment()
9100        );
9101    };
9102
9103    $scope.showCreditCardOption = function() {
9104        return (
9105              ($scope.Due || $scope.requirePayment())
9106            && $scope.isPayPal()
9107            && !$scope.isVantivHosted()
9108            && $scope.hasCardProcessor()
9109            && !$scope.isWholesaleNoCCEnabled()
9110            && !$scope.isProcessing()
9111        );
9112    };
9113
9114    $scope.areTermsEnabled = function() {
9115        return !!(
9116            $scope.InitInfo &&
9117            $scope.InitInfo.CheckoutSettings &&
9118            $scope.InitInfo.CheckoutSettings.TermsEnabled
9119        );
9120    };
9121
9122    $scope.showTerms = function() {
9123        if ($scope.checkFlagCannotViewTermsConditionsCheckoutScreenAddressInfo()) {
9124            return (
9125                $scope.requireTermsWithShipping() || $scope.areTermsEnabled()
9126            );
9127        } else {
9128            return (
9129                $scope.requireTermsWithShipping() ||
9130                ($scope.areTermsEnabled() && $scope.isShippingEntered())
9131            );
9132        }
9133
9134    };
9135
9136    $scope.showPayPalOption = function() {
9137        return (
9138            $scope.Due
9139            && !$scope.isPayPal()
9140            && $scope.isVantivHosted()
9141            && $scope.isPayPalEnabled()
9142            && !$scope.isCardOnFileNeeded()
9143            && !$scope.isProcessing()
9144        );
9145    };
9146
9147    // Primarily for NMI
9148    $scope.showCCPayment = function() {
9149        return (!$scope.isVantivHosted()
9150             && !$scope.isRainProcessor()
9151             && !$scope.isQuiltAdyen()
9152             && !$scope.isPayPal()
9153             && !$scope.isWholesaleNoCCEnabled()
9154        );
9155    };
9156
9157    // For Rain Payments
9158    $scope.showRainProcessing = function() {
9159        return ($scope.isRainProcessor()
9160            && !$scope.isWholesaleNoCCEnabled()
9161        );
9162    }
9163
9164    $scope.showQuiltProcessing = function() {
9165        return ($scope.isQuiltAdyen()
9166            && !$scope.isWholesaleNoCCEnabled()
9167        );
9168    }
9169
9170    $scope.showCheckoutButton = function() {
9171        return $scope.CartStatus.Valid
9172            && $scope.isBillingDone()
9173            && !$scope.isProcessing()
9174            && !$scope.isPayPal()
9175            && !$scope.isFree()
9176    };
9177
9178    $scope.showBillingInitiallyNeededButton = function() {
9179        if (!$scope.checkFlagBlockCheckoutButtonWhileProcessing()) {
9180            return $scope.CartStatus.Valid && !$scope.ShippingStatus.Entered && !$scope.isPayPal() && !$scope.isFree();
9181        }
9182
9183        return $scope.CartStatus.Valid
9184            && !$scope.ShippingStatus.Entered
9185            && !$scope.isPayPal()
9186            && !$scope.isFree()
9187            && !$scope.isProcessing()
9188        ;
9189    };
9190
9191    $scope.showBillingStillRequiredButton = function() {
9192        return $scope.ShippingStatus.Editing && $scope.BillingStatus.Entered && (!$scope.BillingStatus.Valid || !$scope.CardStatus.Valid) && !$scope.isProcessing();
9193    };
9194
9195    $scope.isBillingDone = function() {
9196        return (
9197               ($scope.BillingStatus.Editing && $scope.ShippingStatus.Valid && $scope.ContactStatus.Valid)
9198            || ($scope.ShippingStatus.Editing && $scope.BillingStatus.Entered && $scope.BillingStatus.Valid && $scope.CardStatus.Valid )
9199        );
9200    };
9201
9202    $scope.isBillingRequired = function() {
9203        if ($scope.isWholesaleNoCCEnabled() && !$scope.isNewUIFlagSet()) {
9204            return false;
9205        }
9206
9207        if ($scope.Due === 0 && !$scope.isCardOnFileNeeded()) {
9208            return false;
9209        }
9210
9211        return $scope.BillingStatus.Required;
9212    };
9213
9214    $scope.isStoreGST = function () {
9215        return ($scope.InitInfo.gstEnabled && $scope.getStoreCountryKey() == "AUS");
9216    };
9217
9218    $scope.isShippingCountryGST = function () {
9219        return ($scope.getShippingCountryKey() == 'AUS');
9220    };
9221
9222    $scope.isTaxInclusive = function () {
9223        return ($scope.isStoreGST() && $scope.isShippingCountryGST());
9224    };
9225
9226    $scope.isStoreHasLocationForRetailFee = function () {
9227        return $scope.InitInfo.store_locations.some(function (location) {
9228            return location.state === 'CO';
9229        })
9230    };
9231
9232    $scope.gstToNon = function () {
9233        return ($scope.showAsGST() && !$scope.isShippingCountryGST());
9234    };
9235
9236    $scope.getShippingTotal = function () {
9237
9238        return $scope.ShippingTotal;
9239    };
9240
9241    $scope.getGstShippingTotal = function () {
9242
9243        return $scope.ShippingTotal + $scope.ShippingTaxes;
9244    };
9245
9246    $scope.addShippingTaxes = function (amt) {
9247
9248        return parseFloat(amt) + $scope.calculateShippingTaxAmount(amt);
9249    };
9250
9251    $scope.QuantityChanged = function (item) {
9252        item.quantity_updating = true;
9253
9254        if (item.qty_type === 'fraction') {
9255            var remainderOptions = $scope.RemainderOptions(item);
9256            var availableRemainderValues = (remainderOptions || []).map(function (option) {
9257                return parseInt(option.id);
9258            })
9259
9260            if (availableRemainderValues.length && !availableRemainderValues.includes(parseInt(item.qty_remainder))) {
9261                console.log('item.qty_remainder', item.qty_remainder, ' = ', availableRemainderValues[0])
9262                item.qty_remainder = availableRemainderValues[0];
9263            }
9264        }
9265
9266        item.qty = $scope.getItemQty(item);
9267        item.total = $scope.RoundTo2Dec(fractionalMultiplication(item.qty, $scope.getItemPrice(item)));
9268
9269        var requestQuantityChanged = () => {
9270            return $http.post($scope.BuildUrl('/checkout/itemupdate'), item, {withCredentials: true});
9271        };
9272
9273        if ($scope.isCarePlanEnable()) {
9274            requestQuantityChanged = () => {
9275                return $scope.blockUIWhileCallback(() => {
9276                    return $http.post($scope.BuildUrl('/checkout/itemupdate'), item, {withCredentials: true});
9277                });
9278            };
9279        }
9280
9281        requestQuantityChanged().then(function (result) {
9282            $scope.CartDataAdapt(result.data);
9283            $scope.ShippingStatus.DirtyZip = null; // Force options
9284            $scope.ShippingOptionsGet();
9285            $scope.CartSync();
9286            if ($scope.checkFlagCarePlans()) {
9287                $scope.updateCarePlanPrice();
9288            }
9289        });
9290    };
9291    $scope.carePlanChanged = function (item) {
9292        item.quantity_updating = true;
9293
9294        var requestCarePlan = () => {
9295            return $http.post($scope.BuildUrl('/checkout/itemupdate'), item, {withCredentials: true});
9296        };
9297
9298        if ($scope.isCarePlanEnable()) {
9299            requestCarePlan = () => {
9300                return $scope.blockUIWhileCallback(() => {
9301                    return $http.post($scope.BuildUrl('/checkout/itemupdate'), item, {withCredentials: true});
9302                });
9303            };
9304        }
9305
9306        requestCarePlan().then(function (result) {
9307            $scope.CartDataAdapt(result.data);
9308            $scope.ShippingStatus.DirtyZip = null; // Force options
9309            $scope.ShippingOptionsGet();
9310            $scope.CartSync();
9311            if ($scope.checkFlagCarePlans()) {
9312                $scope.updateCarePlanPrice();
9313            }
9314            $scope.Validate();
9315        });
9316    };
9317
9318    $scope.updateCarePlanPrice = function () {
9319        var params = [];
9320        $scope.CartData.Items.forEach(function (cpItem) {
9321            if (cpItem.type === 'care_plan') {
9322                $scope.CartData.Items.forEach(function (item) {
9323                    if (cpItem.parent_uuid === item.uid) {
9324                        var data = {
9325                            itemPrice: item.discounted_total / item.qty,
9326                            pslrId: item.pslr_id,
9327                            carePlanItem: cpItem,
9328                        }
9329                        params.push(data);
9330                    }
9331                });
9332            }
9333        })
9334        if (params.length) {
9335            $.blockUI();
9336            $http.post($scope.BuildUrl('/checkout/carePlanUpdate'), params, {withCredentials: false}).then(function (result) {
9337                $scope.CartDataAdapt(result.data);
9338                $scope.ShippingStatus.DirtyZip = null; // Force options
9339                $scope.ShippingOptionsGet();
9340                $scope.CartSync();
9341            }).finally(function () {
9342                $.unblockUI();
9343            });
9344        }
9345    };
9346
9347    $scope.getItemQty = function(item) {
9348        var qty = null;
9349
9350        var quotient = Number(item.qty_integer);
9351        var remainder = Number(item.qty_remainder);
9352        var min = Number(item.qty_min);
9353
9354        if (quotient === 0 && remainder === 0) {
9355            qty = min;
9356        } else {
9357            qty = quotient + (remainder / 100);
9358        }
9359
9360        return qty;
9361    };
9362
9363    $scope.IntegerOptions = function (item) {
9364        var min = Math.floor(Number(item.qty_min));
9365        var max = Math.floor(Number(item.qty_max));
9366        var options = [];
9367        for (var i = min; i <= max; i++) {
9368            if ((i % item.sold_in_increments) > 0) {
9369                continue;
9370            }
9371            options.push({id: i + '', name: i + ''})
9372        }
9373        return options;
9374    };
9375
9376    $scope.RemainderOptions = function (item) {
9377        var options = null;
9378
9379        options = $scope.getRemainderOptions(item);
9380
9381        return options;
9382    };
9383
9384    $scope.getRemainderOptions = function (item) {
9385        var options = null;
9386
9387        var qtyType = item.qty_type;
9388        var measureUnit = item.measure_unit;
9389
9390        if ((qtyType === 'fraction') && (measureUnit === 'Meter')) {
9391            options = $scope.getMetricFractionalRemainderOptions(item);
9392        } else {
9393            options = $scope.getRemainderOptions1(item);
9394        }
9395
9396        return options;
9397    };
9398
9399    $scope.getRemainderOptions1 = function (item) {
9400        var options = [];
9401
9402        var minint = Math.floor(item.qty_min);
9403        var maxint = Math.floor(item.qty_max);
9404
9405        if (item.qty_type == 'fraction') {
9406            if (item.measure_unit == 'Meter') {
9407                var minrem1 = Math.round(item.qty_min * 10) % 10;
9408                var maxrem1 = Math.round(item.qty_max * 10) % 10;
9409                var min1 = Number(item.qty_integer) == minint ? minrem1 : 0;
9410                var max1 = Number(item.qty_integer) == maxint ? maxrem1 : 9;
9411                for (var a = min1; a <= max1; a++) {
9412                    var name1 = a == 0 ? '0' : '.' + a;
9413                    options.push({id: a + '', name: name1});
9414                }
9415            } else {
9416                var minrem2 = Math.round(item.qty_min * 100) % 100;
9417                var maxrem2 = Math.round(item.qty_max * 100) % 100;
9418                var min2 = Number(item.qty_integer) == minint ? minrem2 : 0;
9419                var max2 = Number(item.qty_integer) == maxint ? maxrem2 : 75;
9420                for (var b = min2; b <= max2; b += 25) {
9421                    var name2 = '0';
9422                    if (b == 25) {
9423                        name2 = '1/4';
9424                        if (item.FatQuarter && item.qty_integer == 0) {
9425                            name2 = 'Fat Quarter';
9426                        }
9427                    }
9428                    if (b == 50) name2 = '1/2';
9429                    if (b == 75) name2 = '3/4';
9430                    options.push({id: b + '', name: name2});
9431                }
9432            }
9433        }
9434
9435        return options;
9436    };
9437
9438
9439    $scope.getMetricFractionalRemainderOptions = function (item) {
9440        var options = [];
9441
9442        var qtyMinStr = item.qty_min;
9443        var qtyMaxStr = item.qty_max;
9444        var qtyIntegerStr = item.qty_integer;
9445
9446        var qtyMin = parseFloat(qtyMinStr);
9447        var qtyMax = parseFloat(qtyMaxStr);
9448        var qtyInteger = parseFloat(qtyIntegerStr);
9449
9450        var includeFatQuarter = ((qtyInteger === 0) && (qtyMin <= 0.25));
9451
9452        var minint = Math.floor(qtyMin);
9453        var maxint = Math.floor(qtyMax);
9454
9455        var minrem1 = Math.round(qtyMin * 100) % 100;
9456        var maxrem1 = Math.round(qtyMax * 100) % 100;
9457
9458        var min1 = (qtyInteger === minint) ? minrem1 : 0;
9459        var max1 = (qtyInteger === maxint) ? maxrem1 : 90;
9460
9461        if (min1 === 25) {
9462            min1 = 30;
9463        }
9464        for (var i = min1; i <= max1; i += 10) {
9465            if ((i === 30) && includeFatQuarter) {
9466                options.push(
9467                    {
9468                        id: "25", name: ".25"
9469                    }
9470                );
9471            }
9472
9473            var decimal = i / 10;
9474
9475            var label = (i === 0) ? '0' : ('.' + decimal);
9476            options.push(
9477                {
9478                    id: (i + ''), name: label
9479                }
9480            );
9481        }
9482
9483        return options;
9484    };
9485
9486    $scope.showCarePlanInformation = function (item) {
9487        item.isShowCarePlanInformation = !item.isShowCarePlanInformation;
9488    }
9489
9490    $scope.viewAvailableCarePlans = function (item) {
9491        item.is_care_plan_selected = true;
9492
9493        var parentItem = $scope.getItemByParentUuid(item.parent_uuid);
9494
9495        var itemSalePrice = parentItem.discounted_total / parentItem.qty;
9496        window.vueEventHub.$eventHub.$emit('jm-care-plan-modal-open', {
9497            callFrom: 'Web',
9498            itemPrice: item.pslr_data.item_price,
9499            itemSalePrice: itemSalePrice || null,
9500            itemQty: item.qty,
9501            pslrId: item.pslr_data.parent_pslr_id,
9502            itemId: item.id,
9503            callback: function() {
9504                this.addToCart();
9505            }.bind(this),
9506        });
9507    };
9508
9509    $scope.getItemByParentUuid = function (parentUuid) {
9510        return $scope.CartData.Items.find(function (item) {
9511            return item.uid === parentUuid;
9512        });
9513    };
9514
9515    $scope.registerCarePlanHandleEvent = function() {
9516        window.vueEventHub.$eventHub.$on('jm-care-plan-handle-line', function(data) {
9517            if (Object.prototype.toString.call(data) !== "[object Object]"
9518                || Object.prototype.toString.call(data.carePlanForItems) !== "[object Array]"
9519                || !data.carePlanForItems.length
9520            ) {
9521                return;
9522            }
9523
9524            var carePlanItem = data.carePlanForItems[0].length === 0 ? $scope.CartData.Items[0] : data.carePlanForItems[0];
9525            var carePlanLine = {};
9526
9527            carePlanLine.id = data.itemId;
9528            carePlanLine.type = 'care_plan';
9529            carePlanLine.title = 'JM &#174; Care Plan - ' + data.pslrData.product_type_name + ' (' + carePlanItem.applicablePlanDuration + ')';
9530            carePlanLine.planSku = carePlanItem.planSku;
9531            carePlanLine.pslr_data = data.pslrData;
9532            carePlanLine.is_care_plan_selected = true;
9533            carePlanLine.selected_care_plan = carePlanItem;
9534            carePlanLine.care_plans = data.carePlans;
9535            carePlanLine.price = carePlanItem.planRetailPrice;
9536            carePlanLine.qty = 1;
9537            carePlanLine.qty_type = 'integer';
9538            carePlanLine.taxBitmap = data.taxBitmap;
9539            carePlanLine.parent_uuid = $scope.CartData.Items.find(item => item.pslr_id === data.pslrData.parent_pslr_id).uid;
9540            if (data.eligibleForDiscount) {
9541                carePlanLine.discount_eligible = true;
9542                carePlanLine.discount_applied = true;
9543            }
9544
9545            if (data.carePlanForItems[0].length === 0) {
9546                $scope.removeSelectedCarePlan(carePlanLine);
9547            } else {
9548                $scope.carePlanChanged(carePlanLine);
9549            }
9550        });
9551    }
9552
9553    $scope.removeSelectedCarePlan = function (item) {
9554        item.selected_care_plan = [];
9555        item.is_care_plan_selected = false;
9556
9557        var requestRemoveSelectedCarePlan = () => {
9558            return $http.post($scope.BuildUrl('/checkout/itemupdate'), item, {withCredentials: true});
9559        };
9560
9561        if ($scope.isCarePlanEnable()) {
9562            requestRemoveSelectedCarePlan = () => {
9563                return $scope.blockUIWhileCallback(() => {
9564                    return $http.post($scope.BuildUrl('/checkout/itemupdate'), item, {withCredentials: true});
9565                });
9566            };
9567        }
9568
9569        requestRemoveSelectedCarePlan().then(function (result) {
9570            $scope.CartDataAdapt(result.data);
9571            $scope.ShippingStatus.DirtyZip = null; // Force options
9572            $scope.ShippingOptionsGet();
9573            $scope.CartSync();
9574        });
9575    };
9576
9577    $scope.ItemRemove = function (item) {
9578        item.removing = true;
9579        var id = item.id;
9580
9581        if (item.type === 'class') {
9582            id = item.uid;
9583        }
9584
9585        var params = {
9586            item_id: id
9587        };
9588
9589        if (item.condition) {
9590            params['condition'] = item.condition.id;
9591        }
9592
9593        var requestItemRemove = () => {
9594            return $http.get($scope.BuildUrl('/checkout/itemremove'), {
9595                params: params,
9596                withCredentials: true
9597            });
9598        };
9599
9600        if ($scope.isCarePlanEnable()) {
9601            requestItemRemove = () => {
9602                return $scope.blockUIWhileCallback(() => {
9603                    return $http.get($scope.BuildUrl('/checkout/itemremove'), {
9604                        params: params,
9605                        withCredentials: true
9606                    });
9607                });
9608            };
9609        }
9610
9611        requestItemRemove().then(function (result) {
9612            $scope.CartDataAdapt(result.data);
9613            $scope.ShippingStatus.DirtyZip = null; // Force options
9614            $scope.ShippingOptionsGet();
9615            $scope.CartSync();
9616            if ($scope.checkFlagCarePlans()) {
9617                $scope.updateCarePlanPrice();
9618            }
9619        });
9620    };
9621
9622    // endregion
9623
9624    // ******************************************************************************************
9625    // region Gift Cards and Coupons
9626    // ******************************************************************************************
9627
9628    $scope.shouldShowGiftCardsCoupons = function () {
9629        var isWholesale = (typeof $scope.InitInfo.wholesale !== typeof undefined) && $scope.InitInfo.wholesale.checkout_enabled;
9630
9631        return !isWholesale && !$scope.isCartTotalExceeded();
9632    };
9633
9634    $scope.shouldShowGiftCardCouponErrorMsg = function () {
9635        return (($scope.giftCardCouponMsg !== '') && !$scope.shouldShowGiftCardAppliedMsg() && !$scope.shouldShowCouponAppliedMsg());
9636    };
9637
9638    $scope.shouldShowGiftCardAppliedMsg = function () {
9639        return ($scope.giftCardCouponMsg === 'Gift Card Applied');
9640    };
9641
9642    $scope.showGiftCardSummary = function() {
9643        return ($scope.CartData.GiftCards.length > 0 && !$scope.isPayAtPickup());
9644    }
9645
9646    $scope.shouldShowCouponAppliedMsg = function () {
9647        return ($scope.giftCardCouponMsg === 'Coupon Added');
9648    };
9649
9650    $scope.shouldEnableGiftCardCouponApplyBtn = function () {
9651        var code = $scope.StatusInfo.giftCardOrCouponCode;
9652        return (code !== '');
9653    };
9654
9655    $scope.getGiftGardCouponCodeSelector = function () {
9656        return $('#gift-card-coupon-code');
9657    };
9658
9659    $scope.focusGCCouponCodeTextBox = function () {
9660        var textBox = $scope.getGiftGardCouponCodeSelector();
9661        textBox.focus();
9662    };
9663
9664    $scope.clearGCCouponCodeTextBox = function () {
9665        $scope.StatusInfo.giftCardOrCouponCode = '';
9666    };
9667
9668    $scope.showGiftCardCouponMessage = function (msg) {
9669        $scope.clearGiftCardCouponMessage();
9670        $scope.giftCardCouponMsg = msg;
9671    };
9672
9673    $scope.clearGiftCardCouponMessage = function () {
9674        $scope.giftCardCouponMsg = '';
9675    };
9676
9677    $scope.giftCardOrCouponCodeKeyDown = function (event) {
9678        $scope.clearGiftCardCouponMessage();
9679
9680        var code = $scope.StatusInfo.giftCardOrCouponCode;
9681        if (code === '') {
9682            return;
9683        }
9684
9685        var keyCode = event.keyCode;
9686        if (keyCode === 13 || keyCode === 10) {
9687            event.preventDefault();
9688            event.stopPropagation();
9689
9690            $scope.getGiftCardOrCoupon();
9691        }
9692    };
9693
9694    $scope.blockUIWhileCallback = function (promise) {
9695        var beforeunloadHandler = function (event) {
9696            event.returnValue = null;
9697        };
9698
9699        $.blockUI();
9700        window.addEventListener('beforeunload', beforeunloadHandler);
9701        window.localStorage.setItem('loading-interrupted', 'true');
9702
9703        return promise().then((data) => {
9704            window.localStorage.removeItem('loading-interrupted');
9705
9706            return data;
9707        }).finally(() => {
9708            window.removeEventListener('beforeunload', beforeunloadHandler);
9709            $.unblockUI();
9710        });
9711    };
9712
9713    $scope.applyCouponFromInvitePage = function () {
9714        $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADING;
9715        $scope.giftCardOrCoupon = [];
9716
9717        var code = $scope.StatusInfo.giftCardOrCouponCode.replace('CPN**', '').replace('cpn**', '');
9718        var params = {
9719            gc_coupon_code: code
9720        };
9721
9722        var url = $scope.getGiftCardOrCouponApiUrl();
9723
9724        var requestGiftCard = () => {
9725            return $http.get(url, {params: params, withCredentials: true});
9726        };
9727
9728        if ($scope.isCarePlanEnable()) {
9729            requestGiftCard = () => {
9730                return $scope.blockUIWhileCallback(() => {
9731                    return $http.get(url, {params: params, withCredentials: true});
9732                });
9733            };
9734        }
9735
9736        requestGiftCard().then(function successCallback(response) {
9737            var data = null;
9738
9739            if (typeof response.data !== typeof undefined) {
9740                data = response.data;
9741            }
9742
9743            var couponFound = false;
9744            if (typeof data.coupon_code_found !== typeof undefined) {
9745                var couponCodeFound = data.coupon_code_found;
9746                if (couponCodeFound === code) {
9747                    couponFound = true;
9748                }
9749            }
9750
9751            var gcFound = false;
9752            if (typeof data.gc_code_found !== typeof undefined) {
9753                var gcCodeFound = data.gc_code_found;
9754                if (gcCodeFound === code) {
9755                    gcFound = true;
9756                }
9757            }
9758
9759            var responseError = null;
9760            if (typeof data.error !== typeof undefined) {
9761                responseError = data.error;
9762                if (responseError === 'already-added') {
9763                    $scope.clearGCCouponCodeTextBox();
9764                    $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9765                    $scope.showGiftCardCouponMessage('Gift Card or Coupon ' + code + ' has already been applied');
9766                    return;
9767                }
9768            }
9769
9770            if (!couponFound) {
9771                return;
9772            }
9773
9774            var couponId = $scope.getResponseDataCouponId(data);
9775            if (!couponId) {
9776                if (window.console && window.console.info) {
9777                    console.info("Coupon found but ID not returned - Code:" + code);
9778                }
9779
9780                return;
9781            }
9782
9783            $scope.StatusInfo.CouponError = responseError;
9784            $scope.CartDataAdapt(data);
9785
9786            if ($scope.giftCardOrCouponRemovalMap.hasOwnProperty(couponId)) {
9787                if (window.console && window.console.info) {
9788                    console.info("Coupon found but can't be used - Coupon ID:" + couponId);
9789                }
9790                $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9791                return;
9792            }
9793
9794            $scope.clearGCCouponCodeTextBox();
9795            $scope.ShippingOptionsGet(true);
9796
9797            $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9798            $scope.showGiftCardCouponMessage('Coupon Added');
9799        }, function errorCallback(response) {
9800            alert('Gor some error');
9801        })
9802    };
9803
9804    $scope.getGiftCardOrCoupon = function () {
9805        $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADING;
9806        $scope.giftCardOrCoupon = [];
9807
9808        var code = $scope.StatusInfo.giftCardOrCouponCode.replace('CPN**', '').replace('cpn**', '');
9809        var params = {
9810            gc_coupon_code: code
9811        };
9812
9813        var couponIsInUse = $scope.CartData.Coupons.some((coupon) => {
9814            return coupon.couponCode === code;
9815        });
9816
9817        if (couponIsInUse) {
9818            $scope.showGiftCardCouponMessage('Coupon ' + code + ' is already in use');
9819            $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9820
9821            return;
9822        }
9823
9824        var url = $scope.getGiftCardOrCouponApiUrl();
9825
9826        var requestGiftCard = () => {
9827            return $http.get(url, {params: params, withCredentials: true});
9828        };
9829
9830        if ($scope.isCarePlanEnable()) {
9831            requestGiftCard = () => {
9832                return $scope.blockUIWhileCallback(() => {
9833                    return $http.get(url, {params: params, withCredentials: true});
9834                });
9835            };
9836        }
9837
9838        requestGiftCard().then(function successCallback(response) {
9839                var data = null;
9840
9841                if (typeof response.data !== typeof undefined) {
9842                    data = response.data;
9843                }
9844
9845                var couponFound = false;
9846                if (typeof data.coupon_code_found !== typeof undefined) {
9847                    var couponCodeFound = data.coupon_code_found;
9848                    if (couponCodeFound === code) {
9849                        couponFound = true;
9850                    }
9851                }
9852
9853                var gcFound = false;
9854                if (typeof data.gc_code_found !== typeof undefined) {
9855                    var gcCodeFound = data.gc_code_found;
9856                    if (gcCodeFound === code) {
9857                        gcFound = true;
9858                    }
9859                }
9860
9861                var responseError = null;
9862                if (typeof data.error !== typeof undefined) {
9863                    responseError = data.error;
9864                    if (responseError === 'already-added') {
9865                        $scope.clearGCCouponCodeTextBox();
9866                        $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9867                        $scope.showGiftCardCouponMessage('Gift Card or Coupon ' + code + ' has already been applied');
9868                        return;
9869                    }
9870                }
9871
9872                if (couponFound) {
9873                    var couponId = $scope.getResponseDataCouponId(data);
9874                    if (!couponId) {
9875                        if (window.console && window.console.info) {
9876                            console.info("Coupon found but ID not returned - Code:" + code);
9877                        }
9878                        var errorMsg = 'Gift Card / Coupon Error';
9879                        if (data && data.error) {
9880                            errorMsg = data.error;
9881                        }
9882
9883                        $scope.focusGCCouponCodeTextBox();
9884                        $scope.showGiftCardCouponMessage(errorMsg);
9885                        $scope.giftCardOrCouponDataState = $scope.DATA_STATE_ERROR;
9886                        return;
9887                    }
9888
9889                    $scope.StatusInfo.CouponError = responseError;
9890                    if ((responseError === null) || (responseError === '')) {
9891                        $scope.CartDataAdapt(data);
9892                        //todo use it
9893                        if ($scope.checkFlagCarePlans()) {
9894                            $scope.updateCarePlanPrice();
9895                        }
9896                    }
9897                    if ($scope.giftCardOrCouponRemovalMap.hasOwnProperty(couponId)) {
9898                        if (window.console && window.console.info) {
9899                            console.info("Coupon found but can't be used - Coupon ID:" + couponId);
9900                        }
9901                        $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9902                        return;
9903                    }
9904
9905                    $scope.clearGCCouponCodeTextBox();
9906                    $scope.ShippingOptionsGet(true);
9907
9908                    $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9909                    $scope.showGiftCardCouponMessage('Coupon Added');
9910                } else if (gcFound) {
9911                    $scope.clearGCCouponCodeTextBox();
9912
9913                    $scope.StatusInfo.GiftCardError = responseError;
9914                    if ((responseError === null) || (responseError === '')) {
9915                        $scope.CartDataAdapt(data);
9916                        if ($scope.checkFlagCarePlans()) {
9917                            $scope.updateCarePlanPrice();
9918                        }
9919                    }
9920
9921                    $scope.giftCardOrCouponDataState = $scope.DATA_STATE_LOADED;
9922                    $scope.showGiftCardCouponMessage('Gift Card Applied');
9923                } else {
9924                    if (window.console && window.console.error) {
9925                        console.error("Gift card / coupon error - Code: " + code, response);
9926                    }
9927
9928                    $scope.focusGCCouponCodeTextBox();
9929                    $scope.showGiftCardCouponMessage('Gift card / coupon error');
9930                    $scope.giftCardOrCouponDataState = $scope.DATA_STATE_ERROR;
9931                }
9932            }, function errorCallback(response) {
9933                var errorMsg = 'Unknown Error';
9934                if ((typeof response.data !== typeof undefined) && (typeof response.data.error !== typeof undefined)) {
9935                    errorMsg = response.data.error;
9936                }
9937                if (window.console && window.console.info) {
9938                    console.info("Invalid Gift Card or Coupon Code:" + code, errorMsg);
9939                }
9940
9941                $scope.focusGCCouponCodeTextBox();
9942                $scope.showGiftCardCouponMessage(response.data);
9943                $scope.giftCardOrCouponDataState = $scope.DATA_STATE_ERROR;
9944            })
9945    };
9946
9947    $scope.getResponseDataCouponId = function (data) {
9948        try {
9949            var couponId = data.cartdata.Coupons[0].couponId;
9950            return couponId;
9951        } catch (e) {
9952            return null;
9953        }
9954    };
9955
9956    $scope.getGiftCardOrCouponApiUrl = function () {
9957        var url = $scope.BuildUrl('/checkout/get-gc-coupon');
9958        return url;
9959    };
9960
9961    $scope.removeCoupon = function (couponId, errorMsg) {
9962        if (!$scope.checkFlagShowCouponErrorMessageOnCheckoutPage()) {
9963            $scope.showGiftCardCouponMessage('');
9964        }
9965
9966        if ($scope.giftCardOrCouponRemovalMap.hasOwnProperty(couponId)) {
9967            if (window.console && window.console.log) {
9968                console.info("Skipping Coupon Removal - Already In Progress - Coupon ID:" + couponId);
9969            }
9970            return;
9971        }
9972
9973        if ($scope.checkFlagShowCouponErrorMessageOnCheckoutPage()) {
9974            $scope.showGiftCardCouponMessage('');
9975        }
9976        $scope.giftCardOrCouponRemovalMap[couponId] = true;
9977
9978        if (errorMsg) {
9979            if (window.console && window.console.log) {
9980                console.info("removeCoupon - Setting error msg: " + errorMsg);
9981            }
9982
9983            if ($scope.checkFlagShowCouponErrorMessageOnCheckoutPage()) {
9984                var message = errorMsg;
9985
9986                var removedCoupon = ($scope.CartData.Coupons || []).find(function (c) {
9987                    return c.couponId == couponId;
9988                });
9989                var couponCode = removedCoupon ? (removedCoupon.couponCode || removedCoupon.number) : '';
9990                message = couponCode ? 'Coupon ' + couponCode + ' was removed: ' + errorMsg : errorMsg;
9991
9992                $scope.focusGCCouponCodeTextBox();
9993                $scope.showGiftCardCouponMessage(message);
9994            } else {
9995                $scope.focusGCCouponCodeTextBox();
9996                $scope.showGiftCardCouponMessage(errorMsg);
9997            }
9998        }
9999
10000        var params = {
10001            coupon_id: couponId
10002        };
10003
10004        $scope.StatusInfo.CouponUpdating = true;
10005
10006        var requestRemoveCoupon = () => {
10007            return $http.get($scope.BuildUrl('/checkout/remove-coupon'), {
10008                params: params,
10009                withCredentials: true
10010            });
10011        };
10012
10013        if ($scope.isCarePlanEnable()) {
10014            requestRemoveCoupon = () => {
10015                return $scope.blockUIWhileCallback(() => {
10016                    return $http.get($scope.BuildUrl('/checkout/remove-coupon'), {
10017                        params: params,
10018                        withCredentials: true
10019                    });
10020                });
10021            };
10022        }
10023
10024        requestRemoveCoupon().then(function (result) {
10025            delete $scope.giftCardOrCouponRemovalMap[couponId];
10026            if (window.console && window.console.log) {
10027                console.info("removeCoupon - Removed coupon - Coupon ID:" + couponId);
10028            }
10029
10030            $scope.StatusInfo.CouponUpdating = false;
10031
10032            $scope.CartDataAdapt(result.data);
10033            if ($scope.checkFlagCarePlans()) {
10034                $scope.updateCarePlanPrice();
10035            }
10036            $scope.ShippingOptionsGet(true);
10037        });
10038    };
10039
10040    $scope.removeGiftCard = function (gcId) {
10041        var params =
10042            {
10043                gc_id: gcId
10044            };
10045
10046        $scope.StatusInfo.GiftCardUpdating = true;
10047
10048        var requestRemoveGiftCard = () => {
10049            return $http.get($scope.BuildUrl('/checkout/remove-gc'), {
10050                params: params,
10051                withCredentials: true
10052            });
10053        };
10054
10055        if ($scope.isCarePlanEnable()) {
10056            requestRemoveGiftCard = () => {
10057                return $scope.blockUIWhileCallback(() => {
10058                    return $http.get($scope.BuildUrl('/checkout/remove-gc'), {
10059                        params: params,
10060                        withCredentials: true
10061                    });
10062                });
10063            };
10064        }
10065
10066        requestRemoveGiftCard().then(function (result) {
10067            $scope.StatusInfo.GiftCardUpdating = false;
10068
10069            $scope.CartDataAdapt(result.data);
10070            if ($scope.StatusInfo.Processor === 'gc') {
10071                $scope.StatusInfo.Processor = null;
10072            }
10073        });
10074    };
10075
10076    // endregion
10077
10078    // ******************************************************************************************
10079    // region Gift Message
10080    // ******************************************************************************************
10081
10082    $scope.GiftMessageEdit = function () {
10083        if ($scope.GiftMessage.Checked) {
10084            $scope.GiftMessage.Show = true;
10085        } else {
10086            $scope.GiftMessage.Show = !$scope.GiftMessage.Show;
10087        }
10088    };
10089
10090    $scope.isProductInCart = function () {
10091        return $scope.CartData.Items && $scope.CartData.Items.length ? $scope.CartData.Items.some(function (item) {
10092            return item.type === 'pslr';
10093        }) : false;
10094
10095    }
10096
10097    $scope.isFlagInsuranceEnabled = function () {
10098        return !!$scope.knobby && $scope.knobby.module_jm_insurance;
10099    }
10100
10101    $scope.isFlagLuxsuranceEnabled = function () {
10102        return !!$scope.knobby && $scope.knobby.module_luxsurance_functionality;
10103    }
10104
10105    $scope.isFlagSaveLuxsuranceInsuranceInfoAfterReloadingEnabled = function () {
10106        return !!$scope.knobby && $scope.knobby.module_save_luxsurance_insurance_info_after_reloading;
10107    }
10108
10109    $scope.handleInsuranceCheckbox = function () {
10110        document.getElementById('isInsuranceSwitchedOn').checked = false;
10111        if ($scope.isInsuranceSwitchedOn) {
10112            $scope.isInsuranceSwitchedOn = false;
10113            if ($scope.isFlagSaveLuxsuranceInsuranceInfoAfterReloadingEnabled()) {
10114                $scope.setInsuranceInfoToLocalStorage();
10115            }
10116        } else {
10117            $scope.openJmQuoteContactModal({isInsuranceSwitchedOn: true});
10118        }
10119
10120        $scope.safeApply();
10121    }
10122
10123    $scope.handleLuxsuranceCheckbox = function () {
10124        document.getElementById('isLuxsuranceSwitchedOn').checked = false;
10125        if ($scope.isLuxsuranceSwitchedOn) {
10126            $scope.isLuxsuranceSwitchedOn = false;
10127            if ($scope.isFlagSaveLuxsuranceInsuranceInfoAfterReloadingEnabled()) {
10128                $scope.setInsuranceInfoToLocalStorage();
10129            }
10130        } else {
10131            $scope.openJmQuoteContactModal({isLuxsuranceSwitchedOn: true});
10132        }
10133
10134        $scope.safeApply();
10135    }
10136
10137    $scope.hasContactInformationConfirmed = function () {
10138        return JSON.stringify($scope.contactJMInsuranceInfo) !== '{}';
10139    }
10140
10141    $scope.showContactInformationConfirmModal = function () {
10142        var dataForContactInformationConfirmModal = {};
10143        if (JSON.stringify($scope.contactJMInsuranceInfo) !== '{}') {
10144            dataForContactInformationConfirmModal = {
10145                contactInfo: $scope.contactJMInsuranceInfo,
10146                isInsuranceSwitchedOn: $scope.isInsuranceSwitchedOn,
10147                isLuxsuranceSwitchedOn: $scope.isLuxsuranceSwitchedOn,
10148            };
10149        }
10150
10151        $scope.openJmQuoteContactModal({isViewMode: true, ...dataForContactInformationConfirmModal});
10152    }
10153
10154    $scope.getDataForJmQuoteContactModal = function () {
10155        if (JSON.stringify($scope.contactJMInsuranceInfo) !== '{}') {
10156            return ({
10157                contactInfo: $scope.contactJMInsuranceInfo,
10158                isInsuranceSwitchedOn: $scope.isInsuranceSwitchedOn,
10159                isLuxsuranceSwitchedOn: $scope.isLuxsuranceSwitchedOn,
10160            });
10161        } else if (
10162            $scope.ShippingInfo &&
10163            (
10164                $scope.ShippingInfo.CountryKey === 'USA' ||
10165                $scope.ShippingInfo.CountryKey === 'CAN'
10166            )
10167        ) {
10168            return ({
10169                contactInfo: {
10170                    name_first: $scope.ShippingInfo.FirstName || '',
10171                    name_last: $scope.ShippingInfo.LastName || '',
10172                    email: $scope.BillingInfo.Email || '',
10173                    phone: $scope.BillingInfo.Phone || '',
10174                    country: $scope.ShippingInfo.CountryKey || '',
10175                    address: $scope.ShippingInfo.Address1 || '',
10176                    address2: $scope.ShippingInfo.Address2 || '',
10177                    city: $scope.ShippingInfo.City || '',
10178                    state: $scope.ShippingInfo.State || '',
10179                    zip: $scope.ShippingInfo.Zip || '',
10180                },
10181                isInsuranceSwitchedOn: $scope.isInsuranceSwitchedOn,
10182                isLuxsuranceSwitchedOn: $scope.isLuxsuranceSwitchedOn,
10183            });
10184        }
10185
10186        return {};
10187    };
10188
10189    $scope.openJmQuoteContactModal = function (dataForContactInformationConfirmModal = {}) {
10190        var isTransactionCompleted = false;
10191        var isConfirmed = $scope.confirmedJMInsurance;
10192        window.vueEventHub.$eventHub.$emit('jm-quote-contact-modal-open', {
10193            ...$scope.getDataForJmQuoteContactModal(),
10194            ...dataForContactInformationConfirmModal,
10195            isTransactionCompleted,
10196            isConfirmed,
10197            isFlagInsuranceEnabled: $scope.isFlagInsuranceEnabled(),
10198            isFlagLuxsuranceEnabled: $scope.isFlagLuxsuranceEnabled(),
10199        });
10200    }
10201
10202    $scope.openCouponDetailsModal = function (coupon) {
10203        if ($scope.knobby['module_better_coupons']) {
10204            coupon.better_coupons_flag = true;
10205        }
10206        window.vueEventHub.$eventHub.$emit('coupon-details-modal-open', { coupon });
10207    }
10208
10209
10210    $scope.GiftMessageAdd = function () {
10211        $scope.GiftMessage.Show = false;
10212
10213    };
10214
10215    // endregion
10216
10217    // ******************************************************************************************
10218    // region Session
10219    // ******************************************************************************************
10220
10221    $scope.SessionUpdate = function () {
10222        var data =
10223            {
10224                ShippingInfo: $scope.ShippingInfo,
10225                BillingInfo: $scope.BillingInfo
10226            };
10227
10228        $http.post($scope.BuildUrl('/checkout/session'), data);
10229    };
10230
10231    // endregion
10232
10233    // ******************************************************************************************
10234    // ****  Errors
10235    // ******************************************************************************************
10236
10237    $scope.ErrorMessageClose = function () {
10238        $scope.StatusInfo.ErrorMessage = null;
10239    };
10240
10241    $scope.ErrorMessageBottomClose = function () {
10242        $scope.StatusInfo.ErrorMessageBottom = null;
10243    };
10244
10245    $scope.InventoryMessagesClose = function () {
10246        $scope.InventoryMessages = {};
10247    }
10248
10249    $scope.AvailabilityMessagesClose = function () {
10250        $scope.AvailabilityMessages = {};
10251    };
10252
10253    $scope.getItemUnitType = function (item) {
10254        if (item.unit_type == 'each') {
10255            return '';
10256        }
10257
10258        if (item.unit_type == 'per yard') {
10259            return item.measure_unit == 'Meter' ? 'Metres' : (item.measure_unit + '(s)');
10260        }
10261
10262        return item.unit_type + '(s)';
10263    };
10264
10265    $scope.isDynamicTaxServiceEnabled = function () {
10266        return $scope.knobby && $scope.tax_service;
10267    }
10268
10269    $scope.getShippingDiscount = function () {
10270        var coupon = $scope.getFreeShippingCoupon();
10271
10272        if (!coupon || !$scope.ShippingTotal) {
10273            return 0;
10274        }
10275
10276        var shippingAmount = +$scope.ShippingTotal;
10277        var couponMaxDiscount = parseFloat(coupon.maxDiscount) ? parseFloat(coupon.maxDiscount) : shippingAmount;
10278
10279        if (shippingAmount <= couponMaxDiscount) {
10280            return shippingAmount;
10281        } else {
10282            return couponMaxDiscount;
10283        }
10284    }
10285
10286    $scope.getFreeShippingCoupon = function () {
10287        var coupons = $scope.CartData.Coupons || [];
10288
10289        for (var c = 0; c < coupons.length; c++) {
10290            var coupon = coupons[c];
10291
10292            if (coupon.couponType === 'free_shipping_off') {
10293                return coupon
10294            }
10295        }
10296
10297        return null;
10298    }
10299
10300    $scope.getShippingWithDiscount = function() {
10301        var shipping = $scope.ShippingInfo.Option ? $scope.ShippingInfo.Option.price : 0.00;
10302
10303        if (shipping <= 0) {
10304            return shipping;
10305        }
10306
10307        return shipping - $scope.getShippingDiscount();
10308    };
10309
10310    $scope.getLargestDimensions = function() {
10311        var dimensions = {};
10312
10313        if ($scope.CartData && $scope.CartData.Items && $scope.CartData.Items.length > 0) {
10314            var items = $scope.CartData.Items;
10315            items.sort(function (a, b) {
10316                if ((a.dimensions && Object.keys(a.dimensions).length > 0) && (b.dimensions && Object.keys(b.dimensions).length > 0)) {
10317                    var aVolume = parseInt(a.dimensions.length) * parseInt(a.dimensions.height) * parseInt(a.dimensions.width);
10318                    var bVolume = parseInt(b.dimensions.length) * parseInt(b.dimensions.height) * parseInt(b.dimensions.width);
10319
10320                    return bVolume - aVolume;
10321                }
10322
10323                return 0;
10324            });
10325
10326            if (items[0].dimensions && Object.keys(items[0].dimensions).length > 0) {
10327                dimensions = items[0].dimensions;
10328            }
10329        }
10330        return dimensions;
10331    };
10332
10333    $scope.getCurrentTaxProfileId = function () {
10334        if ($scope.HasDynamicTaxes && $scope.isDynamicTaxServiceEnabled()) {
10335            return $scope.tax_service;
10336        }
10337
10338        var useStoreTaxTable = $scope.isPickup() ? true : $scope.taxAtCountryLevel();
10339
10340        var state = $scope.ShippingInfo.State;
10341        if (useStoreTaxTable) {
10342            if ($scope.isPickup() && $scope.ShippingInfo && $scope.ShippingInfo.Option && $scope.ShippingInfo.Option.address && $scope.ShippingInfo.Option.address.state) {
10343                state = $scope.ShippingInfo.Option.address.state;
10344            } else if ($scope.InitInfo.Store) {
10345                state = $scope.InitInfo.Store.state;
10346            }
10347        }
10348
10349        var taxTable = $scope.InitInfo.TaxableRegions[state] || null;
10350
10351        if (!taxTable) {
10352            return null;
10353        }
10354
10355        return taxTable.tax_profile || null;
10356    };
10357
10358    // OSR
10359
10360    $scope.getOsrItem = function () {
10361        if (!$scope.CartData || !$scope.CartData.Items) {
10362            return null;
10363        }
10364
10365        return  $scope.CartData.Items.find(function (item) {
10366            return item.type === 'rto' && Object.hasOwn(item, 'osrData');
10367        }) || null;
10368    };
10369
10370    $scope.getOsrData = function () {
10371        var item = $scope.getOsrItem();
10372
10373        return item ? item.osrData : null;
10374    };
10375
10376    $scope.hasOsrItem = function () {
10377        return !!$scope.getOsrData();
10378    };
10379
10380    $scope.isOsrShippingEnabled = function () {
10381        var data = $scope.getOsrData();
10382
10383        return data ? data.allow_shipping : false;
10384    };
10385
10386    $scope.isOsrStorePickupEnabled = function () {
10387        var data = $scope.getOsrData();
10388
10389        return data ? data.allow_store_pickup : false;
10390    };
10391
10392    $scope.isOsrSchoolPickupEnabled = function () {
10393        var data = $scope.getOsrData();
10394
10395        return data ? data.allow_school_pickup : false;
10396    };
10397
10398    $scope.getOsrAvailablePslrIds = function () {
10399        var data = $scope.getOsrData();
10400
10401        return data ? data.available_pslr_ids : [];
10402    };
10403
10404    $scope.isOsrSchoolPickupAvailable = function () {
10405        if (!$scope.isOsrSchoolPickupEnabled()) {
10406            return false;
10407        }
10408
10409        var osrItem = $scope.getOsrItem();
10410        var availablePslrIds = $scope.getOsrAvailablePslrIds();
10411
10412        return $scope.CartData.Items.every(function (item) {
10413            if (item.id === osrItem.id && item.type === osrItem.type) {
10414                return true;
10415            }
10416
10417            return item.type === 'pslr' && availablePslrIds.includes(+item.pslr_id);
10418        });
10419    };
10420
10421    $scope.isRtoOrOsr = function () {
10422        return $scope.hasOsrItem() || $scope.isRtoItemExist();
10423    };
10424
10425    $scope.onDistrictChange = function () {
10426        if ($scope.ShippingInfo.selectedDistrict) {
10427            $scope.schools = $scope.allSchools.filter(school => school.district_id === $scope.ShippingInfo.selectedDistrict.id);
10428            $scope.ShippingInfo.selectedSchool = null;
10429        } else {
10430            $scope.schools = [];
10431            $scope.ShippingInfo.selectedSchool = null;
10432            $scope.makeShippingAddressFieldsEditable();
10433        }
10434    };
10435
10436    $scope.makeShippingAddressFieldsEditable = function () {
10437        $scope.ShippingInfoPrefilledFlags = {
10438            Address1: false,
10439            Address2: false,
10440            City: false,
10441            State: false,
10442            Zip: false,
10443            Country: false
10444        };
10445    };
10446
10447    $scope.fillShippingAddressFromSchool = function (school) {
10448        if (!school) {
10449            return;
10450        }
10451
10452        $scope.ShippingInfo.Address1 = school.address || '';
10453        $scope.ShippingInfoPrefilledFlags.Address1 = !!school.address;
10454
10455        $scope.ShippingInfo.Address2 = school.address2 || '';
10456        $scope.ShippingInfoPrefilledFlags.Address2 = !!school.address2;
10457
10458        $scope.ShippingInfo.City = school.city || '';
10459        $scope.ShippingInfoPrefilledFlags.City = !!school.city;
10460
10461        $scope.ShippingInfo.State = school.state || '';
10462        $scope.ShippingInfoPrefilledFlags.State = !!school.state;
10463
10464        $scope.ShippingInfo.Zip = school.zip || '';
10465        $scope.ShippingInfoPrefilledFlags.Zip = !!school.zip;
10466
10467        $scope.ShippingInfoPrefilledFlags.Country = !!school.country;
10468
10469        $scope.ShippingOptionsGet();
10470    };
10471
10472    $scope.$watch('ShippingInfo.selectedSchool', function (newSchool) {
10473        $scope.makeShippingAddressFieldsEditable();
10474        if (newSchool && $scope.ShippingInfo.shipToSchool) {
10475            $scope.fillShippingAddressFromSchool(newSchool);
10476        }
10477    });
10478
10479    $scope.$watch('ShippingInfo.shipToSchool', function () {
10480        $scope.ShippingInfo.selectedSchool = null;
10481        $scope.ShippingInfo.selectedDistrict = null;
10482        if ($scope.ShippingInfo.shipToSchool) {
10483            $scope.HideBillingSameAsShipping = true;
10484            $scope.BillingInfo.AddressSameAsShipping = false;
10485        } else {
10486            $scope.HideBillingSameAsShipping = false;
10487            $scope.BillingInfo.AddressSameAsShipping = true;
10488        }
10489        $scope.makeShippingAddressFieldsEditable();
10490    });
10491
10492    $scope.intervalPromise = $interval(function() {
10493        if (!!$scope.knobby && Object.keys($scope.knobby).length > 0 && window.vueEventHub && window.vueEventHub.$eventHub) {
10494            $scope.registerEventHubEvents();
10495            $interval.cancel($scope.intervalPromise);
10496        }
10497    }, 1000);
10498
10499    $scope.setInsuranceInfoToLocalStorage = function () {
10500        var insuranceInfo = JSON.stringify(
10501            {
10502                isInsuranceSwitchedOn: $scope.isInsuranceSwitchedOn,
10503                isLuxsuranceSwitchedOn: $scope.isLuxsuranceSwitchedOn,
10504                contactJMInsuranceInfo: $scope.contactJMInsuranceInfo,
10505                confirmedJMInsurance: $scope.confirmedJMInsurance,
10506            }
10507        );
10508
10509        window.localStorage.setItem('insurance_info', insuranceInfo);
10510    };
10511
10512    $scope.setInsuranceInfo = function () {
10513        var insuranceInfo = window.localStorage.getItem('insurance_info');
10514        if (!insuranceInfo || typeof insuranceInfo !== 'string') {
10515            return;
10516        }
10517
10518        insuranceInfo = JSON.parse(insuranceInfo)
10519
10520        $scope.isInsuranceSwitchedOn = insuranceInfo.isInsuranceSwitchedOn;
10521        $scope.isLuxsuranceSwitchedOn = insuranceInfo.isLuxsuranceSwitchedOn;
10522        $scope.contactJMInsuranceInfo = insuranceInfo.contactJMInsuranceInfo;
10523        $scope.confirmedJMInsurance = insuranceInfo.customerIdForJMInsurance;
10524        if (document.getElementById('isInsuranceSwitchedOn')) {
10525            document.getElementById('isInsuranceSwitchedOn').checked = $scope.isInsuranceSwitchedOn;
10526        }
10527
10528        if (document.getElementById('isLuxsuranceSwitchedOn')) {
10529            document.getElementById('isLuxsuranceSwitchedOn').checked = $scope.isLuxsuranceSwitchedOn;
10530        }
10531
10532        $scope.safeApply();
10533    };
10534
10535    $scope.registerEventHubEvents = function () {
10536        if ($scope.isFlagInsuranceEnabled() || $scope.isFlagLuxsuranceEnabled()) {
10537            window.vueEventHub.$eventHub.$on('jm-quote-contact-modal-cancel', function () {
10538                $scope.confirmedJMInsurance = false;
10539                if (!$scope.isFlagLuxsuranceEnabled() && !$scope.isFlagInsuranceEnabled()) {
10540                    $scope.contactJMInsuranceInfo = {};
10541                }
10542            });
10543
10544            window.vueEventHub.$eventHub.$on('jm-quote-contact-modal-confirm', function (data) {
10545                $scope.contactJMInsuranceInfo = data.contactJMInsuranceInfo;
10546                $scope.confirmedJMInsurance = true;
10547                $scope.isInsuranceSwitchedOn = data.isInsuranceSwitchedOn;
10548                $scope.isLuxsuranceSwitchedOn = data.isLuxsuranceSwitchedOn;
10549
10550                if ($scope.isFlagSaveLuxsuranceInsuranceInfoAfterReloadingEnabled()) {
10551                    $scope.setInsuranceInfoToLocalStorage();
10552                }
10553
10554                if (document.getElementById('isInsuranceSwitchedOn')) {
10555                    document.getElementById('isInsuranceSwitchedOn').checked = data.isInsuranceSwitchedOn;
10556                }
10557
10558                if (document.getElementById('isLuxsuranceSwitchedOn')) {
10559                    document.getElementById('isLuxsuranceSwitchedOn').checked = data.isLuxsuranceSwitchedOn;
10560                }
10561
10562                $scope.safeApply();
10563            });
10564        }
10565    };
10566
10567    $scope.getFormattedExpirationDate = function (date) {
10568        if (!date) {
10569            return '';
10570        }
10571
10572        var month = date.slice(0, 2);
10573        var year = 2000 + parseInt(date.slice(2, 4), 10);
10574
10575        var formatter = new Intl.DateTimeFormat('en-US', {
10576            year: 'numeric',
10577            month: '2-digit',
10578            day: '2-digit'
10579        });
10580
10581        return formatter.format(new Date(year, month, 0));
10582    };
10583
10584    $scope.onSelectSavedCard = function (card) {
10585        $scope.paymentMode = 'saved';
10586        $scope.formData.selectedCustomerCardId = card.pos_customer_card_id;
10587    };
10588
10589    $scope.isSavedCardsFlagEnabled = function () {
10590        return $scope.knobby && $scope.knobby.module_saved_cards_at_checkout;
10591    };
10592
10593    $scope.getFormattedExp = function(card) {
10594        if (!card?.exp_month || !card?.exp_year) {
10595            return '';
10596        }
10597
10598        const month = String(card.exp_month).padStart(2, '0');
10599
10600        return `(${month}/${card.exp_year})`;
10601    };
10602
10603    $scope.isSavedCardsAllowed = function () {
10604        return $scope.isSavedCardsFlagEnabled()
10605            && $scope.customerCards?.length
10606            && !$scope.paymentType
10607            && ($scope.isCardProcessorRain() || $scope.isCardProcessorQuiltAdyen());
10608    };
10609
10610    $scope.getCard = function () {
10611        return $scope.customerCards?.find(c => c.pos_customer_card_id === $scope.formData.selectedCustomerCardId) || [];
10612    };
10613
10614    $scope.$on('$destroy', function() {
10615        $interval.cancel($scope.intervalPromise);
10616    });
10617}]);
10618
10619checkoutApp.directive('capitalizeFirst', function ($parse) {
10620    return {
10621        require: 'ngModel',
10622        link: function ($scope, $element, $attrs, $ctrl) {
10623            var capitalize = function (inputValue) {
10624                if (!inputValue) {
10625                    inputValue = '';
10626                }
10627
10628                return inputValue.replace(/\w\S*/g, function (txt) {
10629                    return txt.charAt(0).toUpperCase() + txt.substring(1);
10630                });
10631            };
10632
10633            // Handle changes from the user
10634            var listener = function () {
10635                var value = $element.val();
10636                $element.val(capitalize(value));
10637            };
10638            $element.bind('change', listener);
10639
10640            // Handles changes from the model
10641            $ctrl.$formatters.unshift(function () {
10642                return capitalize($ctrl.$modelValue);
10643            });
10644        }
10645    };
10646});
10647
10648checkoutApp.directive('paymentMethods', function () {
10649    var controller = ['$scope', function ($scope) {
10650        $scope.paymentMethods = {
10651            creditCard: 'cc',
10652            affirm: 'affirm',
10653            klarna: 'klarna',
10654            paypal: 'paypal',
10655            afterpay: 'afterpay',
10656            pickup: 'pickup'
10657        };
10658
10659        $scope.paymentMethod = $scope.paymentMethods.creditCard;
10660
10661        $scope.isBNPLPaymentMethodEnabled = function (type) {
10662            if (type === $scope.paymentMethods.affirm && !$scope.isAffirmEnabled) {
10663                return false;
10664            }
10665
10666            if (type === $scope.paymentMethods.klarna && !$scope.isKlarnaEnabled) {
10667                return false;
10668            }
10669
10670            if (type === $scope.paymentMethods.afterpay && !$scope.isAfterpayEnabled) {
10671                return false;
10672            }
10673
10674            return $scope.isBNPLPaymentMethodMerchantCountrySupported(type);
10675        };
10676
10677        $scope.isBNPLPaymentMethodMerchantCountrySupported = function (type) {
10678            if (!$scope.model) {
10679                return false;
10680            }
10681
10682            return $scope.model.getModel(type).isSupportedMerchantCountry($scope.merchantCountry);
10683        };
10684
10685        $scope.isBNPLPaymentMethodAmountSupported = function (type) {
10686            if (!$scope.model) {
10687                return false;
10688            }
10689
10690            return $scope.model.getModel(type).isSupportedAmount($scope.amount, $scope.currency, $scope.country);
10691        };
10692
10693        $scope.isBNPLPaymentMethodCurrencySupported = function (type) {
10694            if (!$scope.model) {
10695                return false;
10696            }
10697
10698            return $scope.model.getModel(type).isSupportedCurrency($scope.currency, $scope.country);
10699        };
10700
10701        $scope.isBNPLPaymentMethodSupported = function (type) {
10702            if (!$scope.model) {
10703                return false;
10704            }
10705
10706            return $scope.model.getModel(type).isSupported($scope.amount, $scope.currency, $scope.country, $scope.merchantCountry);
10707        };
10708
10709        $scope.supportedAmountsByBNPLPaymentMethod = function (type) {
10710            if (!$scope.model) {
10711                return null;
10712            }
10713
10714            var formatter = new Intl.NumberFormat('en-US', {
10715                style: 'currency',
10716                currency: $scope.currency,
10717            });
10718
10719            var amounts = $scope.model.getModel(type).getSupportedAmounts($scope.country, $scope.currency);
10720
10721            if (!amounts) {
10722                return null;
10723            }
10724
10725            return {
10726                min: formatter.format(amounts.minAmount ? amounts.minAmount / 100 : 0),
10727                max: formatter.format(amounts.maxAmount ? amounts.maxAmount / 100 : 0),
10728            };
10729        };
10730
10731        $scope.checkoutOldCart = function () {
10732            if (!window?.knobby.module_update_to_captcha_v3) {
10733                $scope.CheckoutProcess();
10734
10735                return;
10736            }
10737
10738            return $scope.checkCaptcha().then(function() {
10739                if (!$scope.showCaptcha) {
10740                    return $scope.CheckoutProcess();
10741                }
10742            });
10743        };
10744
10745        $scope.init = function () {
10746            $scope.$watch('paymentMethod', function (paymentMethod) {
10747                $scope.onPaymentMethodChanged(paymentMethod);
10748            });
10749        };
10750
10751        $scope.init();
10752
10753        if (typeof $scope.onReady === 'function') {
10754            $scope.onReady();
10755        }
10756    }];
10757
10758    return {
10759        restrict: 'E',
10760        controller: controller,
10761        scope: {
10762            isKlarnaEnabled: '=',
10763            isAffirmEnabled: '=',
10764            isAfterpayEnabled: '=',
10765            isPaypalEnabled: '=',
10766            isPayAtPickUpEnabled: '=',
10767            amount: '=',
10768            due: '=',
10769            currency: '=',
10770            currencySymbol:'=',
10771            country: '=',
10772            merchantCountry: '=',
10773            model: '=',
10774            onPaymentMethodChanged: '=',
10775            onReady: '=',
10776        },
10777        templateUrl: '/checkout/partial/paymentMethods.html',
10778    };
10779});
10780
10781checkoutApp.directive('billingAddress', function () {
10782    var controller = ['$scope', function ($scope) {
10783        $scope.billingAddressOptions = {
10784            same: 'same',
10785            different: 'different',
10786        };
10787        $scope.billingAddressOption = $scope.billingAddressOptions.same;
10788        $scope.checkoutCtrl = $scope.$parent.$parent;
10789
10790        $scope.init = function () {
10791            $scope.$watch('billingAddressOption', function (billingAddressOption) {
10792                $scope.checkoutCtrl.BillingInfo.AddressSameAsShipping = billingAddressOption === $scope.billingAddressOptions.same;
10793
10794                if (!$scope.checkoutCtrl.StatusInfo.Initializing) {
10795                    $scope.checkoutCtrl.BillingVerify();
10796                }
10797            });
10798
10799            $scope.$watch('showBillingAddress', function (showBillingAddress) {
10800                if (showBillingAddress) {
10801                    $scope.billingAddressOption = $scope.billingAddressOptions.different;
10802                }
10803            });
10804        };
10805
10806        $scope.init();
10807    }];
10808
10809    return {
10810        restrict: 'E',
10811        controller: controller,
10812        scope: {
10813            showBillingAddress: '=',
10814            isShippingRequired: '=',
10815        },
10816        templateUrl: '/checkout/partial/billingAddress.html',
10817    };
10818});
10819
10820checkoutApp.directive('phone', function () {
10821    var validator = function (phone) {
10822        if (!phone) {
10823            return true;
10824        }
10825
10826        // Simple for now, we may care to do better later
10827        var result = phone.match(/\d/g);
10828
10829        if (!result) {
10830            return false;
10831        }
10832
10833        result = result.length >= 10;
10834
10835        if (result) {
10836            result = !/([A-Za-z])/.test(phone);
10837        }
10838
10839        return result;
10840    };
10841
10842    return {
10843        restrict: 'A',
10844        require: 'ngModel',
10845        link: function(scope, elem, attr, ngModel) {
10846            //For DOM -> model validation
10847            ngModel.$parsers.unshift(function(value) {
10848                var valid = validator(value);
10849
10850                ngModel.$setValidity('phone', valid);
10851
10852                return valid ? value : undefined;
10853            });
10854
10855            //For model -> DOM validation
10856            ngModel.$formatters.unshift(function(value) {
10857                var valid = validator(value);
10858
10859                ngModel.$setValidity('phone', valid);
10860
10861                return value;
10862            });
10863        }
10864    };
10865});
10866
10867checkoutApp.directive('streetAddress', function () {
10868    function shouldValidate(scope) {
10869        try {
10870            return !!(
10871                (
10872                    (scope.isQuiltAdyen && scope.isQuiltAdyen())
10873                    || (scope.isCardProcessorQuiltAdyen && scope.isCardProcessorQuiltAdyen())
10874                )
10875                && !(scope.isPickup && scope.isPickup())
10876            );
10877        } catch (e) {
10878            return false;
10879        }
10880    }
10881
10882    function isValidAddress(value) {
10883        if (value === null || typeof value === 'undefined') {
10884            return true;
10885        }
10886
10887        var s = String(value).trim();
10888        if (!s) {
10889            return true;
10890        }
10891
10892        var lower = s.toLowerCase();
10893
10894        if (lower.indexOf(' ') === -1) {
10895            var generic = ['street','st','avenue','ave','road','rd','boulevard','blvd','lane','ln','drive','dr','highway','hwy','way'];
10896            if (generic.indexOf(lower) !== -1) {
10897                return false;
10898            }
10899        }
10900
10901        if (!/\d/.test(s)) {
10902            return false;
10903        }
10904
10905        // Require at least one letter and one number (house number pattern).
10906        if (!/[A-Za-z]/.test(s)) {
10907            return false;
10908        }
10909
10910        if (/^\d+$/.test(s)) {
10911            return false;
10912        }
10913
10914        if (/^street\s+\d+$/i.test(s) || /^address\s*\d*$/i.test(s) || /^test\s*\d*$/i.test(s)) {
10915            return false;
10916        }
10917
10918        return true;
10919    }
10920
10921    return {
10922        restrict: 'A',
10923        require: 'ngModel',
10924        link: function (scope, elem, attr, ngModel) {
10925            function validate(value) {
10926                if (!shouldValidate(scope)) {
10927                    ngModel.$setValidity('streetAddress', true);
10928                    return value;
10929                }
10930
10931                var valid = isValidAddress(value);
10932                ngModel.$setValidity('streetAddress', valid);
10933                return value;
10934            }
10935
10936            ngModel.$parsers.unshift(validate);
10937            ngModel.$formatters.unshift(function (value) {
10938                validate(value);
10939                return value;
10940            });
10941        }
10942    };
10943});
10944
10945checkoutApp.directive('phoneNumber', phoneNumberDirective);
10946
10947function phoneNumberDirective() {
10948    function getDomesticRegion(countries, domesticCountry) {
10949        var alpha3ToAlpha2 = {
10950            USA: "US",
10951            CAN: "CA",
10952            AUS: "AU",
10953        };
10954
10955        var defaultRegion = Object.keys(countries).find(function (key) {
10956            return countries[key] === domesticCountry;
10957        }) || 'USA';
10958
10959        return alpha3ToAlpha2[defaultRegion] || 'US';
10960    }
10961
10962    function formatNumber(value, region) {
10963        if (!value) {
10964            return '';
10965        }
10966
10967        try {
10968            var formatter = new libphonenumber.asYouType(region);
10969
10970            return formatter.input(value);
10971        } catch (e) {
10972            console.warn('Phone formatting failed:', e);
10973
10974            return value;
10975        }
10976    }
10977
10978    function sanitize(value) {
10979        if (!value) {
10980            return '';
10981        }
10982
10983        value = value.replace(/[^0-9+]/g, '');
10984
10985        if (value.indexOf('+') > 0) {
10986            value = value.replace(/\+/g, '');
10987        }
10988
10989        return value;
10990    }
10991
10992    function isValidNumber(value, region) {
10993        try {
10994            var number = libphonenumber.parse(value, region);
10995
10996            return libphonenumber.isValidNumber(number);
10997        } catch (e) {
10998            return false;
10999        }
11000    }
11001
11002    function detectRegion(value, fallbackRegion) {
11003        try {
11004            var parsed = libphonenumber.parse(value);
11005
11006            if (parsed && parsed.country) {
11007                return parsed.country;
11008            }
11009        } catch (e) {
11010            console.warn('Region detect is failed:', e);
11011        }
11012
11013        return fallbackRegion;
11014    }
11015
11016    function updatePlaceholder(elem, region) {
11017        var dummy = '9999999999';
11018        var formatType = 'International';
11019        var placeholder;
11020
11021        if (region === 'AU') {
11022            dummy = '0412345678';
11023        } else if (region === 'US') {
11024            formatType = 'National';
11025        }
11026
11027        try {
11028            var formatter = new libphonenumber.asYouType(region);
11029            var parsed = libphonenumber.parseCustom(dummy, region, formatter.metadata);
11030            var formatted = libphonenumber.formatCustom(parsed, formatType, formatter.metadata);
11031
11032            if (formatType === 'National') {
11033                placeholder = formatted.replace(/\d/g, '_');
11034            } else {
11035                placeholder = formatted.replace(/^(\+\d{1,3}\s*)(.*)$/, function(_, prefix, rest) {
11036                    return prefix + rest.replace(/\d/g, '_');
11037                });
11038            }
11039
11040            elem.attr('placeholder', placeholder);
11041        } catch (e) {
11042            elem.attr('placeholder', '(___) ___-____');
11043        }
11044    }
11045
11046    var validator = function (phone) {
11047        if (!phone) {
11048            return true;
11049        }
11050
11051        var result = phone.match(/\d/g);
11052
11053        if (!result) {
11054            return false;
11055        }
11056
11057        result = result.length >= 10;
11058
11059        if (result) {
11060            result = !/([A-Za-z])/.test(phone);
11061        }
11062
11063        return result;
11064    };
11065
11066    return {
11067        restrict: 'A',
11068        require: 'ngModel',
11069        link: function(scope, elem, attr, ngModel) {
11070            var defaultRegion = 'US';
11071            var currentRegion = defaultRegion;
11072
11073            var unwatch = scope.$watch('InitInfo', function(initInfo) {
11074                if (initInfo && initInfo.DomesticCountry) {
11075                    defaultRegion = getDomesticRegion(initInfo.Countries, initInfo.DomesticCountry);
11076                    currentRegion = defaultRegion;
11077
11078                    updatePlaceholder(elem, currentRegion);
11079
11080                    unwatch();
11081                }
11082            });
11083
11084            updatePlaceholder(elem, currentRegion);
11085
11086            ngModel.$formatters.push(function(value) {
11087                if (!value) {
11088                    return '';
11089                }
11090
11091                return formatNumber(value, currentRegion);
11092            });
11093
11094            ngModel.$parsers.push(function(value) {
11095                return sanitize(value);
11096            });
11097
11098            ngModel.$validators.phone = function (modelValue) {
11099                return validator(modelValue);
11100            };
11101
11102            elem.on('keypress', function (event) {
11103                var char = String.fromCharCode(event.which);
11104
11105                if (/[0-9]/.test(char)) {
11106                    return;
11107                }
11108
11109                if (char === '+' && elem[0].selectionStart === 0) {
11110                    return;
11111                }
11112
11113                event.preventDefault();
11114            });
11115
11116            elem.on('paste', function (event) {
11117                event.preventDefault();
11118
11119                var e = event.originalEvent || event;
11120                var pasted = (e.clipboardData || window.clipboardData).getData('text') || '';
11121                var cleanPasted = sanitize(pasted);
11122                var input = elem[0];
11123                var selStart = input.selectionStart;
11124                var selEnd = input.selectionEnd;
11125                var currentValue = elem.val();
11126                var before = currentValue.slice(0, selStart);
11127                var after  = currentValue.slice(selEnd);
11128                var clean = sanitize(before + cleanPasted + after);
11129
11130                currentRegion = detectRegion(clean, defaultRegion);
11131                updatePlaceholder(elem, currentRegion);
11132
11133                if (!isValidNumber(clean, currentRegion) && !clean.startsWith('+')) {
11134                    try {
11135                        before = '+' + before;
11136                        clean = sanitize(before + cleanPasted + after);
11137
11138                        currentRegion = detectRegion(clean, defaultRegion);
11139                        updatePlaceholder(elem, currentRegion);
11140                    } catch (e) {
11141                        console.warn('Failed to prepend code:', e);
11142                    }
11143                }
11144
11145                var formatted = formatNumber(clean, currentRegion);
11146                var digitsBeforeCaret = sanitize(before + cleanPasted).length;
11147
11148                scope.$applyAsync(function () {
11149                    ngModel.$setViewValue(formatted);
11150                    ngModel.$render();
11151
11152                    elem.val(formatted);
11153
11154                    var caret = 0;
11155                    var seen = 0;
11156
11157                    while (caret < formatted.length) {
11158                        if (/\d/.test(formatted[caret])) {
11159                            seen++;
11160                            if (seen === digitsBeforeCaret) {
11161                                caret--;
11162                                break;
11163                            }
11164                        }
11165                        caret++;
11166                    }
11167
11168                    input.setSelectionRange(caret, caret);
11169                });
11170            });
11171
11172            elem.on('input', function(event) {
11173                var e = event.originalEvent || event;
11174                var raw = elem.val();
11175                var clean = sanitize(raw);
11176
11177                currentRegion = detectRegion(clean, defaultRegion);
11178                updatePlaceholder(elem, currentRegion);
11179
11180                scope.$applyAsync(function() {
11181                    ngModel.$setViewValue(clean);
11182                });
11183
11184                if (clean) {
11185                    var formatted = formatNumber(clean, currentRegion);
11186
11187                    if (formatted !== raw) {
11188                        var input = e.target;
11189                        var digitsBeforeCaret = sanitize(raw.slice(0, input.selectionStart)).length;
11190
11191                        elem.val(formatted);
11192
11193                        var caret = 0;
11194                        var seen = 0;
11195
11196                        while (caret < formatted.length && seen < digitsBeforeCaret) {
11197                            if (/\d/.test(formatted[caret])) {
11198                                seen++;
11199                            }
11200                            caret++;
11201                        }
11202
11203                        input.setSelectionRange(caret, caret);
11204                    }
11205                } else {
11206                    elem.val('');
11207                }
11208            });
11209
11210            elem.on('blur', function() {
11211                scope.$applyAsync(function() {
11212                    var clean = sanitize(ngModel.$modelValue);
11213
11214                    if (!clean) {
11215                        return;
11216                    }
11217
11218                    var formatted = formatNumber(clean, currentRegion);
11219
11220                    ngModel.$setViewValue(formatted);
11221                    ngModel.$render();
11222                });
11223            });
11224        }
11225    };
11226}
11227
11228var getQueryString = function (field, url) {
11229    var href = url ? url : window.location.href;
11230    if (href) {
11231        href = href.replace(/\+/g, ' ');
11232    }
11233    var reg = new RegExp('[?&]' + field + '=([^&#]*)', 'i');
11234    var string = reg.exec(href);
11235    return string ? decodeURIComponent(string[1]) : null;
11236};
11237
11238var CouponConstants = {
11239    coupon_dollar_off: "dollar_off",
11240    coupon_percent: "percent_off",
11241    coupon_free_shipping: "free_shipping_off",
11242    coupon_regular: "regular",
11243    coupon_bogo: "bogo",
11244    better_coupon_type_free_shipping: "shipping",
11245
11246    use_single: 'single',
11247    use_multiple: 'multiple',
11248    use_per_customer: 'per_customer',
11249};
11250
11251var BetterCouponFactory = function (dataForCoupon) {
11252    "use strict";
11253
11254    this.dataForCoupon = dataForCoupon;
11255
11256    this.createCoupon = function () {
11257        var coupon = {};
11258
11259        if (this.dataForCoupon.couponBaseType === CouponConstants.better_coupon_type_free_shipping) {
11260            coupon = new FreeShippingCoupon(this.dataForCoupon);
11261        } else if (this.dataForCoupon.couponBaseType === CouponConstants.coupon_regular) {
11262            coupon = new RegularCoupon(this.dataForCoupon);
11263        } else if (this.dataForCoupon.couponBaseType === CouponConstants.coupon_bogo) {
11264            coupon = new BOGOCoupon(this.dataForCoupon);
11265        } else {
11266            throw new Error("Cannot identify type of coupon");
11267        }
11268
11269        return coupon;
11270    }
11271};
11272
11273/**
11274 * Abstract class for coupons
11275 * @abstract
11276 */
11277var BaseBetterCoupon = function (dataForCoupon) {
11278    "use strict";
11279
11280    if (new.target === BaseBetterCoupon) {
11281        throw new Error("Cannot instantiate abstract class BaseBetterCoupon directly");
11282    }
11283
11284    this.couponId = dataForCoupon.couponId || dataForCoupon.id || '';
11285    this.couponCode = dataForCoupon.couponCode || '';
11286    this.couponBaseType = dataForCoupon.couponBaseType || 'none';
11287    this.couponType = dataForCoupon.couponType || 'none';
11288    this.discount = dataForCoupon.discount || 0;
11289    this.dollarOffAmount = dataForCoupon.dollarOffAmount || 0;
11290    this.endDate = dataForCoupon.endDate || null;
11291    this.maxDiscount = dataForCoupon.maxDiscount || 0;
11292    this.minDollarAmount = dataForCoupon.minDollarAmount || 0;
11293    this.number = dataForCoupon.number || 0;
11294    this.percentOff = dataForCoupon.percentOff || null;
11295    this.title = dataForCoupon.title || 'Base Better Coupon';
11296    this.use = dataForCoupon.use || null;
11297
11298    this.appliedLineUuid = [];
11299    this.totalDiscountableItemsValue = 0;
11300
11301    this.limitedCustomerGroupList = dataForCoupon.isLimitedToCustomerGroups ? dataForCoupon.customerGroupIdList : [];
11302    this.categoryIdArray = dataForCoupon.categoryIdArray || [];
11303    this.itemTypesArray = dataForCoupon.itemTypesArray || [];
11304    this.departmentIdArray = dataForCoupon.departmentIdArray || [];
11305    this.manufacturerNameArray = dataForCoupon.manufacturerNameArray || [];
11306    this.pslrIdArray = dataForCoupon.pslrIdArray || [];
11307    this.classIdArray = dataForCoupon.classIdArray || [];
11308    this.vendorIdArray = dataForCoupon.vendorIdArray || [];
11309
11310    this.selectedTiers = [];
11311
11312    this.setAmountsBasedOnTiers = function (spentMoneyValue) {
11313    }
11314
11315    this.setAmounts = function (amounts) {
11316        this.minDollarAmount = amounts.minDollarAmount || 0;
11317        this.percentOff = amounts.percentOff || 0;
11318        this.dollarOffAmount = amounts.dollarOffAmount || 0;
11319    }
11320
11321    this.getSortedSelectedTiers = function () {
11322        return this.selectedTiers.slice().sort((a, b) => a.title.localeCompare(b.title));
11323    }
11324
11325    this.notMetMinimumAmount = function () {
11326        this.notEligible = true;
11327        this.discount = 0;
11328    }
11329
11330    this.resetNotEligible = function () {
11331        this.notEligible = false;
11332    }
11333
11334    this.allowedMaxDiscount = function () {
11335        return this.maxDiscount && this.maxDiscount > 0;
11336    }
11337
11338    if (!!window.knobby && window.knobby['module_better_coupons'] && (dataForCoupon.tiers || []).length > 0) {
11339        this.setAmounts({});
11340    }
11341};
11342
11343var RegularCoupon = function (dataForCoupon) {
11344    "use strict";
11345    BaseBetterCoupon.call(this, dataForCoupon);
11346
11347    this.tiers = dataForCoupon.tiers || [];
11348
11349    this.setAmountsBasedOnTiers = function (spentMoneyValue) {
11350        var tier = this.tiers[0] || {};
11351
11352        if (this.tiers.length > 1) {
11353            var that = this;
11354            var minDistance = Infinity;
11355
11356            //min tier in case we dont have suitable tier
11357            tier = this.tiers.reduce(function (minObj, currentObj) {
11358                if (that.couponType === CouponConstants.coupon_percent) {
11359                    return (currentObj.min_purchase < minObj.min_purchase) ? currentObj : minObj;
11360                } else if (that.couponType === CouponConstants.coupon_dollar_off) {
11361                    return (currentObj.spend_amount < minObj.spend_amount) ? currentObj : minObj;
11362                }
11363            });
11364
11365            //select suitable tier
11366            this.tiers.forEach(function (closestTier) {
11367                var comparingValue;
11368                if (that.couponType === CouponConstants.coupon_percent) {
11369                    comparingValue = closestTier.min_purchase;
11370                } else if (that.couponType === CouponConstants.coupon_dollar_off) {
11371                    comparingValue = closestTier.spend_amount;
11372                }
11373
11374                if (spentMoneyValue >= comparingValue) {
11375                    const distance = spentMoneyValue - comparingValue;
11376                    if (distance < minDistance) {
11377                        minDistance = distance;
11378                        tier = closestTier;
11379                    }
11380                }
11381            });
11382        }
11383
11384        this.selectedTiers = [tier];
11385
11386        if (this.couponType === CouponConstants.coupon_percent) {
11387            this.setAmounts({percentOff: tier.percent_off || 0, minDollarAmount: tier.min_purchase || 0});
11388        } else if (this.couponType === CouponConstants.coupon_dollar_off) {
11389            this.setAmounts({dollarOffAmount: tier.amount_off || 0, minDollarAmount: tier.spend_amount || 0});
11390        }
11391    }
11392
11393    this.allowedMaxDiscount = function () {
11394        return this.couponType === CouponConstants.coupon_percent && this.maxDiscount && this.maxDiscount > 0;
11395    }
11396};
11397
11398var BOGOCoupon = function (dataForCoupon) {
11399    "use strict";
11400    BaseBetterCoupon.call(this, dataForCoupon);
11401
11402    this.tiers = dataForCoupon.tiers || [];
11403    this.isBogoMultipleTimes = dataForCoupon.isBogoMultipleTimes || false;
11404    this.limitedBogoLines = {};
11405    this.suitableTiers = {};
11406
11407    this.setAmountsBasedOnTiers = function (objectLinesAndAmounts) {
11408        var that = this;
11409        var lines = objectLinesAndAmounts.lines;
11410        var disableCouponsForSalePriceItems = objectLinesAndAmounts.disableCouponsForSalePriceItems || false;
11411        this.resetNotEligible();
11412        this.resetBogoLinesAndSuitableTiers();
11413
11414        lines = lines.filter(function (line) {
11415            return line.type !== 'gc' && (line.discount_eligible === true || line.discount_eligible === 'true');
11416        });
11417
11418        if (disableCouponsForSalePriceItems) {
11419            lines = lines.filter(function (line) {
11420                return !line.on_sale;
11421            });
11422        }
11423
11424        if (((this.pslrIdArray && this.pslrIdArray.length) || (this.classIdArray && this.classIdArray.length))) {
11425            var pslrLines = lines.filter(function (line) {
11426                return that.pslrIdArray.map(Number).includes(line.id);
11427            });
11428            var classLines = lines.filter(function (line) {
11429                if (!!window.knobby && window.knobby['module_fix_web_class_coupon_id_matching']) {
11430                    return that.classIdArray.map(Number).includes(Number(line.quiltClassId));
11431                }
11432                return that.classIdArray.map(Number).includes(line.id);
11433            });
11434            lines = pslrLines.concat(classLines);
11435        } else if (this.departmentIdArray && this.departmentIdArray.length) {
11436            lines = lines.filter(function (line) {
11437                return that.departmentIdArray.includes(line.departmentId);
11438            });
11439        }  else if (this.itemTypesArray && this.itemTypesArray.length) {
11440            lines = lines.filter(function (line) {
11441                var itemType;
11442                if (line.product_id) {
11443                    itemType = 'products';
11444                } else if (line.type === 'class') {
11445                    itemType = 'classes';
11446                }
11447
11448                return (that.itemTypesArray.includes(itemType)) && !line.package_id;
11449            });
11450        } else if (this.categoryIdArray && this.categoryIdArray.length) {
11451            lines = lines.filter(function (line) {
11452                return line.categoryIdArray.some(function (categoryId) {
11453                    return that.categoryIdArray.includes(categoryId);
11454                });
11455            });
11456        } else if (this.vendorIdArray && this.vendorIdArray.length) {
11457            lines = lines.filter(function (line) {
11458                return that.vendorIdArray.map(Number).includes(line.pos_vendor_id);
11459            });
11460        } else if (this.manufacturerNameArray && this.manufacturerNameArray.length) {
11461            lines = lines.filter(function (line) {
11462                return that.manufacturerNameArray.includes(line.manufacturer_name);
11463            });
11464        }
11465
11466        const productAmounts = lines.reduce(function (acc, line) {
11467            const { product_id, qty } = line;
11468
11469            if (!acc[product_id]) {
11470                acc[product_id] = { product_id, qty: 0 };
11471            }
11472
11473            acc[product_id].qty += qty;
11474
11475            return acc;
11476        }, {});
11477
11478        Object.values(productAmounts).forEach(function (product) {
11479            product.suitableBogoTiers = this.getSuitableBogoTiers(product, lines);
11480        }, this);
11481
11482        const allSuitableTiersAreEmpty = Object.values(productAmounts).every(function (product) {
11483            return product.suitableBogoTiers && !product.suitableBogoTiers.length
11484        });
11485
11486        if (allSuitableTiersAreEmpty) {
11487            this.notMetMinimumAmount();
11488            this.setAmounts({minDollarAmount: 0, percentOff: 0, dollarOffAmount: 0});
11489        }
11490
11491        Object.values(productAmounts).forEach(function (product) {
11492            Object.values(product.suitableBogoTiers).forEach(function (suitableBogoTier) {
11493                const tierId = suitableBogoTier.tier.coupon_tiers_id;
11494
11495                this.suitableTiers[tierId] = this.suitableTiers[tierId] || {
11496                    actualTotalDiscountCalculated: 0,
11497                    totalDiscountToCompare: 0,
11498                    tier: null,
11499                    tierDiscounts: {}
11500                };
11501
11502                this.suitableTiers[tierId].totalDiscountToCompare += suitableBogoTier.totalDiscountToCompare;
11503                this.suitableTiers[tierId].tier = suitableBogoTier.tier;
11504                this.suitableTiers[tierId].tierDiscounts = Object.assign(this.suitableTiers[tierId].tierDiscounts, suitableBogoTier.tierDiscounts);
11505
11506                this.limitedBogoLines = Object.assign(this.limitedBogoLines, suitableBogoTier.tierDiscounts);
11507
11508                this.selectedTiers.push(suitableBogoTier.tier);
11509            }, this);
11510        }, this);
11511
11512        const firstSuitableTier = Object.values(this.suitableTiers)[0] || {};
11513
11514        var couponAmountOff = 0;
11515
11516        for (var uuid in this.limitedBogoLines) {
11517            if (this.limitedBogoLines.hasOwnProperty(uuid)) {
11518                var limitedBogoLine = this.limitedBogoLines[uuid];
11519
11520                var lineByUUid = lines.find(function (line) {
11521                    return line.uuid === uuid;
11522                }) || null;
11523
11524                if (lineByUUid) {
11525                    if (this.couponType === CouponConstants.coupon_percent) {
11526                        couponAmountOff += (lineByUUid.price * limitedBogoLine.discountableItemsAmount * (limitedBogoLine.bogoLinePercentOff/100));
11527                    } else if (this.couponType === CouponConstants.coupon_dollar_off) {
11528                        if (lineByUUid.price > limitedBogoLine.bogoLineAmountOff) {
11529                            couponAmountOff += limitedBogoLine.bogoLineAmountOff * limitedBogoLine.discountableItemsAmount;
11530                        } else {
11531                            couponAmountOff += lineByUUid.price * limitedBogoLine.discountableItemsAmount;
11532                        }
11533                    }
11534                }
11535            }
11536        }
11537
11538        if (this.couponType === CouponConstants.coupon_percent) {
11539            this.setAmounts({dollarOffAmount: couponAmountOff, percentOff: (firstSuitableTier.tier && firstSuitableTier.tier.percent_off) ? firstSuitableTier.tier.percent_off : 0, minDollarAmount: 0});
11540        } else if (this.couponType === CouponConstants.coupon_dollar_off) {
11541            this.setAmounts({dollarOffAmount: couponAmountOff, minDollarAmount : 0});
11542        }
11543    }
11544
11545    this.getSuitableBogoTiers = function ({product_id, qty}, lines) {
11546        const suitableTier = this.tiers.reduce(function (closestTier, tier) {
11547            var totalQty = parseFloat(tier.buy_qty) + parseFloat(tier.get_qty);
11548            var difference = qty - totalQty;
11549
11550            if (difference >= 0 && (closestTier === null || (difference >= 0 && difference < closestTier.difference))) {
11551                return { tier, difference };
11552            }
11553
11554            return closestTier;
11555        }, null);
11556
11557        const suitableTiersForProductByQuantity = suitableTier ? [suitableTier.tier] : [];
11558
11559        suitableTiersForProductByQuantity.forEach(function (t) {
11560            var totalQty = parseFloat(t.buy_qty) + parseFloat(t.get_qty);
11561            var multiplier = Math.floor(qty / totalQty);
11562            if (this.isBogoMultipleTimes) {
11563                t.computed_get_qty = t.get_qty * multiplier;
11564            } else {
11565                t.computed_get_qty = t.get_qty;
11566            }
11567        }, this);
11568
11569        var tierDiscountsToComparing = [];
11570        Object.values(suitableTiersForProductByQuantity).forEach(function (suitableTier) {
11571            tierDiscountsToComparing.push(this.getBogoTierDiscountAmountsByTiers(lines, suitableTier, product_id));
11572        }, this);
11573
11574        return tierDiscountsToComparing.filter(function (itemToCompare) {
11575            return itemToCompare.totalDiscountToCompare > 0;
11576        });
11577    }
11578
11579    this.getBogoTierDiscountAmountsByTiers = function (lines, tier, product_id) {
11580        const { computed_get_qty, percent_off, amount_off, coupon_tiers_id} = tier;
11581
11582        var filteredLines = lines.filter(function (line) {
11583            return line.product_id === product_id;
11584        });
11585
11586        filteredLines.sort(function (a, b) {
11587            return (
11588                // a.qty - b.qty ||
11589                a.price - b.price ||
11590                a.product_store_location_rel_id - b.product_store_location_rel_id
11591            );
11592        });
11593
11594        var remainingQty = computed_get_qty;
11595        var totalDiscountToCompare = 0;
11596        var tierDiscounts = {};
11597
11598        for (let line of filteredLines) {
11599            if (!tierDiscounts[line.uuid]) {
11600                tierDiscounts[line.uuid] = {bogoLinePercentOff: 0, bogoLineAmountOff: 0, discountableItemsAmount: 0, couponTiersId: coupon_tiers_id};
11601            }
11602
11603            var qtyCounter = 0;
11604
11605            while (remainingQty > 0) {
11606                if (line.qty > 0 && qtyCounter !== line.qty && remainingQty > 0) {
11607                    qtyCounter += 1;
11608                    remainingQty -= 1;
11609
11610                    var discountForUnit = 0;
11611                    if (this.couponType === CouponConstants.coupon_percent) {
11612                        discountForUnit = line.price * (percent_off / 100);
11613                        tierDiscounts[line.uuid]['bogoLinePercentOff'] = percent_off;
11614                    } else {
11615                        tierDiscounts[line.uuid]['bogoLineAmountOff'] = discountForUnit = parseFloat(amount_off);
11616                    }
11617
11618                    tierDiscounts[line.uuid]['discountableItemsAmount']++;
11619
11620                    totalDiscountToCompare += discountForUnit;
11621                } else {
11622                    break;
11623                }
11624            }
11625        }
11626
11627        tierDiscounts = Object.fromEntries(
11628            Object.entries(tierDiscounts).filter(function(entry) {
11629                var value = entry[1];
11630                return value.bogoLinePercentOff !== 0 || value.bogoLineAmountOff !== 0;
11631            })
11632        );
11633
11634        return { totalDiscountToCompare, tierDiscounts, tier};
11635    }
11636
11637    this.resetBogoLinesAndSuitableTiers = function () {
11638        this.limitedBogoLines = {};
11639        this.suitableTiers = {};
11640        this.selectedTiers = [];
11641    }
11642
11643    this.getSortedSelectedTiers = function () {
11644        return Object.values(this.selectedTiers).slice().filter(function(item){
11645            var suitable = this.suitableTiers[item.coupon_tiers_id];
11646            if (suitable && suitable.actualTotalDiscountCalculated > 0) {
11647                return item;
11648            }
11649
11650            return false;
11651        }, this).sort(function (a, b) { return a.title.localeCompare(b.title); });
11652    }
11653};
11654
11655var FreeShippingCoupon = function (dataForCoupon) {
11656    "use strict";
11657    BaseBetterCoupon.call(this, dataForCoupon);
11658
11659    this.setAmountsBasedOnTiers = function (spentMoneyValue) {
11660        this.setAmounts({percentOff: 100, minDollarAmount: this.minDollarAmount});
11661    }
11662};
11663
11664//extending coupons
11665FreeShippingCoupon.prototype = Object.create(BaseBetterCoupon.prototype);
11666FreeShippingCoupon.prototype.constructor = FreeShippingCoupon;
11667
11668RegularCoupon.prototype = Object.create(BaseBetterCoupon.prototype);
11669RegularCoupon.prototype.constructor = RegularCoupon;
11670
11671BOGOCoupon.prototype = Object.create(BaseBetterCoupon.prototype);
11672BOGOCoupon.prototype.constructor = BOGOCoupon;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.