{"version":3,"file":"main-O33wuVTT.js","sources":["../../src/app/options.js","../../src/app/utilities/OraDispatcher.js","../../src/app/constants/Constants.js","../../src/app/stores/Store.js","../../src/app/utilities/WidgetOptions.js","../../src/app/utilities/Http.js","../../src/app/utilities/LocalStorage.js","../../src/app/utilities/BasketService.js","../../src/app/utilities/BasketUtilities.js","../../src/app/stores/BasketStore.js","../../src/app/components/applywizard/deep-link-parser.js","../../src/app/components/applywizard/begin.jsx","../../src/app/utilities/AvailabilityService.js","../../src/app/utilities/Cookies.js","../../src/app/utilities/AdManagerService.js","../../src/app/actions/BasketActionCreators.js","../../src/app/components/applywizard/customer/customer-base.js","../../src/app/components/applywizard/press-enter-to-continue.jsx","../../src/app/components/common/form-group.jsx","../../src/app/components/common/react-onclickoutside.jsx","../../src/app/components/common/country_mobile_data.js","../../src/app/validators/Telephone.js","../../src/app/components/common/react-telephone-input.jsx","../../src/app/components/applywizard/navigation-buttons.jsx","../../src/app/components/common/loading.jsx","../../src/app/components/common/country-list.js","../../src/app/components/common/guess-user-country.js","../../src/app/components/applywizard/customer/customer.jsx","../../src/app/components/applywizard/student/student-base.js","../../src/app/components/common/nationality-list.js","../../src/app/stores/CourseStore.js","../../src/app/components/applywizard/student/student.jsx","../../src/app/components/applywizard/how-to-study.jsx","../../src/app/stores/AvailabilityStore.js","../../src/app/actions/AvailabilityActionCreators.js","../../src/app/stores/TimeZonesStore.js","../../src/app/components/applywizard/what-timezone.jsx","../../src/app/components/applywizard/region-pretty-name-lookup.js","../../src/app/components/applywizard/what-region.jsx","../../src/app/utilities/Ui.js","../../src/app/components/applywizard/course-session-picker.jsx","../../src/app/components/applywizard/what-course.jsx","../../src/app/stores/OptionsStore.js","../../src/app/components/common/react-fit-text.jsx","../../src/app/utilities/permute.js","../../src/app/components/optionspicker/options-picker.jsx","../../src/app/components/applywizard/what-session-options.jsx","../../src/app/utilities/QueryStringParser.js","../../src/app/stores/FiltersStore.js","../../src/app/utilities/SiteService.js","../../src/app/actions/SiteActionCreators.js","../../src/app/components/applywizard/what-site.jsx","../../src/app/actions/UpsellActionCreators.js","../../src/app/components/applywizard/upsells.jsx","../../src/app/components/applywizard/apply-summary.jsx","../../src/app/components/applywizard/what-next.jsx","../../src/app/components/checkout/checkout-pay.jsx","../../src/app/components/checkout/customer-form.jsx","../../src/app/stores/UserStore.js","../../src/app/utilities/AgentService.js","../../src/app/components/common/typeahead.jsx","../../src/app/components/checkout/agent-form.jsx","../../src/app/components/checkout/checkout-customer.jsx","../../src/app/components/checkout/card-icons.jsx","../../src/app/utilities/TermsAndConditionsService.js","../../src/app/actions/TermsAndConditionsCreators.js","../../src/app/utilities/BookingService.js","../../src/app/actions/CheckoutActionCreators.js","../../src/app/validators/common.js","../../src/app/validators/Customer.js","../../src/app/components/applywizard/billing.jsx","../../src/app/components/flywire/flywire-form.jsx","../../src/app/components/common/BaseCheckoutFlywireContainer.js","../../src/app/components/flywire/countdown-timer.jsx","../../src/app/components/applywizard/flywire.jsx","../../src/app/components/applywizard/no-courses-available.jsx","../../src/app/components/applywizard/step-indicator.jsx","../../src/app/components/applywizard/error-boundary.jsx","../../src/app/components/applywizard/apply-wizard.jsx","../../src/app/utilities/CourseService.js","../../src/app/actions/CourseActionCreators.js","../../src/app/utilities/CountryService.js","../../src/app/actions/CountryActionCreators.js","../../src/app/applywizard.jsx","../../src/app/entry-website.js"],"sourcesContent":["// attach to global namespace\r\nvar isLive = location &&\r\n(\r\n location.href.indexOf('www.oxford-royale.co.uk') > -1 ||\r\n location.href.indexOf('webadmin.oxford-royale.co.uk') > -1 ||\r\n location.href.indexOf('portal.oxford-royale.co.uk') > -1 ||\r\n location.href.indexOf('www.oxford-royale.com') > -1 ||\r\n location.href.indexOf('webadmin.oxford-royale.com') > -1 ||\r\n location.href.indexOf('portal.oxford-royale.com') > -1 ||\r\n (location.href.indexOf('apply.oxford-royale.com') > -1 && location.href.indexOf('flywire-test') === -1)\r\n);\r\nif (!global.OraWidgets) { global.OraWidgets = {}; }\r\nif (!global.OraWidgets.Options) { global.OraWidgets.Options = {}; }\r\nif (!global.OraWidgets.Options.minPartPaymentAmount) { global.OraWidgets.Options.minPartPaymentAmount = 500; }\r\nif (!global.OraWidgets.Options.apiEndpoint) {\r\n global.OraWidgets.Options.apiEndpoint = isLive ? 'https://api.oxford-royale.com' : 'https://localhost:44302';\r\n}\r\n\r\nif (!global.OraWidgets.Options.showOnline) { global.OraWidgets.Options.showOnline = false }\r\nif (!global.OraWidgets.Options.coursesPath) { global.OraWidgets.Options.coursesPath = \"/course\" }\r\nif (!global.OraWidgets.Options.applyUrl) { global.OraWidgets.Options.applyUrl = \"/apply\" }\r\nif (!global.OraWidgets.Options.checkoutUrl) { global.OraWidgets.Options.checkoutUrl = \"/checkout\" }\r\nif (!global.OraWidgets.Options.browseUrl) { global.OraWidgets.Options.browseUrl = \"/courses\" }\r\nif (!global.OraWidgets.Options.imagePath) { global.OraWidgets.Options.imagePath = \"https://az707692.vo.msecnd.net/cdn/images/\" }\r\nif (!global.OraWidgets.Options.googleRecaptchaSiteKey) { global.OraWidgets.Options.googleRecaptchaSiteKey = \"6LcxbioUAAAAALOhOY4OVtFkayyWkNDDzj4w0x2W\" }\r\nif (!global.OraWidgets.Options.stripeKey) { global.OraWidgets.Options.stripeKey = \"pk_test_oZZgPzFuYifWb9ZS2izdxlDp\" }\r\nif (!global.OraWidgets.Options.callBackUrl) { global.OraWidgets.Options.callBackUrl = isLive ? 'https://api.oxford-royale.com/Flywire/Callback' : \"https://flywire-api.ora-uat-website.com/Flywire/Callback\" }\r\nif (!global.OraWidgets.Options.applicationInsightsKey && location && (location.href.indexOf('www.oxford-royale.co.uk') > -1 || location.href.indexOf('www.oxford-royale.com') > -1 || location.href.indexOf('apply.oxford-royale.com') > -1)) { global.OraWidgets.Options.applicationInsightsKey = '80ec8bf1-6d8c-4f05-8fe6-addf2e258c67'; }\r\nif (!global.OraWidgets.Options.zenDeskSiteKey) { global.OraWidgets.Options.zenDeskSiteKey = \"6LcxbioUAAAAALOhOY4OVtFkayyWkNDDzj4w0x2W\" }\r\nif (!global.OraWidgets.Options.checkoutPrivacyNoticeTitle) { global.OraWidgets.Options.checkoutPrivacyNoticeTitle = \"How do we use your information?\" }\r\nif (!global.OraWidgets.Options.checkoutPrivacyNoticeBody) {\r\n global.OraWidgets.Options.checkoutPrivacyNoticeBody =\r\n \"
At Oxford Royale Academy, we do our best to help customers and students find the right course for them.
To assist us in doing that, we will store the information you enter into our Checkout form (excluding payment card information), and if you do not or are not able to complete your enrolment we will get in touch with you by email. If you object to this, please do not enter any information into the form.
For more information, please see our Privacy Notices. Thank you.
\";\r\n}\r\n\r\nif (!global.OraWidgets.Options.checkoutMarketingOptInText) { global.OraWidgets.Options.checkoutMarketingOptInText = \"Please tick this box if you would like to receive information about our courses in the future.\" }\r\nif (!global.OraWidgets.Options.checkoutMarketingOptInTextAdmin) { global.OraWidgets.Options.checkoutMarketingOptInTextAdmin = \"Please tick if the customer would like to receive updates and information regarding other courses, products and services from Oxford Royale Academy\" }\r\nif (!global.OraWidgets.Options.checkoutPrivacyPolicy) { global.OraWidgets.Options.checkoutPrivacyPolicy = \"I accept the Privacy Notice
\" }\r\nif (!global.OraWidgets.Options.cjEnabled) { global.OraWidgets.Options.cjEnabled = false; }\r\nif (!global.OraWidgets.Options.cjType) { global.OraWidgets.Options.cjType = '414210'; }\r\nif (!global.OraWidgets.Options.cjCID) { global.OraWidgets.Options.cjCID = '1555537'; }\r\nif (!global.OraWidgets.Options.cjQueryName) { global.OraWidgets.Options.cjQueryName = 'cjevent'; }\r\nif (!global.OraWidgets.Options.cjCookieMaxAge) { global.OraWidgets.Options.cjCookieMaxAge = 31536000; } // 1 year\r\nif (!global.OraWidgets.Options.isAgentBasket) { global.OraWidgets.Options.isAgentBasket = false; }\r\nif (!global.OraWidgets.Options.paymentEnvironment) { global.OraWidgets.Options.paymentEnvironment = isLive ? \"prod\" : \"demo\"; }\r\nif (!global.OraWidgets.Options.defaultFlywireDestination) { global.OraWidgets.Options.defaultFlywireDestination = isLive ? 'ORR' : 'ORC'; }\r\nif (!global.OraWidgets.Options.flywirePendingMessageTitle) { global.OraWidgets.Options.flywirePendingMessageTitle = 'Thank you for arranging your payment. Your order currently has a status of Pending.'; }\r\nif (!global.OraWidgets.Options.flywirePendingMessageText) {\r\n global.OraWidgets.Options.flywirePendingMessageText = [\r\n 'When we have received the funds from our selected payment partner, Flywire, your order will be Confirmed and we will send you a Confirmation Email, a link to the ORA Portal, and a receipt.',\r\n 'Thank you for choosing Oxford Royale Academy.'\r\n ];\r\n}\r\nif (!global.OraWidgets.Options.flywireErrorMessageTitle) { global.OraWidgets.Options.flywireErrorMessageTitle = \"We're sorry, your payment was not successful.\"; }\r\nif (!global.OraWidgets.Options.flywireErrorMessageText) {\r\n global.OraWidgets.Options.flywireErrorMessageText =\r\n ['Please try again or contact us for assistance.'];\r\n}\r\nif (!global.OraWidgets.Options.ondemandLearningUrl) { global.OraWidgets.Options.ondemandLearningUrl = \"https://oraprep.learnupon.com/store\"; }\r\nif (!global.OraWidgets.Options.virtualTimeZones) {\r\n global.OraWidgets.Options.virtualTimeZones = [\r\n //{ Text: 'Western Hemisphere', Value: 'Western Hemisphere', Offset: '+0' },\r\n { Text: 'Eastern Hemisphere', Value: 'Eastern Hemisphere', Offset: '+11' }\r\n ];\r\n}\r\nif (!global.OraWidgets.Options.showTimesInTimezone) { global.OraWidgets.Options.showTimesInTimezone = false; }\r\nif (!global.OraWidgets.Options.injectCss) {\r\n global.OraWidgets.Options.injectCss = location.href.indexOf('flywire') > -1;\r\n}\r\nif (global.OraWidgets.Options.payTotalDiscount !== false) {\r\n global.OraWidgets.Options.payTotalDiscount = 5;\r\n}","import { Dispatcher } from 'flux';\r\n\r\nexport default new Dispatcher();","import keyMirror from \"keymirror\";\r\n\r\nexport const DATE_FORMAT = \"DD/MM/YYYY\";\r\nexport const ActionTypes = keyMirror({\r\n // Sites\r\n GET_ALL_SITES: null,\r\n\r\n // Courses\r\n GET_COURSE: null,\r\n GET_COURSES: null,\r\n\r\n // Course Types\r\n GET_ALL_COURSE_TYPES: null,\r\n\r\n // Countries\r\n GET_ALL_COUNTRIES: null,\r\n\r\n // Region Session Groups\r\n GET_REGION_SESSION_GROUPS: null,\r\n\r\n // Terms and Conditions\r\n GET_ALL_TERMS_AND_CONDITIONS: null,\r\n\r\n // Agents\r\n GET_ALL_AGENTS: null,\r\n\r\n /* Availability */\r\n GET_TIMEZONES: null,\r\n GET_AVAILABILITY: null,\r\n GET_COURSE_SUGGESTIONS: null,\r\n BEGIN_COURSE_SUGGESTIONS: null,\r\n GET_AGENT_AVAILABILITY: null,\r\n SELECTION_CHOSEN: null,\r\n SELECTION_CLEARED: null,\r\n GET_OPTIONS: null,\r\n CHOOSE_OPTION: null,\r\n CLEAR_OPTIONS: null,\r\n DATES_UPDATED: null,\r\n SELECT_REGION: null,\r\n DESELECT_REGION: null,\r\n\r\n /* Basket */\r\n GET_LINEITEM_STARTED: null,\r\n GET_LINEITEM_FAILURE: null,\r\n GET_LINEITEM_OPTIONS_SUCCESS: null,\r\n GET_LINEITEM_DATA_SUCCESS: null,\r\n ADD_COURSE: null,\r\n REMOVE_COURSE: null,\r\n REPLACE_COURSE: null,\r\n UPDATE_STUDENT: null,\r\n UPDATE_CUSTOMER: null,\r\n CHOOSE_STUDENT: null,\r\n CHOOSE_CUSTOMER: null,\r\n LOAD_BASKET: null,\r\n CLEAR_BASKET: null,\r\n ADDED_UPSELL: null,\r\n DISCOUNTS_FETCHED: null,\r\n\r\n // Filters\r\n UPDATE_FILTER: null,\r\n CLEAR_FILTERS: null,\r\n\r\n // Search\r\n SEARCH_BEGIN: null,\r\n SEARCH_SUCCESS: null,\r\n\r\n // Reservations\r\n MAKE_RESERVATION_STARTED: null,\r\n MAKE_RESERVATION_SUCCESS: null,\r\n MAKE_RESERVATION_FAILURE: null,\r\n MAKE_BOOKING_STARTED: null,\r\n MAKE_BOOKING_SUCCESS: null,\r\n\r\n // Enquiries\r\n BEGIN_ENQUIRY: null,\r\n SEND_ENQUIRY_START: null,\r\n SEND_ENQUIRY_SENT: null,\r\n\r\n // upsells\r\n SEND_UPSELL_REQUEST_BEGIN: null,\r\n SEND_UPSELL_REQUEST_SUCCESS: null,\r\n SEND_UPSELL_REQUEST_FAILURE: null,\r\n\r\n // USERS\r\n WHO_AM_I_SUCCESS: null,\r\n\r\n // Payment\r\n SEND_PAYMENT_STARTED: null,\r\n SEND_PAYMENT_SUCCESS: null,\r\n SEND_PAYMENT_FAILURE: null,\r\n\r\n POLL_PAYMENT_START: null,\r\n POLL_PAYMENT_FINISHED: null,\r\n\r\n CREATE_PAYMENT_INTENT_SUCCESS: null,\r\n});\r\n","import indexOf from 'lodash/indexOf';\r\n\r\nfunction Store() {\r\n this.listeners = [];\r\n}\r\n\r\n// listener registration\r\nStore.prototype.addChangeListener = function(callback) {\r\n this.listeners.push(callback);\r\n};\r\n\r\n// listener de-registration\r\nStore.prototype.removeChangeListener = function(callback) {\r\n if (indexOf(this.listeners, callback) !== -1) {\r\n this.listeners.splice(indexOf(this.listeners, callback), 1);\r\n }\r\n};\r\n\r\n// telling components there's a change with an optional context for those stores that require context\r\nStore.prototype.emitChange = function(context) {\r\n for (var i = 0; i < this.listeners.length; i++) {\r\n this.listeners[i](context);\r\n }\r\n}\r\n\r\nexport default Store;","export default {\r\n apiEndpoint: function() { return global.OraWidgets.Options.apiEndpoint; }\r\n};","// check for jQuery jQuery\r\nif (!jQuery) {\r\n throw \"Ora Widgets require jQuery to be defined\";\r\n}\r\n\r\n// make sure credentials are sent with requests\r\njQuery.ajaxPrefilter(function (options, originalOptions, jqXhr) {\r\n if (options.url.indexOf(global.OraWidgets.Options.apiEndpoint) > -1) {\r\n if (options.xhrFields) {\r\n options.xhrFields.withCredentials = true;\r\n } else {\r\n options.xhrFields = { withCredentials: true };\r\n }\r\n }\r\n});\r\n\r\nfunction get(url, dataType) {\r\n return ajax({\r\n url: url,\r\n method: 'GET',\r\n dataType: dataType\r\n });\r\n}\r\n\r\nfunction post(url, data, dataType) {\r\n return ajax({\r\n url: url,\r\n method: 'POST',\r\n data: data,\r\n dataType: dataType\r\n });\r\n}\r\n\r\nfunction put(url, data, dataType) {\r\n return ajax({\r\n url: url,\r\n method: 'PUT',\r\n data: data,\r\n dataType: dataType\r\n });\r\n}\r\n\r\nfunction del(url) {\r\n return ajax({\r\n url: url,\r\n method: 'DELETE'\r\n });\r\n}\r\n\r\nfunction ajax(options) {\r\n standardiseRequest(options);\r\n return jQuery.ajax(options).fail(handleError);\r\n}\r\n\r\nfunction getQ(url, dataType) {\r\n return queue({\r\n url: url,\r\n method: 'GET',\r\n dataType: dataType\r\n });\r\n}\r\n\r\nfunction postQ(url, data, dataType) {\r\n return queue({\r\n url: url,\r\n method: 'POST',\r\n data: data,\r\n dataType: dataType\r\n });\r\n}\r\n\r\nfunction putQ(url, data, dataType) {\r\n return queue({\r\n url: url,\r\n method: 'PUT',\r\n data: data,\r\n dataType: dataType\r\n });\r\n}\r\n\r\nfunction delQ(url) {\r\n return queue({\r\n url: url,\r\n method: 'DELETE'\r\n });\r\n}\r\n\r\n// inspired (COPIED) from https://github.com/Foliotek/AjaxQ/blob/master/ajaxq.js\r\nvar queues = {};\r\nvar activeRequests = {};\r\n\r\nfunction queue(options, queueName) {\r\n if (!queueName) {\r\n queueName = '__default__';\r\n }\r\n\r\n standardiseRequest(options);\r\n var deferred = jQuery.Deferred(),\r\n promise = deferred.promise();\r\n\r\n promise.success = promise.done;\r\n promise.error = promise.fail;\r\n promise.complete = promise.always;\r\n\r\n // Check whether options are to be evaluated at call time or not.\r\n var deferredOpts = typeof options === 'function';\r\n\r\n // Create a deep copy of the arguments, and enqueue this request.\r\n var clonedOptions = !deferredOpts ? jQuery.extend(true, {}, options) : null;\r\n enqueue(function () {\r\n // Send off the ajax request now that the item has been removed from the queue\r\n var jqXhr = jQuery.ajax.apply(global, [deferredOpts ? options() : clonedOptions]);\r\n\r\n // set a reasonable timeout\r\n if (jqXhr.timeout)\r\n jqXhr.timeout(10000);\r\n\r\n // Notify the returned deferred object with the correct context when the jqXHR is done or fails\r\n // Note that 'always' will automatically be fired once one of these are called: http://api.jquery.com/category/deferred-object/.\r\n jqXhr.done(function () {\r\n deferred.resolve.apply(this, arguments);\r\n });\r\n\r\n jqXhr.fail(function () {\r\n deferred.reject.apply(this, arguments);\r\n });\r\n\r\n jqXhr.always(dequeue); // make sure to dequeue the next request AFTER the done and fail callbacks are fired\r\n\r\n return jqXhr;\r\n });\r\n\r\n return promise.fail(handleError);\r\n\r\n // If there is no queue, create an empty one and instantly process this item.\r\n // Otherwise, just add this item onto it for later processing.\r\n function enqueue(cb) {\r\n if (!queues[queueName]) {\r\n queues[queueName] = [];\r\n var xhr = cb();\r\n activeRequests[queueName] = xhr;\r\n }\r\n else {\r\n queues[queueName].push(cb);\r\n }\r\n }\r\n\r\n // Remove the next callback from the queue and fire it off.\r\n // If the queue was empty (this was the last item), delete it from memory so the next one can be instantly processed.\r\n function dequeue() {\r\n if (!queues[queueName]) {\r\n return;\r\n }\r\n var nextCallback = queues[queueName].shift();\r\n if (nextCallback) {\r\n var xhr = nextCallback();\r\n activeRequests[queueName] = xhr;\r\n }\r\n else {\r\n delete queues[queueName];\r\n delete activeRequests[queueName];\r\n }\r\n }\r\n}\r\n\r\nfunction standardiseRequest(options) {\r\n if (!options || !options.hasOwnProperty('dataType') || !options.dataType) {\r\n options.dataType = 'json';\r\n }\r\n}\r\n\r\nvar lastError;\r\nfunction handleError(response) {\r\n if (response.status == 401 && window.location.href.indexOf('/Opportunity')) {\r\n return; // swallow these as we allow unauthed access\r\n }\r\n\r\n if (!lastError || (new Date()).getTime() - lastError.getTime() > 10000) {\r\n lastError = new Date();\r\n console.error('There was an un-anticipated error. Please refresh the browser and check that your recent edits have worked');\r\n }\r\n}\r\n\r\nvar isQueueRunning = function (qname) {\r\n return (queues.hasOwnProperty(qname) && queues[qname].length > 0) || activeRequests.hasOwnProperty(qname);\r\n};\r\n\r\n\r\nvar isAnyQueueRunning = function () {\r\n for (var i in queues) {\r\n if (queues.hasOwnProperty(i)) {\r\n if (isQueueRunning(i)) return true;\r\n }\r\n }\r\n return false;\r\n};\r\n\r\njQuery(global).on('beforeunload', function () {\r\n if ((jQuery.active || isAnyQueueRunning()) && window.location.href.indexOf('www.oxford-royale.co.uk') === -1 && window.location.href.indexOf('www.oxford-royale.com') === -1) {\r\n return 'There are items being saved. Please click cancel and wait a few seconds before trying to leave the page';\r\n }\r\n});\r\n\r\nexport default {\r\n get: get,\r\n post: post,\r\n put: put,\r\n 'delete': del,\r\n ajax: ajax,\r\n getQ: getQ,\r\n postQ: postQ,\r\n putQ: putQ,\r\n deleteQ: delQ,\r\n queue: queue\r\n};","export default {\r\n getItem: function (key) {\r\n var expiry = JSON.parse(global.localStorage.getItem('wa_ttl') || 'false');\r\n if (expiry && expiry < new Date().getTime()) {\r\n global.localStorage.clear();\r\n return null;\r\n }\r\n\r\n return global.localStorage.getItem(key);\r\n },\r\n\r\n setItem: function (key, value) {\r\n var now = new Date();\r\n var expiry = now.getTime() + (1000 * 60 * 60 * 24 * 31); // 31 days sliding expiry\r\n global.localStorage.setItem('wa_ttl', JSON.stringify(expiry));\r\n global.localStorage.setItem(key, value);\r\n }\r\n};","import options from './WidgetOptions.js';\r\nimport http from './Http.js';\r\nimport isEmpty from 'lodash/isEmpty';\r\nimport map from 'lodash/map';\r\nimport storage from '../utilities/LocalStorage.js';\r\n\r\nfunction resolvedPromise() {\r\n var dfd = global.jQuery.Deferred();\r\n dfd.resolve(null); // basket store checks against null as to whether to update basket identifier\r\n return dfd.promise();\r\n}\r\n\r\nfunction getRequestData(customer, courses, basketIdentifier) {\r\n var data = {\r\n CustomerFirstName: customer.FirstName,\r\n CustomerSurname: customer.LastName,\r\n CustomerEmailAddress: customer.Email,\r\n CustomerMobile: customer.Mobile,\r\n CustomerTitle: customer.Title,\r\n SerializedBasket: JSON.stringify({\r\n version: '2.0.0',\r\n customer: customer,\r\n courses: courses\r\n })\r\n };\r\n\r\n if (basketIdentifier) {\r\n data.BasketIdentifier = basketIdentifier;\r\n }\r\n\r\n return data;\r\n}\r\n\r\nvar lastBasketData = storage.getItem('__ora_widget_lastbasket') || '';\r\nvar lastDiscountRequestData = storage.getItem('__ora_widget_lastdiscountrequestdata') || '';\r\n\r\nexport default {\r\n getDiscounts: function (customer, courses, basketIdentifier) {\r\n var coursesAsJson = JSON.stringify(courses);\r\n if ((lastDiscountRequestData.length === 0 && isEmpty(courses))\r\n || (coursesAsJson === lastDiscountRequestData)) {\r\n // no change\r\n return resolvedPromise();\r\n }\r\n \r\n lastDiscountRequestData = coursesAsJson;\r\n if (global.localStorage) {\r\n storage.setItem('__ora_widget_lastdiscountrequestdata', lastDiscountRequestData);\r\n }\r\n\r\n var requestData = getRequestData(customer, courses, basketIdentifier);\r\n return http.postQ(options.apiEndpoint() + '/Basket/Discounts', requestData);\r\n },\r\n\r\n getUpsellOptions: function (courses) {\r\n var requestData = getRequestData({}, courses);\r\n return http.post(options.apiEndpoint() + '/Upsell/Options', requestData);\r\n },\r\n\r\n saveBasket: function (customer, courses, basketId, basketIdentifier) {\r\n if (lastBasketData.length === 0 && isEmpty(customer) && isEmpty(courses)) {\r\n // no change\r\n return resolvedPromise();\r\n }\r\n\r\n var requestData = getRequestData(customer, courses, basketIdentifier);\r\n var requestDataAsJson = JSON.stringify(requestData);\r\n if (requestDataAsJson === lastBasketData) {\r\n return resolvedPromise();\r\n }\r\n\r\n lastBasketData = requestDataAsJson;\r\n if (global.localStorage) {\r\n storage.setItem('__ora_widget_lastbasket', lastBasketData);\r\n }\r\n\r\n // MOVED to basket identifier now so no need to worry about basketid as well\r\n if (basketId) {\r\n requestData.BasketId = basketId;\r\n }\r\n\r\n if (global.OraWidgets.Options.isAgentBasket) {\r\n requestData.IsAgentBasket = true;\r\n }\r\n\r\n return http.postQ(options.apiEndpoint() + '/Basket', requestData);\r\n },\r\n\r\n getLineItemData: function (courseId, sessionIds, regionId) {\r\n var query = 'courseId=' + courseId;\r\n if (regionId) {\r\n query += '®ionId=' + regionId;\r\n }\r\n\r\n query += '&' + map(sessionIds,\r\n function(sessionId) {\r\n return 'sessionIds=' + sessionId;\r\n }).join('&');\r\n return http.get(options.apiEndpoint() + '/availability/item?' + query);\r\n }\r\n};","function upgradeBasket(basket) {\r\n copyAndDeleteProp(basket, 'courses', 'Courses');\r\n\r\n // map over to PascalCase if necessary\r\n for (var key in basket.Courses) {\r\n if (basket.Courses.hasOwnProperty(key)) {\r\n copyAndDeleteProp(basket.Courses[key], 'course', 'Course');\r\n copyAndDeleteProp(basket.Courses[key], 'itemId', 'ItemId');\r\n copyAndDeleteProp(basket.Courses[key], 'intensity', 'Intensity');\r\n copyAndDeleteProp(basket.Courses[key], 'options', 'Options');\r\n copyAndDeleteProp(basket.Courses[key], 'selectedRegions', 'SelectedRegions');\r\n copyAndDeleteProp(basket.Courses[key], 'region', 'Region');\r\n copyAndDeleteProp(basket.Courses[key], 'student', 'Student');\r\n\r\n if (basket.Courses[key].Course.hasOwnProperty('Type') &&\r\n !basket.Courses[key].Course.hasOwnProperty('CourseType')) {\r\n basket.Courses[key].Course.CourseType = basket.Courses[key].Course.Type;\r\n }\r\n\r\n if (basket.Courses[key].SelectedRegions &&\r\n basket.Courses[key].SelectedRegions.length &&\r\n !basket.Courses[key].Region) {\r\n basket.Courses[key].Region = basket.Courses[key].SelectedRegions[0];\r\n }\r\n\r\n // TODO: maybe change price mapping for old baskets?\r\n if (basket.Courses[key].hasOwnProperty('Price') && \r\n basket.Courses[key].Price.hasOwnProperty('Price')) {\r\n basket.Courses[key].Price = basket.Courses[key].Price.Price;\r\n }\r\n }\r\n }\r\n\r\n return basket;\r\n}\r\n\r\nfunction copyAndDeleteProp(obj, from, to) {\r\n if (obj.hasOwnProperty(from) && !obj.hasOwnProperty(to)) {\r\n obj[to] = obj[from];\r\n delete obj[from];\r\n }\r\n}\r\n\r\nfunction StudentEquals(left, right) {\r\n if (!left || !right) return false;\r\n return left.FirstName == right.FirstName\r\n && left.LastName == right.LastName\r\n && left.Gender == right.Gender\r\n && left.DateOfBirth && left.DateOfBirth.isSame(right.DateOfBirth)\r\n && (left.Nationality ? left.Nationality.NationalityId : -1 ) == (right.Nationality ? right.Nationality.NationalityId : -2);\r\n}\r\n\r\nexport default { \r\n upgradeBasket: upgradeBasket,\r\n studentEquals: StudentEquals\r\n};","import Dispatcher from '../utilities/OraDispatcher.js';\r\nimport { ActionTypes } from '../constants/Constants.js';\r\nimport create from 'lodash/create';\r\nimport assign from 'lodash/assign';\r\nimport omitBy from 'lodash/omitBy';\r\nimport forEach from 'lodash/forEach';\r\nimport map from 'lodash/map';\r\nimport mapValues from 'lodash/mapValues';\r\nimport pick from 'lodash/pick';\r\nimport size from 'lodash/size';\r\nimport every from 'lodash/every';\r\nimport some from 'lodash/some';\r\nimport reduce from 'lodash/reduce';\r\nimport isEmpty from 'lodash/isEmpty';\r\nimport find from 'lodash/find';\r\nimport filter from 'lodash/filter';\r\nimport min from 'lodash/min';\r\nimport uniq from 'lodash/uniq';\r\nimport debounce from 'lodash/debounce';\r\nimport clone from 'lodash/clone';\r\nimport Store from './Store.js';\r\nimport moment from 'moment';\r\nimport BasketService from '../utilities/BasketService.js';\r\nimport BasketUtilities from '../utilities/BasketUtilities.js';\r\nimport storage from '../utilities/LocalStorage.js';\r\nimport { v4 as uuidv4 } from 'uuid';\r\n\r\nvar courses = {}; // keeps track of courses added to the basket\r\nvar students = {};\r\nvar prices = []; // price cache, so that upon adding a new course to the basket if we have a price we can use that // TODO: maybe change name/structure?\r\nvar customer = {};\r\nvar discounts = [];\r\nvar lineItems = {}; // keeps track of lineitem data (with pricing) coming back from the api\r\nvar isReserving = false;\r\nvar isConfirming = false;\r\nvar isConfirmed = false;\r\nvar isPollingPaymentStatus = false;\r\nvar isUpsellSearching = false;\r\nvar countries = [];\r\nvar reservation, reservationFailureResponse;\r\nvar paymentStatus, paymentFailureResponse, paymentIntentId, stripeClientSecret;\r\nvar order = undefined;\r\nvar termsAndConditions;\r\nvar upsells = [];\r\nvar options = { };\r\nvar basketIdentifier = uuidv4();\r\nvar referralSecret = null;\r\nvar successRedirectUri;\r\nvar paymentStatusEnum = { Created: 1, Success: 2, Fail: 3 };\r\n\r\n// when adding courses we go ask the api for the data\r\nvar isFetchingCourseData = false;\r\nvar courseOptionsData = {}; // if it's an options based course we don't add the course, instead we assign it here so that any components can show it\r\n\r\nfunction reset() {\r\n courses = {}; // keeps track of courses added to the basket\r\n students = {}; // keeps track of student data for each item\r\n prices = [];\r\n customer = {};\r\n lineItems = {};\r\n discounts = [];\r\n isReserving = false;\r\n reservation = undefined;\r\n reservationFailureResponse = undefined;\r\n isPollingPaymentStatus = false;\r\n paymentStatus = undefined;\r\n paymentFailureResponse = undefined;\r\n paymentIntentId = undefined;\r\n stripeClientSecret = undefined;\r\n termsAndConditions = undefined;\r\n basketIdentifier = uuidv4();\r\n referralSecret = null;\r\n order = undefined;\r\n}\r\n\r\nfunction BasketStore() {\r\n Store.call(this);\r\n\r\n // load items from localstorage\r\n deserialize();\r\n updateBasketCookie();\r\n notifyApi();\r\n}\r\n\r\nBasketStore.prototype = create(Store.prototype, {\r\n 'constructor': BasketStore,\r\n\r\n getIsFetchingCourseData: function() {\r\n return isFetchingCourseData;\r\n },\r\n\r\n getCourseOptionsData: function (context) {\r\n if (!courseOptionsData.hasOwnProperty(context)) {\r\n return false;\r\n }\r\n\r\n return courseOptionsData[context];\r\n },\r\n\r\n numberOfItems: function () {\r\n return size(courses);\r\n },\r\n\r\n getUpsellOptions: function(lineItem) {\r\n return options[lineItem.ItemId];\r\n },\r\n\r\n isUpsellSearching: function() {\r\n return isUpsellSearching;\r\n },\r\n\r\n hasUpsell: function (lineItemOption) {\r\n for (var i = 0; i < upsells.length; i++) {\r\n if (upsells[i] === lineItemOption) {\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n },\r\n\r\n getCourses: function () {\r\n return courses;\r\n },\r\n\r\n getCourseByItemId: function (itemId) {\r\n if (courses.hasOwnProperty(itemId)) {\r\n return courses[itemId];\r\n }\r\n\r\n return false;\r\n },\r\n\r\n getDiscounts: function() {\r\n return discounts;\r\n },\r\n\r\n getBasketTotals: function () {\r\n var totalExcludingDiscounts = 0;\r\n var totalIncludingCourseDiscounts = 0;\r\n var deposit = 0;\r\n\r\n forEach(courses,\r\n function (lineItem) {\r\n totalExcludingDiscounts += lineItem.PriceExcludingDiscount;\r\n totalIncludingCourseDiscounts += lineItem.PriceIncludingDiscount;\r\n deposit += lineItem.Deposit;\r\n });\r\n \r\n var totalIncludingBasketDiscounts = totalExcludingDiscounts;\r\n forEach(discounts,\r\n function (discount) {\r\n totalIncludingBasketDiscounts -= discount.Discount.Amount;\r\n });\r\n\r\n return { totalIncludingBasketDiscounts: totalIncludingBasketDiscounts, totalIncludingCourseDiscounts: totalIncludingCourseDiscounts, totalExcludingDiscounts: totalExcludingDiscounts, deposit: deposit };\r\n },\r\n\r\n //getPrice: function (course, intensity, options) {\r\n // var price = getPriceObject(course, intensity, options);\r\n // if (!price) {\r\n // return false;\r\n // }\r\n\r\n // return price.price;\r\n //},\r\n\r\n //getStudent: function (itemId) {\r\n // if (!students.hasOwnProperty(itemId)) {\r\n // students[itemId] = {};\r\n // }\r\n\r\n // return students[itemId];\r\n //},\r\n\r\n getLineItem: function(courseId, sessionIds, regionId) {\r\n var key = lineItemKeyMaker(courseId, sessionIds, regionId);\r\n return lineItems[key];\r\n },\r\n\r\n getCustomer: function () {\r\n return clone(customer);\r\n },\r\n\r\n isReserving: function () {\r\n return isReserving;\r\n },\r\n\r\n isConfirming: function () {\r\n return isConfirming;\r\n },\r\n\r\n isConfirmed: function () {\r\n return isConfirmed;\r\n },\r\n\r\n getReservation: function () {\r\n return reservation;\r\n },\r\n\r\n getStripeClientSecret: function () {\r\n return stripeClientSecret;\r\n },\r\n\r\n getPaymentIntentId: function () {\r\n return paymentIntentId;\r\n },\r\n\r\n getReservationFailureResponse: function () {\r\n return reservationFailureResponse;\r\n },\r\n\r\n getCountries: function () {\r\n return countries;\r\n },\r\n\r\n getBasketIdentifier: function() {\r\n return basketIdentifier;\r\n },\r\n\r\n getReferralSecret: function() {\r\n return referralSecret;\r\n },\r\n\r\n getBasketIdentifierAndSecret: function () {\r\n return {\r\n basketIdentifier: basketIdentifier,\r\n secret: referralSecret\r\n }\r\n },\r\n\r\n getApplicableTermsAndConditions: function () {\r\n if (typeof termsAndConditions == \"undefined\") {\r\n return false;\r\n }\r\n\r\n var applicableTermsAndConditions = [];\r\n forEach(courses, function (courseItem) {\r\n forEach(termsAndConditions, function (termsAndConditionsDocument) {\r\n // first we must match on course type\r\n var courseTypePropertyMatches = find(termsAndConditionsDocument.Properties, function (documentProperty) {\r\n return documentProperty.Key === 'CourseType' && documentProperty.Value * 1 === courseItem.Course.CourseType.CourseTypeId;\r\n });\r\n\r\n if (courseTypePropertyMatches) {\r\n // do we match on the dates\r\n var effectiveFrom = moment.utc(find(termsAndConditionsDocument.Properties, function (documentProperty) {\r\n return documentProperty.Key === 'EffectiveFrom';\r\n }).Value, 'DD/MM/YYYY');\r\n var effectiveTo = moment.utc(find(termsAndConditionsDocument.Properties, function (documentProperty) {\r\n return documentProperty.Key === 'EffectiveTo';\r\n }).Value, 'DD/MM/YYYY');\r\n\r\n if (courseItem.Course.CourseType.AvailabilityModel === 0) {\r\n // session based - get the min from date from the options and use that\r\n var date = moment.utc(min(courseItem.Options, function (option) {\r\n return moment.utc(option.Session.From).unix();\r\n }).Session.From);\r\n } else if (courseItem.Course.CourseType.AvailabilityModel === 1) {\r\n // dates based\r\n var date = moment.utc(courseItem.Options[0].StartDate);\r\n } else if (courseItem.Course.CourseType.AvailabilityModel === 2) {\r\n // not based on dates - check based on todays date\r\n var date = moment.utc();\r\n }\r\n\r\n if ((date.isSame(effectiveFrom) || date.isAfter(effectiveFrom)) && (date.isSame(effectiveTo) || date.isBefore(effectiveTo))) {\r\n applicableTermsAndConditions.push(termsAndConditionsDocument);\r\n }\r\n }\r\n });\r\n });\r\n\r\n return uniq(applicableTermsAndConditions, 'DocumentId');\r\n },\r\n\r\n getPaymentIntentStatus: function () {\r\n return paymentStatus;\r\n },\r\n\r\n getPaymentFailureReason: function () {\r\n return paymentFailureResponse;\r\n },\r\n\r\n getOrder: function() {\r\n return order;\r\n },\r\n\r\n getRedirectUri: function () {\r\n return successRedirectUri;\r\n },\r\n\r\n hasPaymentPollingStarted: function () {\r\n return paymentStatus !== undefined && paymentStatus !== null;\r\n },\r\n\r\n isPaymentCreated: function () {\r\n return isPollingPaymentStatus || paymentStatus === paymentStatusEnum.Created;\r\n },\r\n\r\n isPaymentSucceeded: function () {\r\n return paymentStatus === paymentStatusEnum.Success;\r\n },\r\n\r\n isPaymentFailed: function () {\r\n return paymentStatus === paymentStatusEnum.Fail;\r\n },\r\n\r\n notifyApi: notifyApi\r\n});\r\n\r\nfunction lineItemKeyMaker(courseId, sessionIds, regionId) {\r\n var keyBits = [];\r\n keyBits.push(courseId + '');\r\n if (sessionIds) {\r\n keyBits.push(sessionIds.join(','));\r\n } else {\r\n keyBits.push('NOPE');\r\n }\r\n\r\n if (regionId) {\r\n keyBits.push(regionId + '');\r\n } else {\r\n keyBits.push('NOPE');\r\n }\r\n\r\n return keyBits.join('|');\r\n}\r\n\r\nfunction isPriceMatch(course, intensity, options, region, price) { // TODO: maybe change method slightly?\r\n return price.courseId === course.CourseId\r\n && price.intensityId === intensity.IntensityId\r\n && ((price.regionId === 0 && region == null) || (region && price.regionId == region.RegionId))\r\n && (course.CourseType.AvailabilityModel === 2\r\n || every(options, function (option) {\r\n return some(price.options, function (myOption) {\r\n return (option.Session && myOption.sessionId === option.Session.SessionId)\r\n || (option.StartDate && myOption.startDate == option.StartDate);\r\n });\r\n }));\r\n}\r\n\r\nfunction getPriceObject(course, intensity, options, region) { // TODO: maybe change method slightly?\r\n var candidatePrices = filter(prices, function (price) {\r\n return isPriceMatch(course, intensity, options, region, price);\r\n });\r\n\r\n if (candidatePrices.length === 0) {\r\n return false;\r\n } else if (candidatePrices.length > 1) {\r\n throw \"Argh, found too many matching prices\";\r\n }\r\n\r\n // if it's expired we remove from the prices and return false\r\n var price = candidatePrices[0];\r\n if (price.expires < new Date().getTime()) {\r\n prices = filter(prices, function (otherPrice) {\r\n return otherPrice !== price;\r\n });\r\n\r\n return false;\r\n }\r\n\r\n return price;\r\n}\r\n\r\nfunction serialize() {\r\n if (!global.localStorage || global.OraWidgets.Options.IsStorageDisabled) {\r\n return;\r\n }\r\n\r\n storage.setItem('__ora_widget_courses', JSON.stringify(courses));\r\n storage.setItem('__ora_widget_prices', JSON.stringify(prices));\r\n storage.setItem('__ora_widget_customer', JSON.stringify(customer));\r\n storage.setItem('__ora_widget_discounts', JSON.stringify(discounts));\r\n storage.setItem('__ora_widget_basket_identifier', JSON.stringify(basketIdentifier));\r\n storage.setItem('__ora_widget_referral_secret', JSON.stringify(referralSecret));\r\n}\r\n\r\nfunction copyAndDeleteProp(obj, from, to) {\r\n if (obj.hasOwnProperty(from) && !obj.hasOwnProperty(to)) {\r\n obj[to] = obj[from];\r\n delete obj[from];\r\n }\r\n}\r\n\r\nfunction deserialize() {\r\n if (!global.localStorage || global.OraWidgets.Options.IsStorageDisabled) {\r\n return;\r\n }\r\n\r\n courses = mapValues(JSON.parse(storage.getItem('__ora_widget_courses')) || {},\r\n function(item) {\r\n copyAndDeleteProp(item, 'course', 'Course');\r\n copyAndDeleteProp(item, 'itemId', 'ItemId');\r\n copyAndDeleteProp(item, 'intensity', 'Intensity');\r\n copyAndDeleteProp(item, 'options', 'Options');\r\n copyAndDeleteProp(item, 'selectedRegions', 'SelectedRegions');\r\n copyAndDeleteProp(item, 'student', 'Student');\r\n if (item.Student && item.Student.DateOfBirth) {\r\n // in the WA version dates can be DD/MM/YYYY with any part missing e.g. 05// so we don't want to parse them as moment\r\n var serialized = item.Student.DateOfBirth;\r\n item.Student.DateOfBirth = moment.utc(item.Student.DateOfBirth, moment.ISO_8601); \r\n if (!item.Student.DateOfBirth.isValid()) {\r\n item.Student.DateOfBirth = serialized;\r\n }\r\n }\r\n\r\n return item;\r\n });\r\n\r\n // to prevent issues with old baskets having no course type\r\n var shouldResetBasket = false;\r\n\r\n forEach(courses, function(course) {\r\n if (!course.Course.CourseType) {\r\n shouldResetBasket = true;\r\n return;\r\n }\r\n });\r\n\r\n if (shouldResetBasket) {\r\n reset();\r\n serialize();\r\n return;\r\n }\r\n\r\n // TODO: maybe change name/structure?\r\n prices = filter(JSON.parse(storage.getItem('__ora_widget_prices')) || [], function (price) {\r\n return price.expires > (new Date().getTime());\r\n });\r\n\r\n customer = JSON.parse(storage.getItem('__ora_widget_customer')) || {};\r\n discounts = JSON.parse(storage.getItem('__ora_widget_discounts')) || [];\r\n basketIdentifier = JSON.parse(storage.getItem('__ora_widget_basket_identifier'));\r\n if (!basketIdentifier) { \r\n // generate new one and save in local storage for later\r\n basketIdentifier = uuidv4();\r\n storage.setItem('__ora_widget_basket_identifier', JSON.stringify(basketIdentifier));\r\n }\r\n \r\n referralSecret = JSON.parse(storage.getItem('__ora_widget_referral_secret')) || null;\r\n}\r\n\r\nfunction notifyApi() {\r\n if (!OraWidgets.Options.IsApiStorageDisabled) {\r\n BasketService.saveBasket(customer, courses, OraWidgets.Options.BasketId, basketIdentifier).then(function (data) {\r\n if (data != undefined && data != null) {\r\n basketIdentifier = data.BasketIdentifier;\r\n referralSecret = data.ReferralSecret;\r\n }\r\n });\r\n }\r\n}\r\n\r\nvar debouncedNotifyApi = debounce(notifyApi, 1000);\r\n\r\nvar searchId = 0;\r\nfunction getDiscounts() {\r\n searchId++;\r\n BasketService.getDiscounts(customer, courses, basketIdentifier)\r\n .done((function createResponseFunc(mySearchId) {\r\n return function (response) {\r\n if (mySearchId !== searchId) {\r\n return;\r\n }\r\n\r\n if (response != undefined && response != null) {\r\n discounts = response;\r\n basketStore.emitChange();\r\n setTimeout(serialize, 1); // record in localStorage\r\n }\r\n };\r\n })(searchId));\r\n}\r\n\r\nvar debouncedGetDiscounts = debounce(getDiscounts, 1000, { leading: true });\r\n\r\nvar basketStore = new BasketStore();\r\nbasketStore.dispatchToken = Dispatcher.register(function (payload) {\r\n switch (payload.type) {\r\n case ActionTypes.GET_LINEITEM_DATA_SUCCESS:\r\n var key = lineItemKeyMaker(payload.courseId, payload.sessionIds, payload.regionId);\r\n lineItems[key] = payload.lineItem;\r\n break;\r\n\r\n case ActionTypes.GET_LINEITEM_STARTED:\r\n isFetchingCourseData = true;\r\n break;\r\n\r\n case ActionTypes.GET_LINEITEM_OPTIONS_SUCCESS:\r\n isFetchingCourseData = false;\r\n courseOptionsData[payload.context] = payload;\r\n break;\r\n\r\n case ActionTypes.ADD_COURSE:\r\n isFetchingCourseData = false;\r\n var newItemId = payload.itemId ? payload.itemId : getNextItemId() + '_';\r\n var newCourse = {};\r\n\r\n newCourse[newItemId] = {\r\n ItemId: newItemId,\r\n ParentItemId: payload.parentItemId,\r\n Course: payload.lineItem.Course,\r\n Intensity: payload.lineItem.Intensity,\r\n Options: payload.lineItem.Options,\r\n Region: payload.lineItem.Region,\r\n Student: payload.student || {},\r\n PriceExcludingDiscount: payload.lineItem.PriceExcludingDiscount,\r\n PriceIncludingDiscount: payload.lineItem.PriceIncludingDiscount,\r\n Deposit: payload.lineItem.Deposit,\r\n TimeZone: payload.lineItem.TimeZone\r\n };\r\n \r\n courses = assign({}, courses, newCourse);\r\n delete courseOptionsData[payload.context];\r\n websiteGetDiscounts();\r\n break;\r\n\r\n case ActionTypes.REMOVE_COURSE:\r\n if (courses.hasOwnProperty(payload.itemId)) {\r\n courses = omitBy(courses,\r\n function(course, id) {\r\n return id == payload.itemId || course.ParentItemId == payload.itemId;\r\n });\r\n }\r\n\r\n websiteGetDiscounts();\r\n break;\r\n\r\n case ActionTypes.REPLACE_COURSE:\r\n if (courses.hasOwnProperty(payload.itemId)) {\r\n courses[payload.itemId] = {\r\n ItemId: payload.itemId,\r\n Course: payload.lineItem.Course,\r\n Intensity: payload.lineItem.Intensity,\r\n Options: payload.lineItem.Options,\r\n Region: payload.lineItem.Region,\r\n Student: payload.student || {},\r\n PriceExcludingDiscount: payload.lineItem.PriceExcludingDiscount,\r\n PriceIncludingDiscount: payload.lineItem.PriceIncludingDiscount,\r\n Deposit: payload.lineItem.Deposit,\r\n TimeZone: payload.lineItem.TimeZone\r\n };\r\n }\r\n\r\n break;\r\n\r\n case ActionTypes.UPDATE_STUDENT:\r\n // update the student within the course\r\n courses = mapValues(courses, function (course, itemId) {\r\n if (itemId === payload.itemId\r\n || course.ParentItemId === payload.itemId) {\r\n var propChange = {};\r\n propChange[payload.property] = payload.value;\r\n var update = assign({}, course.Student, propChange);\r\n return assign({}, course, { Student: update });\r\n }\r\n\r\n return course;\r\n });\r\n\r\n // update the students within the upsell options\r\n if (options && options[payload.itemId] && options[payload.itemId].Options) {\r\n forEach(options[payload.itemId].Options,\r\n function(lineItemOption) {\r\n forEach(lineItemOption.Options,\r\n function(option) {\r\n if (option.LineItem.ItemId === payload.itemId) {\r\n option.LineItem.Student[payload.property] = payload.value;\r\n }\r\n });\r\n });\r\n }\r\n\r\n break;\r\n\r\n case ActionTypes.CHOOSE_STUDENT:\r\n var currentStudent = assign({}, courses[payload.itemId].Student);\r\n\r\n // update the student for the course\r\n courses = mapValues(courses, function (course, itemId) {\r\n if (itemId === payload.itemId\r\n || BasketUtilities.studentEquals(course.Student, currentStudent)) {\r\n var update = assign({}, course.Student, payload.person);\r\n return assign({}, course, { Student: update });\r\n }\r\n\r\n return course;\r\n });\r\n\r\n // update the students within the upsell options\r\n if (options && options[payload.itemId] && options[payload.itemId].Options) {\r\n forEach(options[payload.itemId].Options,\r\n function (lineItemOption) {\r\n forEach(lineItemOption.Options,\r\n function (option) {\r\n if (option.LineItem.ItemId === payload.itemId) {\r\n option.LineItem.Student = payload.person;\r\n }\r\n });\r\n });\r\n }\r\n\r\n websiteGetDiscounts();\r\n break;\r\n\r\n case ActionTypes.UPDATE_CUSTOMER:\r\n customer[payload.property] = payload.value;\r\n\r\n break;\r\n\r\n case ActionTypes.CHOOSE_CUSTOMER:\r\n customer = assign({}, customer, payload.person);\r\n customer.ConfirmEmail = customer.Email;\r\n\r\n break;\r\n\r\n case ActionTypes.MAKE_RESERVATION_STARTED:\r\n reservationFailureResponse = undefined;\r\n reservation = undefined;\r\n isReserving = true;\r\n\r\n isPollingPaymentStatus = false;\r\n paymentStatus = undefined;\r\n paymentFailureResponse = undefined;\r\n order = undefined;\r\n break;\r\n\r\n case ActionTypes.MAKE_RESERVATION_SUCCESS:\r\n reservation = payload.reservation;\r\n paymentIntentId = reservation.PaymentIntentId;\r\n isReserving = false;\r\n if (!payload.makePayment) {\r\n alert('The reservation was successfully made');\r\n reset();\r\n }\r\n break;\r\n\r\n case ActionTypes.MAKE_RESERVATION_FAILURE:\r\n reservationFailureResponse = payload.response;\r\n isReserving = false;\r\n break;\r\n\r\n case ActionTypes.GET_ALL_COUNTRIES:\r\n countries = payload.countries;\r\n break;\r\n \r\n case ActionTypes.GET_ALL_TERMS_AND_CONDITIONS:\r\n termsAndConditions = payload.termsAndConditions;\r\n break;\r\n\r\n case ActionTypes.MAKE_BOOKING_STARTED:\r\n isConfirming = true;\r\n break;\r\n\r\n case ActionTypes.MAKE_BOOKING_SUCCESS:\r\n isConfirmed = true;\r\n isConfirming = false;\r\n break;\r\n\r\n case ActionTypes.CLEAR_BASKET:\r\n // reset everything\r\n reset();\r\n websiteGetDiscounts();\r\n break;\r\n\r\n case ActionTypes.LOAD_BASKET:\r\n reset();\r\n customer = payload.customer;\r\n if (payload.basketIdentifier != undefined && payload.basketIdentifier != null) {\r\n basketIdentifier = payload.basketIdentifier;\r\n }\r\n\r\n websiteGetDiscounts();\r\n break;\r\n\r\n case ActionTypes.ADDED_UPSELL:\r\n upsells.push(payload.lineItemOption);\r\n break;\r\n\r\n case ActionTypes.SEND_UPSELL_REQUEST_BEGIN:\r\n isUpsellSearching = true;\r\n break;\r\n\r\n case ActionTypes.SEND_UPSELL_REQUEST_FAILURE:\r\n isUpsellSearching = false;\r\n options = { \"dummy\": { Options: [] }};\r\n break;\r\n\r\n case ActionTypes.SEND_UPSELL_REQUEST_SUCCESS:\r\n isUpsellSearching = false;\r\n\r\n // we go through and replace the current line items in here with our versions from above\r\n // this keeps the data structure the same but also ensures we have prices!\r\n if (payload.options && payload.options.data && payload.options.data.Options) {\r\n forEach(payload.options.data.Options, function (lineItemOption) {\r\n forEach(lineItemOption.Options, function (option) {\r\n option.LineItem = basketStore.getCourseByItemId(option.LineItem.Key);\r\n });\r\n });\r\n }\r\n\r\n options[payload.options.itemId] = payload.options.data;\r\n break;\r\n \r\n case ActionTypes.CREATE_PAYMENT_INTENT_SUCCESS:\r\n paymentIntentId = payload.paymentIntentId;\r\n break;\r\n\r\n case ActionTypes.POLL_PAYMENT_START:\r\n isPollingPaymentStatus = true;\r\n paymentStatus = paymentStatusEnum.Created;\r\n break;\r\n \r\n case ActionTypes.POLL_PAYMENT_FINISHED:\r\n isPollingPaymentStatus = false;\r\n paymentStatus = payload.paymentStatus;\r\n paymentFailureResponse = payload.paymentFailureReason;\r\n successRedirectUri = payload.redirectUri;\r\n order = payload.order;\r\n break;\r\n\r\n case ActionTypes.DISCOUNTS_FETCHED:\r\n discounts = payload.discounts;\r\n break;\r\n \r\n default:\r\n return;\r\n }\r\n\r\n basketStore.emitChange();\r\n setTimeout(serialize, 1); // record in localStorage\r\n updateBasketCookie();\r\n if (!global.OraWidgets.IsWebsite) { // the website uses apply wizard which provides student info and courses all at once. This makes it simpler to calculate discounts so we don't send this all time\r\n debouncedGetDiscounts();\r\n }\r\n debouncedNotifyApi();\r\n});\r\n\r\nfunction websiteGetDiscounts() {\r\n if (global.OraWidgets.IsWebsite) { \r\n debouncedGetDiscounts();\r\n }\r\n}\r\n\r\nfunction updateBasketCookie() {\r\n // we set the cookie for 30 days as this checkout code does not exist \r\n // on the rest of the site so a session based cookie would fail when someone\r\n // re-visited the site and they did have a basket but hadn't hit the checkout/apply page yet\r\n if (!OraWidgets.Options.IsApiStorageDisabled) {\r\n document.cookie = 'hasBasket=' + (isEmpty(courses) ? 'false' : 'true') + ';max-age=2592000;path=/';\r\n }\r\n}\r\n\r\n// next item id function \r\n// we use milliseconds as this should be unique across tabs/windows for the same computer\r\nfunction getNextItemId() {\r\n return new Date().getTime();\r\n}\r\n\r\n// listen to storage events from other tabs and update if necessary\r\nif (window.addEventListener) {\r\n window.addEventListener('storage', updateBasketFromStorage, false);\r\n} else {\r\n window.attachEvent('onstorage', updateBasketFromStorage);\r\n}\r\n\r\nfunction updateBasketFromStorage() {\r\n deserialize();\r\n basketStore.emitChange();\r\n}\r\n\r\nexport default basketStore;","import assign from 'lodash/assign.js';\r\nimport find from 'lodash/find';\r\nimport filter from 'lodash/filter';\r\nimport map from 'lodash/map';\r\nimport some from 'lodash/some';\r\nimport maxBy from 'lodash/maxBy';\r\nimport flatMap from 'lodash/flatMap';\r\nimport uniqBy from 'lodash/uniqBy';\r\nimport sumBy from 'lodash/sumBy';\r\n\r\nimport BasketStore from '../../stores/BasketStore';\r\n\r\nfunction parseDeepLinks(courses, wizardState) {\r\n // ignore if we're already in a basket (they're adding a second course)\r\n if (BasketStore.numberOfItems() > 0) return {};\r\n\r\n // don't override if they've chosen their accommodation\r\n if (wizardState.Accommodation && Object.keys(wizardState.Accommodation).length > 0) return {};\r\n\r\n var deepLinks = getDeepLinkedCourseSlugs();\r\n if (!deepLinks) return {};\r\n\r\n var regionSlugs = deepLinks[0].split(\"-\");\r\n var courseSlugs = deepLinks[1] ? deepLinks[1].split(\"-\") : [];\r\n\r\n var courseCandidates = search(courses, regionSlugs, courseSlugs, deepLinks[0], deepLinks[1]);\r\n if (!courseCandidates) {\r\n return {};\r\n }\r\n\r\n var result = {};\r\n if (courseCandidates.course) {\r\n result.Course = assign({ IsPreselected: true }, courseCandidates.course);\r\n }\r\n \r\n if (courseCandidates.region) {\r\n result.Region = assign({ IsPreselected: true }, courseCandidates.region);\r\n }\r\n\r\n return result;\r\n}\r\n\r\nfunction getDeepLinkedCourseSlugs() {\r\n var intent = new URL(location).searchParams.get(\"intent\");\r\n if (!intent) return false;\r\n\r\n return intent.replace(/^\\/+/, '').split(\"/\").filter(s => s);\r\n}\r\n\r\nfunction search(courses, regionSlugs, courseSlugs, regionSlug, courseSlug) {\r\n\r\n \r\n // find the courses that have at least one site with a name like in the slug\r\n \r\n var bestRegionMatch = maxBy(\r\n filter(\r\n map(\r\n uniqBy(\r\n filter (\r\n flatMap(courses, function (course) { return course.Sites; }),\r\n function (site) { return site; }\r\n ),\r\n function (site) { return site.RegionId; }\r\n ),\r\n function (site) { return { site: site, score: sumBy(regionSlugs, slug => site.RegionName.toLowerCase().includes(slug) ? 1 : 0)} }\r\n ),\r\n function (regionPair) { return regionPair.score > 0; }\r\n ),\r\n function (regionPair) { return regionPair.score; }\r\n );\r\n\r\n if (!bestRegionMatch) {\r\n return false;\r\n }\r\n\r\n if (!some(courseSlugs)) {\r\n return {\r\n region: bestRegionMatch.site\r\n }\r\n }\r\n \r\n // hand match their slugs\r\n var slugsToCourse = {\r\n 'architecture-design': ['Architecture & Design']\r\n , 'business-innovation-and-entrepreneurship-summer-school-in-london': ['Inventing the Future: Business, Innovation & Entrepreneurship']\r\n , 'business-innovation-entrepreneurship-16-18': ['Inventing the Future: Business, Innovation & Entrepreneurship']\r\n , 'business-innovation-entrepreneurship-13-15': ['Explore Business, Innovation & Entrepreneurship']\r\n , 'business-innovation-and-entrepreneurship-summer-school-in-berkeley': ['Inventing the Future: Business, Innovation & Entrepreneurship']\r\n , 'explore-business-innovation-and-entrepreneurship-summer-school-in-yale': ['Explore Business, Innovation & Entrepreneurship']\r\n , 'business-enterprise': ['Inventing the Future: Business, Innovation & Entrepreneurship']\r\n , 'curing-the-future-medicine': ['Curing the Future: Medicine & Disease']\r\n , 'engineering': ['Designing Tomorrow: Engineering & Technology', 'Explore Engineering & Technology']\r\n , 'explore-engineering': ['Explore Engineering & Technology']\r\n , 'explore-medicine': ['Explore Medicine']\r\n , 'medicine-13-15': ['Explore Medicine']\r\n , 'medicine-16-18': ['Curing the Future: Medicine & Disease']\r\n , 'film-academy-13-18': ['Film Academy: Oxford Through The Lens for ages 13-15', 'Film Academy: Oxford Through The Lens for ages 16-18']\r\n , 'law': ['Law & Politics', 'Law & Trial Advocacy Academy']\r\n , 'learn-english': ['English as a Foreign Language (ages 13-15)', 'English as a Foreign Language (ages 16-18)']\r\n , 'mathematics': ['Mathematics']\r\n , 'philosophy-literature-and-modern-history': ['Philosophy, Literature and Modern History']\r\n , 'racing-extinction': ['Racing Extinction: Climate, Politics & Global Leadership for 13-15', 'Racing Extinction: Climate, Politics & Global Leadership for ages 16-18']\r\n };\r\n\r\n var possibleCourses = slugsToCourse.hasOwnProperty(courseSlug) ? slugsToCourse[courseSlug] : null;\r\n if (!possibleCourses) {\r\n possibleCourses = find(slugsToCourse, function (val, key) { return key.indexOf(courseSlug) === 0; }); \r\n }\r\n\r\n var bestCourseMatch;\r\n if (possibleCourses) {\r\n bestCourseMatch = find(courses, function (c) { return some(possibleCourses, function (courseName) { return c.Name === courseName; })});\r\n if (bestCourseMatch) {\r\n return {\r\n region: bestRegionMatch.site,\r\n course: bestCourseMatch\r\n };\r\n }\r\n } \r\n\r\n // OR find the course with the closest name in the same region\r\n bestCourseMatch = maxBy(\r\n filter(\r\n map(\r\n filter(\r\n courses, \r\n function (course) { return some(course.Sites, site => site.RegionId === bestRegionMatch.site.RegionId); }\r\n ),\r\n function (course) { return { \r\n course: course, \r\n score: sumBy(courseSlugs, function (slug) {\r\n if (course.Name.toLowerCase().includes(slug)) {\r\n if (/\\d+/.test(slug)) { // e.g. 13 in Ages 13-15\r\n return 1;\r\n } else {\r\n return 3; // prioritise 1 word match over 2 number matches\r\n }\r\n }\r\n return 0;\r\n })\r\n } \r\n }\r\n ),\r\n function(courseMatch) { return courseMatch.score > 0; }\r\n ),\r\n function (courseMatch) { return courseMatch.score; }\r\n );\r\n\r\n return {\r\n region: bestRegionMatch.site,\r\n course: bestCourseMatch ? bestCourseMatch.course : false,\r\n }\r\n}\r\n\r\nexport default parseDeepLinks;","import React from 'react';\r\nimport createReactClass from 'create-react-class';\r\n\r\nvar Begin = createReactClass({ \r\n displayName: 'Begin',\r\n render: function() {\r\n var intentParams = new URL(location).searchParams.get(\"intent\"); // if exists, should be oxford-summer-school/course-slug\r\n var intentSplashImage = intentParams \r\n ? 'url(\"https://oxford-royale.com/Apply-' + intentParams.replace(/^\\/+/, '').split(\"/\")[0] + '.jpg\")'\r\n : 'url(\"https://www.oxford-royale.com/Apply-Cover-Image-sm.jpg\")'; \r\n\r\n returnComplete our application form
to secure your place
(takes 5 minutes to complete)
\r\n\r\n \r\n
\r\nYour local time: {this.state.time.toLocaleTimeString()}
\r\n : false }\r\nCurrent time: {calcTime(this.state.time, timeZone.Offset)\r\n }
\r\n : false }\r\nIf you would like to study at more than one campus, you can come back and add another after completing this booking
\r\n\r\n {start.format(\"ddd Do MMMM\")} - {end.format(\"ddd Do MMMM\")}\r\n
\r\n\r\n\r\n {placesForStudent <= 5\r\n ? placesForStudent <= 0 && !isStudentAgeOutOfRange\r\n ? (hasWaitlistBeenJoined\r\n ? Waiting list joined\r\n : Join waiting list)\r\n : Last few spaces\r\n : Good availability}\r\n
\r\n\r\n If you would like {student.FirstName} to study a different programme, please click here to see all our programmes in {GetRegionName(wizard.state.Region)}. Or, to see all our programmes:\r\n
\r\n : false}\r\n \r\nPlease note that the selection you make in one list may determine the availability in another list.
\r\n : false}\r\n {map(this.state.courseScheduleSlotGroups, function (options, courseScheduleSlotGroupId) {\r\n var indicatorGroup = options[0].CourseScheduleSlotGroup;\r\n var name = indicatorGroup ? indicatorGroup.Name : false;\r\n\r\n var choiceElements = [];\r\n for (var i = 0; i < options.length; i++) {\r\n // we show selections where they've been made\r\n // we show a select box for the next choice they need to make\r\n // we show help text where they've not chosen a predecessor\r\n var selectedOptionForIdx = this.getSelectedOption(courseScheduleSlotGroupId, i);\r\n var optionTitle = (!!name\r\n ? name + ' '\r\n : 'Subject ') + (i + 1);\r\n\r\n if (selectedOptionForIdx) { // choice already made, just show it\r\n choiceElements.push(this.renderChoice(\r\n {optionTitle},\r\n \r\n ✕\r\n {selectedOptionForIdx.Name}\r\n ));\r\n } else {\r\n if (i === 0 || this.getSelectedOption(courseScheduleSlotGroupId, i - 1)) { // it's the first slot in the group or it's the next slot after the last selected\r\n // show a selected box\r\n var groupAvailability = this.state.availabilityInCourseScheduleSlotGroups[courseScheduleSlotGroupId];\r\n var courseOptionAvailability = groupBy(\r\n groupAvailability,\r\n function(option) {\r\n return option.CourseOption.CourseOptionId;\r\n }); // { courseOptionId: [Availability, ...] }\r\n courseOptionAvailability = map(\r\n courseOptionAvailability,\r\n function (availability, courseOptionId) { //[Availability, ...]\r\n var courseOption = availability[0].CourseOption;\r\n return {\r\n CourseOption: courseOption,\r\n IsDisabled: this.getSolution(this.state.courseScheduleSlotGroups, this.state.selectedOptions, courseOption) === null,\r\n Options: availability\r\n };\r\n }.bind(this));\r\n\r\n if (removeDisabledOptions) {\r\n courseOptionAvailability = filter(courseOptionAvailability,\r\n function (courseOptionOption) {\r\n return !courseOptionOption.IsDisabled;\r\n });\r\n }\r\n\r\n choiceElements.push(this.renderChoice(\r\n {optionTitle},\r\n ));\r\n } else {\r\n // show a hint\r\n choiceElements.push(this.renderChoice(\r\n {optionTitle},\r\n ));\r\n }\r\n }\r\n }\r\n\r\n returnFor this course, you can arrange your own accommodation and live off campus.\r\n For full details on what's included and what isn't in our non-residential packages,\r\n please contact us on admin@oxford-royale.co.uk or using our live chat facility.\r\n The price to take this course as non-residential is GBP {wizard.state.SessionPricing.NonResidentialPriceIncludingDiscount}\r\n
\r\n \r\n\r\n We have the following accommodation options for {wizard.state.Course.Name} in {wizard.state.Region.RegionName}:\r\n
\r\n\r\n We have the following accommodation options for {wizard.state.Course.Name} in {wizard.state.Region.RegionName} for {from} - {to}:\r\n
}\r\n\r\nEnsuites Available
: false}\r\n{site.Name} not compatible with your choice of accommodation in the {isFirstInLoop ? \"second\" : \"first\"} session. Please choose another.
\r\n : false}\r\n{site.Introduction}
\r\n{upsellOption.Description}
\r\nOnline Summer School
\r\n : false\r\n }\r\n {!booking.Course\r\n ? false\r\n :+ {booking.Course.Name}
\r\n\r\n £{Ui.formatWithCommas(this.getPriceFromUpsells(booking))}\r\n remove\r\n
\r\nTotal fees
\r\n£{Ui.formatWithCommas(total)}
\r\nDue now
\r\n£{Ui.formatWithCommas(totals.deposit)}
\r\nAlternatively, you can:
\r\n\r\n {\r\n values(map(\r\n groupBy(values(this.state.courses), function (booking) {\r\n var student = booking.Student;\r\n return student.FirstName + \"|\" + student.LastName + \"|\" + moment.utc(student.DateOfBirth).format(\"YYYYMMDD\") + \"|\" + student.Gender;\r\n }), function (bookings, identifier) {\r\n var student = bookings[0].Student;\r\n return\r\n \r\n
\r\n }.bind(this)))\r\n }\r\n\r\n\r\n \r\n
\r\n\r\nOr, if you'd like to, you can:
\r\n \r\nThe agent you have selected is on the capped availability model and may have limited or no spaces available for this course
: false}\r\nWould you like the customer to be able to see the financial details (total fees, invoices, outstanding balance etc)?
\r\n {!!this.props.agentId ?\r\n (this.props.areFinancialsVisibleToCustomer && !this.state.areFinancialsVisibleToCustomerAgentDefault ?\r\nThis agent does NOT show financials by default
: (this.state.areFinancialsVisibleToCustomerAgentDefault ?This agent shows financials by default
:This agent does NOT show financials by default
))\r\n : false}\r\n \r\n{alumniMessage}
) : false}\r\nPlease enter your billing address in order to start your payment
\r\nFull Fees. As you are enrolling less than 90 days before your programme, the full fees must be paid immediately after your enrolment is completed. If you elect to pay just the Enrolment Fee now, please settle the balance immediately afterwards via the Oxford Royale Portal. You may receive a reminder from our friendly team via telephone and/or email.
\r\nPay in full now for a saving of GBP {Ui.maybeFormat2Dp(discount.toFixed(2))}
\r\n : false\r\n }\r\n\r\n Prefer to pay by Bank Transfer or another payment method? Click to Contact Us\r\n
\r\n\r\n{message}
;\r\n })}\r\nAlternatively, refresh the page to try the payment again
: false}\r\nReservation held for: {timer}
}\r\n\r\n Please check you've entered the right details for the student who will be attending the course.\r\n You can go back and change their details if required.\r\n
\r\n : false}\r\n \r\n\r\n We currently offer a variety of courses for students aged {lowerAgeBound} - {upperAgeBound} through our website, if you're interested in opportunities for older or youger students, please contact our enrolment team.\r\n
\r\n \r\nSorry, this is not working properly. We've sent the mistake to our tech team so that they can get it fixed asap.
\r\nUsually it's a temporary issue and just starting the application again works.
\r\n \r\n(Sometimes it's a more permanent error - in that case, please contact us below and we'll get your application finished)
\r\nThat's all for now folks