(function () { 'use strict'; var App = { state: { currentStep: 1, totalSteps: 8, isBusy: false, leadId: null, leadRef: null, formCacheKey: 'agpal_registration_form', leadIdKey: 'agpal_registration_lead_id', leadRefKey: 'agpal_registration_lead_ref', practitionerRowCount: 0 }, ctx: window.portalContext || {}, /* Config */ config: { fee: parseFloat((window.portalContext || {}).fee) || 1200, ccSurchargeRate: parseFloat((window.portalContext || {}).ccSurchargeRate) || 0.01927, gstRate: parseFloat((window.portalContext || {}).gstRate) || 0.10, contactTypeId: (window.portalContext || {}).contactTypeId || '', businessId: (window.portalContext || {}).businessId || '', operationalTatDays: parseInt((window.portalContext || {}).operationalTatDays, 10) || 14, practiceTypeGuids: { gp: (window.portalContext || {}).practiceTypeGP || '', ahs: (window.portalContext || {}).practiceTypeAHS || '', mds: (window.portalContext || {}).practiceTypeMDS || '', aboriginal: (window.portalContext || {}).practiceTypeAboriginal || '' }, practitionerTypeMap: { registrar: 1, vocational: 2, other: 10 }, /* Power Automate HTTP-trigger URL that looks up ongc_leadrefid by Lead GUID. Anonymous has no Read on Lead — see notes above createLeadWithRef — so this is the only channel that can return it. Set via portalContext (page/site setting) rather than hardcoded here, so it can be swapped per-environment (Dev/UAT/Prod) without a code change. */ leadRefFlowUrl: (window.portalContext || {}).leadRefFlowUrl || '' }, STATE_MAP: { 'Australian Capital Territory': 107150000, 'New South Wales': 107150001, 'Northern Territory': 107150002, 'Queensland': 107150003, 'South Australia': 107150004, 'Tasmania': 107150005, 'Victoria': 107150006, 'Western Australia': 107150007, /* short-code fallbacks */ 'ACT': 107150000, 'NSW': 107150001, 'NT': 107150002, 'QLD': 107150003, 'SA': 107150004, 'TAS': 107150005, 'VIC': 107150006, 'WA': 107150007 }, mapState: function (v) { if (!v) return undefined; var n = this.STATE_MAP[v]; if (n !== undefined) return n; n = this.STATE_MAP[(v + '').trim()]; return n !== undefined ? n : undefined; }, /* Pending Registration — 14-day threshold Returns true when: - accred-status is "no" (new practice, not yet accredited) - commencement date is more than 14 days from today */ isPendingRegistration: function () { var accred = this.val('accred-status'); if (accred !== 'no') return false; var dateStr = this.val('commencement-date'); if (!dateStr) return false; var commenceDate = new Date(dateStr); if (isNaN(commenceDate.getTime())) return false; var today = new Date(); today.setHours(0, 0, 0, 0); var diffMs = commenceDate.getTime() - today.getTime(); var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); return diffDays > this.config.operationalTatDays; }, dom: {}, /* Init */ init: function () { this.injectSpinnerOverlay(); this.cacheDom(); this.clearSession(); this.bindEvents(); this.handleConditionalUI(); this.updateFeeTable(); this.updateSubmitEnabled(); console.log('[AGPAL] App initialized'); }, cacheDom: function () { var ids = [ 'stepTitle', 'stepCount', 'progressFill', 'btn-submit', 'btn-submit-agpal', 'lead-status-msg', 'lead-card', 'lead-card-content', 'ref-number', 'toast', 'toast-msg', 'payment-method', 'practice-name', 'practice-email', 'group-accred', 'corporate-entity-name', 'practitioners', 'practitioner-dynamic-section', 'practitioner-over10-section', 'practitioner-rows-container', 'surcharge-row', 'gst-row', 'total-row' ]; for (var i = 0; i < ids.length; i++) { this.dom[ids[i]] = document.getElementById(ids[i]); } }, byId: function (id) { return document.getElementById(id); }, val: function (id) { var el = this.byId(id); return el && typeof el.value === 'string' ? el.value.trim() : ''; }, /* ── Toast ──────────────────────────────────────────────── */ showToast: function (message, isError) { if (!this.dom.toast || !this.dom['toast-msg']) return; this.dom['toast-msg'].textContent = message; this.dom.toast.classList.toggle('toast-error', !!isError); this.dom.toast.classList.add('show'); setTimeout(function () { if (App.dom.toast) App.dom.toast.classList.remove('show'); }, 4000); }, injectSpinnerOverlay: function () { if (document.getElementById('agpal-spinner-overlay')) return; var style = document.createElement('style'); style.textContent = '@keyframes agpal-spin{to{transform:rotate(360deg)}}' + '#agpal-spinner-overlay{display:none;position:fixed;inset:0;background:rgba(26,46,74,0.60);' + 'z-index:99999;align-items:center;justify-content:center;flex-direction:column;gap:18px;}'; document.head.appendChild(style); var overlay = document.createElement('div'); overlay.id = 'agpal-spinner-overlay'; overlay.setAttribute('aria-live', 'assertive'); overlay.innerHTML = '
' + '
Please wait…
'; document.body.appendChild(overlay); }, /* Busy */ setBusy: function (busy, buttonText) { this.state.isBusy = busy; var overlay = document.getElementById('agpal-spinner-overlay'); var msgEl = document.getElementById('agpal-spinner-msg'); if (overlay) overlay.style.display = busy ? 'flex' : 'none'; if (msgEl && buttonText) msgEl.textContent = buttonText; if (msgEl && !busy) msgEl.textContent = 'Please wait…'; var buttons = document.querySelectorAll('button'); buttons.forEach(function (btn) { if (busy) { btn.setAttribute('data-was-disabled', btn.disabled ? '1' : '0'); btn.disabled = true; } else { btn.disabled = btn.getAttribute('data-was-disabled') === '1'; } }); if (!busy && this.dom['btn-submit']) { this.dom['btn-submit'].disabled = !this.areDeclarationsChecked(); } }, /* CSRF */ waitForToken: function () { return new Promise(function (resolve, reject) { var cached = (window.portalContext || {}).csrfToken || (window.portalContext || {}).requestVerificationToken; if (cached && cached.length > 20) { resolve(cached); return; } var domEl = document.querySelector('input[name="__RequestVerificationToken"]'); if (domEl && domEl.value) { resolve(domEl.value); return; } var csrfEl = document.getElementById('csrf-token-value'); if (csrfEl && csrfEl.value && csrfEl.value.length > 20) { resolve(csrfEl.value); return; } var attempts = 0; var poll = setInterval(function () { attempts++; var pc = (window.portalContext || {}).csrfToken; if (pc && pc.length > 20) { clearInterval(poll); resolve(pc); return; } var el = document.getElementById('csrf-token-value'); if (el && el.value && el.value.length > 20) { clearInterval(poll); resolve(el.value); return; } if (attempts >= 30) { clearInterval(poll); fetch('/Account/Login', { credentials: 'same-origin', headers: { 'Accept': 'text/html' } }) .then(function (r) { return r.text(); }) .then(function (html) { var m = html.match(/name="__RequestVerificationToken"[^>]*value="([^"]+)"/); if (!m) m = html.match(/value="([^"]{40,})"[^>]*name="__RequestVerificationToken"/); if (m && m[1]) { resolve(m[1]); } else { reject(new Error('CSRF token not available.')); } }) .catch(function (e) { reject(new Error('CSRF: ' + e.message)); }); } }, 150); }); }, /* API helpers */ parseApiError: function (xhr) { try { if (xhr && xhr.responseJSON && xhr.responseJSON.error) return xhr.responseJSON.error.message || 'Unknown error'; } catch (e1) { } try { if (xhr && xhr.responseText) { var p = JSON.parse(xhr.responseText); return (p && p.error && p.error.message) || 'Unknown error'; } } catch (e2) { } return 'Unknown error'; }, extractGuidFromOdata: function (data) { var id = (data && data['@odata.id']) || ''; var m = id.match(/\(([0-9a-f-]{36})\)/i); return m ? m[1] : null; }, extractEntityId: function (data, response) { if (data && typeof data === 'object') { var fromBody = data.leadid || data.accountid || data.contactid || data.ongc_practitionerid || data.ongc_paymentevidenceid || this.extractGuidFromOdata(data); if (fromBody) return fromBody; } var hv = ''; if (response) { if (typeof response.headers === 'object' && typeof response.headers.get === 'function') { hv = response.headers.get('OData-EntityId') || ''; } if (!hv && typeof response.getResponseHeader === 'function') { hv = response.getResponseHeader('OData-EntityId') || response.getResponseHeader('entityid') || ''; } } if (hv) { var m = hv.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i); if (m) return m[0]; } return null; }, /* POST */ dataversePost: async function (entitySet, payload) { var url = '/_api/' + entitySet; if (window.webapi && typeof window.webapi.safeAjax === 'function') { return new Promise(function (resolve, reject) { window.webapi.safeAjax({ type: 'POST', url: url, contentType: 'application/json', data: JSON.stringify(payload), success: function (data, status, xhr) { resolve({ id: App.extractEntityId(data, xhr), data: data || {} }); }, error: function (xhr) { reject(new Error(App.parseApiError(xhr))); } }); }); } var token = await this.waitForToken(); var resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', '__RequestVerificationToken': token, 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' }, body: JSON.stringify(payload) }); if (!resp.ok) { var em = 'API error ' + resp.status; try { var ej = await resp.json(); em = (ej && ej.error && ej.error.message) || em; } catch (e) { } throw new Error(em); } var data = {}; try { data = await resp.json(); } catch (e) { } return { id: this.extractEntityId(data, resp), data: data }; }, /* Misc */ round2: function (n) { return Math.round(n * 100) / 100; }, clearSession: function () { try { sessionStorage.removeItem(this.state.formCacheKey); sessionStorage.removeItem(this.state.leadIdKey); sessionStorage.removeItem(this.state.leadRefKey); } catch (e) { } this.state.leadId = null; this.state.leadRef = null; }, saveState: function () { try { sessionStorage.setItem(this.state.formCacheKey, JSON.stringify(this.collectFormData())); if (this.state.leadId) sessionStorage.setItem(this.state.leadIdKey, this.state.leadId); if (this.state.leadRef) sessionStorage.setItem(this.state.leadRefKey, this.state.leadRef); } catch (e) { } }, resetForm: function () { try { sessionStorage.removeItem(this.state.formCacheKey); sessionStorage.removeItem(this.state.leadIdKey); sessionStorage.removeItem(this.state.leadRefKey); } catch (e) { } document.querySelectorAll('input, select, textarea').forEach(function (el) { if (el.type === 'checkbox' || el.type === 'radio') { el.checked = el.defaultChecked; } else { el.value = el.defaultValue || ''; } }); var c = document.getElementById('practitioner-rows-container'); if (c) c.innerHTML = ''; document.querySelectorAll('.error-msg').forEach(function (el) { el.classList.remove('visible'); }); if (this.dom['lead-card']) this.dom['lead-card'].style.display = 'none'; if (this.dom['lead-status-msg']) this.dom['lead-status-msg'].style.display = 'none'; if (this.dom['ref-number']) this.dom['ref-number'].textContent = '\u2014'; if (this.dom['btn-submit']) { this.dom['btn-submit'].disabled = true; this.dom['btn-submit'].textContent = 'Proceed to Payment'; } this.state.currentStep = 1; this.state.isBusy = false; this.state.leadId = null; this.state.leadRef = null; this.handleConditionalUI(); this.updateFeeTable(); this.goTo(1); }, calcFees: function (isCC) { var base = this.config.fee; var sur = isCC ? this.round2(base * this.config.ccSurchargeRate) : 0; var gst = this.round2((base + sur) * this.config.gstRate); return { base: base, surcharge: sur, gst: gst, total: this.round2(base + sur + gst) }; }, updateFeeTable: function () { var fees = this.calcFees(this.val('payment-method') === 'cc'); if (this.dom['surcharge-row']) this.dom['surcharge-row'].textContent = '$' + fees.surcharge.toFixed(2); if (this.dom['gst-row']) this.dom['gst-row'].textContent = '$' + fees.gst.toFixed(2); if (this.dom['total-row']) this.dom['total-row'].textContent = '$' + fees.total.toFixed(2); }, showError: function (id) { var el = this.byId(id); if (el) el.classList.add('visible'); }, hideError: function (id) { var el = this.byId(id); if (el) el.classList.remove('visible'); }, focusFirstError: function () { var err = document.querySelector('.error-msg.visible'); if (!err) return; var grp = err.closest('.field-group'); if (!grp) return; var inp = grp.querySelector('input, select, textarea'); if (inp) inp.focus(); }, /* Validation */ validateStep: function (step) { var ok = true; function req(id, errId) { if (!App.val(id)) { App.showError(errId); ok = false; } else { App.hideError(errId); } } if (step === 1) { req('accred-status', 'err-accred'); var accred = this.val('accred-status'); if (accred === 'yes-other') { req('other-org-name', 'err-other-org-name'); req('other-expiry-date', 'err-other-expiry-date'); } if (accred === 'no') { req('commencement-date', 'err-commencement-date'); } } if (step === 2) { req('practice-name', 'err-practice-name'); req('practice-abn', 'err-practice-abn'); req('practice-phone', 'err-practice-phone'); req('practice-email', 'err-practice-email'); req('standards', 'err-standards'); } if (step === 3) { req('contact-first', 'err-contact-name'); req('contact-last', 'err-contact-name'); req('contact-position', 'err-contact-position'); req('contact-phone', 'err-contact-phone'); req('contact-email', 'err-contact-email'); req('gp-first', 'err-gp-name'); req('gp-last', 'err-gp-name'); } if (step === 4) { req('addr1', 'err-address'); req('city', 'err-address'); req('state', 'err-address'); req('postcode', 'err-address'); var postalYes = document.querySelector('input[name="postal"][value="yes"]:checked'); if (postalYes) { req('postal-addr1', 'err-postal-address'); req('postal-city', 'err-postal-address'); req('postal-state', 'err-postal-address'); req('postal-postcode', 'err-postal-address'); } else { this.hideError('err-postal-address'); } } if (step === 5) { req('phn', 'err-phn'); req('practice-type', 'err-practice-type'); req('group-accred', 'err-group-accred'); var ga = this.val('group-accred'); if (ga === 'yes-corporate' || ga === 'yes-group') { req('corporate-entity-name', 'err-corporate-entity'); } else { this.hideError('err-corporate-entity'); } } if (step === 6) { var pCount = this.val('practitioners'); if (!pCount) { this.showError('err-practitioners'); ok = false; } else { this.hideError('err-practitioners'); } if (pCount && pCount !== 'over-10') { var n = parseInt(pCount, 10); for (var i = 1; i <= n; i++) { if (!this.val('prac-first-' + i) || !this.val('prac-last-' + i) || !this.val('prac-hours-' + i) || !this.val('prac-type-' + i)) { this.showError('err-practitioner-rows'); ok = false; break; } else { this.hideError('err-practitioner-rows'); } } } } /* Step 7 — payment method only required for non-pending registrations */ if (step === 7) { if (!this.isPendingRegistration()) { req('payment-method', 'err-payment'); } else { this.hideError('err-payment'); } } if (!ok) this.focusFirstError(); return ok; }, areDeclarationsChecked: function () { var basicChecked = !!( this.byId('cb-auth') && this.byId('cb-auth').checked && this.byId('cb-terms') && this.byId('cb-terms').checked && this.byId('cb-prof') && this.byId('cb-prof').checked ); if (!basicChecked) return false; /* For pending registrations the additional acknowledgment checkbox is mandatory */ if (this.isPendingRegistration()) { return !!(this.byId('cb-pending-ack') && this.byId('cb-pending-ack').checked); } return true; }, updateSubmitEnabled: function () { if (!this.dom['btn-submit']) return; /* Pending path: enabled purely by checkbox state — lead created on submit. Normal path: also requires leadId to exist (set after page 7 creates the lead). */ var declarationsOk = this.areDeclarationsChecked(); var canSubmit = this.isPendingRegistration() ? declarationsOk && !this.state.isBusy : declarationsOk && !this.state.isBusy && !!this.state.leadId; this.dom['btn-submit'].disabled = !canSubmit; }, /* Data collection */ collectPractitioners: function () { var result = []; var v = this.val('practitioners'); if (!v || v === 'over-10') return result; var count = parseInt(v, 10); for (var i = 1; i <= count; i++) { var firstName = this.val('prac-first-' + i); var lastName = this.val('prac-last-' + i); result.push({ title: this.val('prac-title-' + i), firstName: firstName, lastName: lastName, name: (firstName + ' ' + lastName).trim(), hours: this.val('prac-hours-' + i), type: this.val('prac-type-' + i) }); } return result; }, collectFormData: function () { var accredVal = this.val('accred-status'); var postalSameAsStreet = !document.querySelector('input[name="postal"][value="yes"]:checked'); return { accredStatus: accredVal, otherOrgName: accredVal === 'yes-other' ? this.val('other-org-name') : '', otherExpiry: accredVal === 'yes-other' ? this.val('other-expiry-date') : '', commenceDate: accredVal === 'no' ? this.val('commencement-date') : '', isPending: this.isPendingRegistration(), practiceName: this.val('practice-name'), practiceAbn: this.val('practice-abn'), practicePhone: this.val('practice-phone'), practiceFax: this.val('practice-fax'), practiceEmail: this.val('practice-email'), standards: this.val('standards'), contactPrefix: this.val('contact-prefix'), contactFirst: this.val('contact-first'), contactLast: this.val('contact-last'), contactPosition: this.val('contact-position'), contactPhone: this.val('contact-phone'), contactEmail: this.val('contact-email'), contactMethod: this.val('contact-method'), gpPrefix: this.val('gp-prefix'), gpFirst: this.val('gp-first'), gpLast: this.val('gp-last'), addr1: this.val('addr1'), addr2: this.val('addr2'), city: this.val('city'), state: this.val('state'), postcode: this.val('postcode'), postalSameAsStreet: postalSameAsStreet, postalAddr1: postalSameAsStreet ? '' : this.val('postal-addr1'), postalAddr2: postalSameAsStreet ? '' : this.val('postal-addr2'), postalCity: postalSameAsStreet ? '' : this.val('postal-city'), postalState: postalSameAsStreet ? '' : this.val('postal-state'), postalPostcode: postalSameAsStreet ? '' : this.val('postal-postcode'), phn: this.val('phn'), pipId: this.val('pip-id'), practiceType: this.val('practice-type'), groupAccred: this.val('group-accred'), corporateEntity: this.val('corporate-entity-name'), practitionerCount: this.val('practitioners'), practitioners: this.collectPractitioners(), paymentMethod: this.val('payment-method') }; }, /* Payloads */ buildLeadPayload: function (d) { var isPracticeAccredited = d.accredStatus === 'yes-agpal' ? 107150001 : d.accredStatus === 'yes-other' ? 107150002 : 107150000; var groupAccredValue = d.groupAccred === 'yes-corporate' ? 107150001 : d.groupAccred === 'yes-group' ? 107150002 : 107150000; var practiceTypeGuid = this.config.practiceTypeGuids[d.practiceType] || null; /* For pending registrations, flag the subject and status so can route the notification to adminbd@agpal.com.au (BCC marketingteam@agpal.com.au) and NOT progress into downstream systems Internally. */ var subjectLine = d.isPending ? 'AGPAL Registration (ON HOLD \u2013 Pending Commencement) \u2013 ' + d.contactFirst + ' ' + d.contactLast : 'AGPAL Registration \u2013 ' + d.contactFirst + ' ' + d.contactLast; var payload = { subject: subjectLine, firstname: d.contactFirst, lastname: d.contactLast || d.practiceName, jobtitle: d.contactPosition, emailaddress1: d.contactEmail || d.practiceEmail, telephone1: d.contactPhone, mobilephone: d.practicePhone, fax: d.practiceFax, companyname: d.practiceName, leadsourcecode: 8, address1_line1: d.addr1, address1_line2: d.addr2, address1_city: d.city, address1_stateorprovince: d.state, address1_postalcode: d.postcode, address1_country: 'Australia', ongc_state: App.mapState(d.state), /* integer choice */ ongc_country: 107150000, /* Australia */ address2_line1: d.postalAddr1, address2_line2: d.postalAddr2, address2_city: d.postalCity, address2_stateorprovince: d.postalState, address2_postalcode: d.postalPostcode, address2_country: d.postalAddr1 ? 'Australia' : '', ongc_enquirytype: 8, ongc_practicename: d.practiceName, ongc_practiceabn: d.practiceAbn, ongc_practicephone: d.practicePhone, ongc_pipid: d.pipId, ongc_corporateentity: d.corporateEntity, ongc_groupname: d.corporateEntity, ongc_postalsameasstreet: d.postalSameAsStreet, ongc_ispracticeaccredited: isPracticeAccredited, ongc_grouppracticesalreadyaccredited: groupAccredValue, ongc_accreditationexpirydate: d.otherExpiry || null, ongc_duedate: d.commenceDate || null, ongc_operationalcommencementdate: d.commenceDate || null, ongc_accreditationorganisationname: d.otherOrgName, statuscode: d.isPending ? 107150002 : undefined, ongc_webresponse: App.buildAgpalWebResponseHtml(d) }; /* * NOTE: ongc_PracticeType and ongc_Business odata.bind are NOT set here. * Anonymous users lack Append To on both related tables so any odata.bind * causes a 90040106 permission error. * * We also cannot add fake columns (ongc_practicetypeguid etc.) because * Dataverse rejects unknown properties with a 9004010D error. * * Solution: encode both GUIDs as a small JSON object in the Lead's * standard `description` field. The plugin reads it, sets both lookups * as SYSTEM, then clears the description field. */ /* * Encode lookup GUIDs + extra fields into description for the plugin. * Keys: * practiceTypeGuid — resolves ongc_PracticeType lookup (Append To blocked for anon) * businessGuid — resolves ongc_Business lookup (same reason) * corporateEntity — used as Account name fallback / ongc_entityname * practiceEmail — emailaddress2 on Account * practicePhone — address1_telephone1 on Account */ var pluginMeta = {}; if (practiceTypeGuid) pluginMeta.practiceTypeGuid = practiceTypeGuid; if (App.config.businessId) pluginMeta.businessGuid = App.config.businessId; if (d.corporateEntity) pluginMeta.corporateEntity = d.corporateEntity; if (d.practiceEmail) pluginMeta.practiceEmail = d.practiceEmail; if (d.practicePhone) pluginMeta.practicePhone = d.practicePhone; if (d.practiceAbn) pluginMeta.practiceAbn = d.practiceAbn; if (d.practiceName) pluginMeta.practiceName = d.practiceName; /* Practitioners: created + N:N associated server-side by the plugin (SYSTEM) from this same blob — see note above buildPractitionerPayloads. Anonymous never touches the ongc_practitioner table directly. */ var pracPayloads = this.buildPractitionerPayloads(d); if (pracPayloads.length) pluginMeta.practitioners = pracPayloads; payload['description'] = '__PLUGIN_META__' + JSON.stringify(pluginMeta); /* * NOTE: parentaccountid and parentcontactid are intentionally NOT set here. * Account and Contact lookup/creation and linking is handled server-side * by the AgpalLeadCreatePlugin Dataverse plugin, which runs as SYSTEM. * Anonymous portal users have NO permission to read, write, or query * Account or Contact records. */ Object.keys(payload).forEach(function (k) { if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k]; }); return payload; }, /* Practitioners: anonymous never calls the ongc_practitioner API at all. Every client-side approach ($ref associate, and POSTing to the N:N nav property as a contained entity) hit the same Power Pages EntityPermissionCreateIsMissing check on Lead, regardless of which table permissions were granted — short of Read/Write/Append/AppendTo on both tables (an exposure we're avoiding), there's no working client-side path. So instead, practitioner rows are embedded in the same __PLUGIN_META__ blob already used for ongc_PracticeType/ ongc_Business, and PortalLeadCreatePlugin (SYSTEM) creates each Practitioner and associates it to the Lead during the same Lead Create transaction. Anonymous only ever needs Create on Lead. */ buildPractitionerPayloads: function (d) { var rows = d.practitioners || []; return rows.map(function (prac) { var hours = parseFloat(prac.hours); var fullName = (prac.firstName + ' ' + prac.lastName).trim(); var payload = { ongc_title: prac.title || '', ongc_firstname: prac.firstName || '', ongc_lastname: prac.lastName || '', ongc_name: fullName || prac.name || '', ongc_type: App.config.practitionerTypeMap[prac.type] !== undefined ? App.config.practitionerTypeMap[prac.type] : 10, ongc_hoursperweek: isNaN(hours) ? null : hours }; Object.keys(payload).forEach(function (k) { if (k === 'ongc_type') return; if (payload[k] === null || payload[k] === undefined || payload[k] === '') delete payload[k]; }); return payload; }); }, /* Lead card */ renderLeadCard: function (d) { if (!this.dom['lead-card-content']) return; function row(label, value) { return '
' + label + '' + (value || '\u2014') + '
'; } this.dom['lead-card-content'].innerHTML = row('Reference', App.state.leadRef || (App.state.leadId || '').toUpperCase()) + row('Practice', d.practiceName) + row('ABN', d.practiceAbn) + row('Contact', (d.contactFirst + ' ' + d.contactLast).trim()) + row('Email', d.contactEmail || d.practiceEmail) + row('Phone', d.contactPhone) + row('Payment', this.val('payment-method') === 'eft' ? 'Bank Transfer (EFT)' : 'Credit Card'); if (this.dom['lead-card']) this.dom['lead-card'].style.display = 'block'; if (this.dom['ref-number']) this.dom['ref-number'].textContent = App.state.leadRef || (App.state.leadId || '').toUpperCase(); }, /* Create the Lead. NOTE: ongc_leadrefid can NOT come back in this response — Dataverse requires Read privilege on Lead to include any field beyond the primary key via $select/return=representation, proven across every approach tried (confirmed via a direct $select test that returned EntityPermissionReadIsMissing). Anonymous only has Create, by design, so this call only returns leadid. The reference number is fetched separately via fetchLeadRefFromFlow. */ createLeadWithRef: async function (payload) { var token = await this.waitForToken(); var resp = await fetch( '/_api/leads', { method: 'POST', headers: { 'Content-Type': 'application/json', '__RequestVerificationToken': token, 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' }, body: JSON.stringify(payload) } ); if (!resp.ok) { var em = 'API error ' + resp.status; try { var ej = await resp.json(); em = (ej && ej.error && ej.error.message) || em; } catch (e) { } throw new Error(em); } var data = {}; try { data = await resp.json(); } catch (e) { } return { id: this.extractEntityId(data, resp), data: data }; }, /* Look up ongc_leadrefid via the Power Automate HTTP-trigger flow — the only channel that can return it without granting anonymous Read on Lead (a deliberate constraint, not an oversight). Fails soft: any error here just leaves leadRef null and the card shows "Pending" — it never blocks or fails the registration itself. */ fetchLeadRefFromFlow: async function (leadId) { if (!App.config.leadRefFlowUrl || !leadId) return null; try { var resp = await fetch(App.config.leadRefFlowUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ leadId: leadId }) }); if (!resp.ok) { console.warn('[AGPAL] leadRef flow HTTP', resp.status); return null; } var data = await resp.json(); return (data && data.leadRef) || null; } catch (e) { console.warn('[AGPAL] leadRef flow error:', e.message); return null; } }, createLeadAndChildren: async function () { var d = this.collectFormData(); this.setBusy(true, 'Submitting registration...10% \u2026'); this.saveState(); try { /* 1 — Lead + Practitioners (embedded in pluginMeta) + Account/Contact linking — all handled server-side by the plugin in one Create. */ var lead = await this.createLeadWithRef(this.buildLeadPayload(d)); this.state.leadId = lead.id; if (!this.state.leadId) throw new Error('Lead GUID not returned. Check API permissions.'); console.log('[AGPAL] Lead created:', this.state.leadId); console.log('[AGPAL] Account/Contact/Practitioner linking delegated to server-side plugin.'); this.saveState(); /* ongc_leadrefid via the Flow lookup — see fetchLeadRefFromFlow. Non-blocking: if this fails, leadRef stays null and the card shows "Pending" rather than holding up the registration. */ this.state.leadRef = await this.fetchLeadRefFromFlow(this.state.leadId); this.saveState(); /* Done */ if (d.isPending) { /* Pending path: navigate to success page, render lead card there */ this.goToSuccess(true, d); this.showToast('Registration submitted successfully.'); } else { /* Normal path: show lead card on page 8, user proceeds to payment */ this.renderLeadCard(d); if (this.dom['lead-status-msg']) { this.dom['lead-status-msg'].style.display = 'block'; this.dom['lead-status-msg'].className = 'lead-status-ready'; this.dom['lead-status-msg'].textContent = 'Registration record submitted. You may now proceed to payment.'; } this.goTo(8); /* Re-apply pending UI now that we are on page 8 */ this.handleConditionalUI(); this.showToast('Registration submitted successfully.'); } } catch (err) { console.error('[AGPAL] Lead creation failed', err); if (this.dom['lead-status-msg']) { this.dom['lead-status-msg'].style.display = 'block'; this.dom['lead-status-msg'].className = 'lead-status-error'; this.dom['lead-status-msg'].textContent = 'Registration could not be submitted. Please try again.'; } this.showToast(err.message || 'Registration creation failed.', true); } finally { this.setBusy(false); this.updateSubmitEnabled(); } }, /* Payment evidence */ createPaymentEvidence: async function (stripeResult) { stripeResult = stripeResult || {}; var isCC = this.val('payment-method') === 'cc'; var fees = this.calcFees(isCC); var now = new Date().toISOString(); /* * DO NOT include ongc_Lead@odata.bind here. * Anonymous portal users lack Append To on lead → ongc_paymentevidence, * which causes error 90040106 on any @odata.bind that references lead. * * The Lead linkage is handled by AgpalPaymentEvidenceCreatePlugin: * Entity: ongc_paymentevidence | Message: Create | Stage: Post-Op | Run As: System * * The leadId is carried in ongc_description (JSON) which already * exists on this table. The plugin reads it and sets ongc_Lead as SYSTEM. * No extra column or description field needed on ongc_paymentevidence. */ var leadId = this.state.leadId || ''; var payload = { ongc_payername: stripeResult.fullName || (this.val('contact-first') + ' ' + this.val('contact-last')).trim(), ongc_paymentamount: fees.base, ongc_surchargefee: fees.surcharge, ongc_gst: fees.gst, ongc_totalamount: fees.total, ongc_paymentreference: stripeResult.paymentIntentId || stripeResult.chargeId || '', ongc_paymentstatus: stripeResult.status || (isCC ? 'succeeded' : 'pending-eft'), /* Payment Mode (Choice): 107150000 = Credit Card, 107150001 = Bank Transfer */ ongc_paymentmode: isCC ? 107150000 : 107150001, ongc_paymentdate: stripeResult.paymentDate || now, /* Embed leadId in the response message JSON so the plugin can extract it. __leadId__ is a known key the plugin looks for. */ ongc_description: JSON.stringify( Object.assign({ __leadId__: leadId }, stripeResult.rawResponse || stripeResult || {}), null, 2 ) }; Object.keys(payload).forEach(function (k) { if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k]; }); console.log('[AGPAL] Payment Evidence payload (no odata.bind):', JSON.stringify(payload)); return this.dataversePost('ongc_paymentevidences', payload); }, /* Submit */ handleSubmit: async function () { if (!this.areDeclarationsChecked()) { this.showError('err-submit'); this.focusFirstError(); return; } this.hideError('err-submit'); this.hideError('err-pending-ack'); /* Pending path: create lead record NOW (on page 8 "Confirm and Submit"). createLeadAndChildren detects isPending and routes to goToSuccess(true). */ if (this.isPendingRegistration()) { await this.createLeadAndChildren(); return; } /* Normal path: lead already created on page 7. Credit card -> Stripe payment. Bank transfer (EFT) -> no Stripe, registration is finalised immediately. */ this.setBusy(true, 'Processing\u2026'); this.saveState(); try { if (!this.state.leadId) { throw new Error('Registration record not found. Please go back and recreate the registration.'); } if (this.val('payment-method') === 'eft') { await this.completeEftRegistration(); } else { await this.initiateStripePayment(); } } catch (err) { console.error('[AGPAL] Submit failed', err); this.showToast(err.message || 'Submission failed.', true); this.setBusy(false); this.updateSubmitEnabled(); } }, /* Bank transfer (EFT): no Stripe session — create the payment evidence record as pending-eft and complete the registration straight away. Mirrors what agpalHandleStripeReturn does for the CC/Stripe path, minus the redirect and Stripe-specific response data. */ completeEftRegistration: async function () { this.setBusy(true, 'Finalising registration\u2026'); await this.createPaymentEvidence({ status: 'pending-eft' }); this.setBusy(true, 'Updating registration record\u2026'); await this.appendPaymentToWebResponse({ status: 'pending-eft' }, this.calcFees(false), 'Bank Transfer'); this.goToSuccess(false); this.showToast('Registration submitted successfully.'); }, initiateStripePayment: function () { var fees = this.calcFees(true); var params = new URLSearchParams({ leadId: this.state.leadId || '', leadRef: this.state.leadRef || '', amount: fees.total.toFixed(2), base: fees.base.toFixed(2), surcharge: fees.surcharge.toFixed(2), gst: fees.gst.toFixed(2), email: this.val('contact-email') || this.val('practice-email'), name: (this.val('contact-first') + ' ' + this.val('contact-last')).trim(), ref: 'AGPAL Registration \u2013 ' + this.val('practice-name'), returnUrl: window.location.href.split('?')[0] }); window.location.href = '/agpal-stripe-checkout?' + params.toString(); }, /* Navigation */ goTo: function (step) { document.querySelectorAll('.step-page').forEach(function (p) { p.classList.remove('active'); }); var page = this.byId('page-' + step); if (page) page.classList.add('active'); document.querySelectorAll('.step-dot').forEach(function (d, i) { d.classList.toggle('active', i + 1 === step); d.classList.toggle('completed', i + 1 < step); }); var titles = [ 'Step 1: Accreditation Status', 'Step 2: Practice Information', 'Step 3: Practice Contacts', 'Step 4: Practice Address', 'Step 5: Further Practice Details', 'Step 6: Practitioner Details', 'Step 7: Self Assessment Fee', 'Step 8: Authorisation & Submission' ]; if (step <= this.state.totalSteps) { if (this.dom.stepTitle) this.dom.stepTitle.textContent = titles[step - 1]; if (this.dom.stepCount) this.dom.stepCount.textContent = 'Page ' + step + ' of ' + this.state.totalSteps; if (this.dom.progressFill) this.dom.progressFill.style.width = ((step / this.state.totalSteps) * 100) + '%'; } this.state.currentStep = step; window.scrollTo(0, 0); }, goToSuccess: function (isPending, d) { document.querySelectorAll('.step-page').forEach(function (p) { p.classList.remove('active'); }); var s = this.byId('page-success'); if (s) s.classList.add('active'); /* Show the appropriate success message depending on registration type */ var msgNormal = this.byId('success-msg-normal'); var msgPending = this.byId('success-msg-pending'); if (msgNormal) msgNormal.style.display = isPending ? 'none' : 'block'; if (msgPending) msgPending.style.display = isPending ? 'block' : 'none'; /* Reference number */ if (this.dom['ref-number']) { this.dom['ref-number'].textContent = App.state.leadRef || (App.state.leadId || '').toUpperCase(); } /* Pending path: render lead status + card inside the success page */ if (isPending && d) { var statusEl = this.byId('pending-lead-status-msg'); if (statusEl) { statusEl.textContent = 'Your details have been recorded. A member of the AGPAL Client Liaison team will contact you closer to your opening date to progress your registration.'; statusEl.style.display = 'block'; } this.renderLeadCardPending(d); } this.setBusy(false); window.scrollTo(0, 0); }, /* Render lead card on the success page */ renderLeadCardPending: function (d) { var container = this.byId('pending-lead-card-content'); if (!container) return; function row(label, value) { return '
' + label + '' + (value || '\u2014') + '
'; } container.innerHTML = row('REFERENCE', App.state.leadRef || (App.state.leadId || '').toUpperCase()) + row('PRACTICE', d.practiceName) + row('ABN', d.practiceAbn) + row('CONTACT', (d.contactFirst + ' ' + d.contactLast).trim()) + row('EMAIL', d.contactEmail) + row('PHONE', d.contactPhone); var card = this.byId('pending-lead-card'); if (card) card.style.display = 'block'; }, /* Conditional UI */ handleConditionalUI: function () { var accred = this.val('accred-status'); var yesAgpal = this.byId('group-yes-agpal'); var yesOther = this.byId('group-yes-other'); var noGroup = this.byId('group-no'); var btnNext1 = this.byId('btn-next-1'); var btnSubmitAg = this.byId('btn-submit-agpal'); if (yesAgpal) yesAgpal.style.display = accred === 'yes-agpal' ? 'block' : 'none'; if (yesOther) yesOther.style.display = accred === 'yes-other' ? 'block' : 'none'; if (noGroup) noGroup.style.display = accred === 'no' ? 'block' : 'none'; if (btnNext1) btnNext1.style.display = accred === 'yes-agpal' ? 'none' : 'inline-block'; if (btnSubmitAg) btnSubmitAg.style.display = accred === 'yes-agpal' ? 'inline-block' : 'none'; var ga = this.val('group-accred'); var corpWrap = this.byId('group-corporate-entity'); if (corpWrap) corpWrap.style.display = (ga === 'yes-corporate' || ga === 'yes-group') ? 'block' : 'none'; var postalYes = document.querySelector('input[name="postal"][value="yes"]:checked'); var postalWrap = this.byId('postal-address-group'); if (postalWrap) postalWrap.style.display = postalYes ? 'block' : 'none'; var v = this.val('practitioners'); if (this.dom['practitioner-dynamic-section']) this.dom['practitioner-dynamic-section'].style.display = 'none'; if (this.dom['practitioner-over10-section']) this.dom['practitioner-over10-section'].style.display = 'none'; if (v === 'over-10') { if (this.dom['practitioner-over10-section']) this.dom['practitioner-over10-section'].style.display = 'block'; this.state.practitionerRowCount = 0; if (this.dom['practitioner-rows-container']) this.dom['practitioner-rows-container'].innerHTML = ''; } else if (v) { var count = parseInt(v, 10); if (!isNaN(count) && count >= 1 && count <= 10) { this.buildPractitionerRows(count); if (this.dom['practitioner-dynamic-section']) this.dom['practitioner-dynamic-section'].style.display = 'block'; } } /* Pending Registration UI — driven by isPendingRegistration() */ var isPending = this.isPendingRegistration(); /* Page 1 — pending notice */ var pendingNoticeP1 = this.byId('pending-notice-p1'); if (pendingNoticeP1) pendingNoticeP1.style.display = isPending ? 'block' : 'none'; /* Page 7 — fee info box (two versions) */ var feeInfoNormal = this.byId('fee-info-normal'); var feeInfoPending = this.byId('fee-info-pending'); if (feeInfoNormal) feeInfoNormal.style.display = isPending ? 'none' : 'block'; if (feeInfoPending) feeInfoPending.style.display = isPending ? 'block' : 'none'; /* Page 7 — payment method dropdown (hidden for pending) */ var paymentMethodGroup = this.byId('payment-method-group'); if (paymentMethodGroup) paymentMethodGroup.style.display = isPending ? 'none' : 'block'; /* Page 7 — "What happens next?" box (two versions) */ var nextStepsNormal = this.byId('next-steps-normal'); var nextStepsPending = this.byId('next-steps-pending'); if (nextStepsNormal) nextStepsNormal.style.display = isPending ? 'none' : 'block'; if (nextStepsPending) nextStepsPending.style.display = isPending ? 'block' : 'none'; /* Page 7 — Next button label */ var btnNext7 = this.byId('btn-next-7'); if (btnNext7) btnNext7.textContent = isPending ? 'Confirm and Next' : 'Next'; /* Page 8 — "Save and return" notice (only relevant if payment is happening) */ var saveReturnNotice = this.byId('save-return-notice'); if (saveReturnNotice) saveReturnNotice.style.display = isPending ? 'none' : 'block'; /* Page 8 — pending notice at top */ var pendingNoticeP8 = this.byId('pending-notice-p8'); if (pendingNoticeP8) pendingNoticeP8.style.display = isPending ? 'block' : 'none'; /* Page 8 — pending acknowledgment checkbox group */ var pendingAckGroup = this.byId('pending-ack-group'); if (pendingAckGroup) pendingAckGroup.style.display = isPending ? 'block' : 'none'; /* Page 8 — fee table (hidden for pending — no payment being made) */ var feeTableWrap = this.byId('fee-table-wrap'); if (feeTableWrap) feeTableWrap.style.display = isPending ? 'none' : 'block'; /* Page 8 — lead-card/status-msg wrap: Normal path → visible (populated after page 7 Next creates the lead). Pending path → hidden here; shown on the success page instead after Confirm and Submit. */ var p8LeadWrap = this.byId('p8-lead-wrap'); if (p8LeadWrap) p8LeadWrap.style.display = isPending ? 'none' : 'block'; /* Page 8 — submit button label and enabled state for pending Pending: enabled by declarations alone (no lead pre-creation required). Normal: enabled only after lead GUID confirmed (updateSubmitEnabled handles this). */ var submitBtn = this.dom['btn-submit']; if (submitBtn) { submitBtn.textContent = isPending ? 'Confirm and Submit' : 'Proceed to Payment'; if (isPending) { submitBtn.disabled = !this.areDeclarationsChecked(); } } }, buildPractitionerRows: function (count) { if (!this.dom['practitioner-rows-container']) return; if (this.state.practitionerRowCount === count) return; this.state.practitionerRowCount = count; this.dom['practitioner-rows-container'].innerHTML = ''; for (var i = 1; i <= count; i++) { var row = document.createElement('div'); row.className = 'practitioner-row'; row.style.cssText = 'display:grid;grid-template-columns:0.6fr 1fr 1fr 1fr 0.7fr;gap:12px;margin-bottom:20px;align-items:end;'; row.innerHTML = '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
'; this.dom['practitioner-rows-container'].appendChild(row); } }, /* Event binding */ bindEvents: function () { var self = this; ['accred-status', 'group-accred', 'practitioners', 'payment-method'].forEach(function (id) { var el = self.byId(id); if (!el) return; el.addEventListener('change', function () { self.handleConditionalUI(); self.updateFeeTable(); self.saveState(); }); }); /* Commencement date drives the pending-registration logic — re-evaluate conditional UI whenever it changes */ var commenceDateEl = self.byId('commencement-date'); if (commenceDateEl) { commenceDateEl.addEventListener('change', function () { self.handleConditionalUI(); self.updateFeeTable(); self.saveState(); }); } document.querySelectorAll('input[name="postal"]').forEach(function (el) { el.addEventListener('change', function () { self.handleConditionalUI(); self.saveState(); }); }); [1, 2, 3, 4, 5, 6].forEach(function (step) { var btn = self.byId('btn-next-' + step); if (!btn) return; btn.addEventListener('click', function () { if (self.validateStep(step)) { self.saveState(); self.goTo(step + 1); } }); }); var btnNext7 = this.byId('btn-next-7'); if (btnNext7) { btnNext7.addEventListener('click', function () { if (!self.validateStep(7)) return; if (self.isPendingRegistration()) { /* Pending path: page 7 just navigates to page 8. Lead is created later on page 8 "Confirm and Submit". */ self.saveState(); self.goTo(8); self.handleConditionalUI(); } else { /* Normal path: create lead + practitioners, then navigate to page 8. Account/Contact linking handled by server-side plugin. */ self.createLeadAndChildren(); } }); } [2, 3, 4, 5, 6, 7, 8].forEach(function (step) { var btn = self.byId('btn-prev-' + step); if (!btn) return; btn.addEventListener('click', function () { if (!self.state.isBusy) self.goTo(step - 1); }); }); var submit = this.byId('btn-submit'); if (submit) submit.addEventListener('click', function () { self.handleSubmit(); }); /* Close button on success page — closes tab or falls back to portal home */ var btnClose = this.byId('btn-close-success'); if (btnClose) { btnClose.addEventListener('click', function () { try { window.close(); /* window.close() silently fails if page wasn't opened by script; fall back to home after a short delay */ setTimeout(function () { window.location.href = window.location.origin + '/'; }, 300); } catch (e) { window.location.href = window.location.origin + '/'; } }); } ['cb-auth', 'cb-terms', 'cb-prof', 'cb-pending-ack'].forEach(function (id) { var cb = self.byId(id); if (!cb) return; cb.addEventListener('change', function () { self.updateSubmitEnabled(); self.saveState(); }); }); var btnSubmitAgpal = this.byId('btn-submit-agpal'); if (btnSubmitAgpal) { btnSubmitAgpal.addEventListener('click', function () { self.showToast('Thank you. Please call 1300 362 111 to re-accredit.'); }); } document.querySelectorAll('input, select, textarea').forEach(function (el) { el.addEventListener('change', function () { self.saveState(); }); }); }, /* ── Web response HTML ──────────────────────────────────── */ buildAgpalWebResponseHtml: function (d) { var now = new Date().toLocaleString('en-AU', { timeZone: 'Australia/Sydney', dateStyle: 'medium', timeStyle: 'short' }); function row(label, value) { return '' + '' + (label || '') + '' + '' + (value || '\u2014') + '' + ''; } function section(title) { return '' + title + ''; } var html = '
' + '' + '' + ''; if (d.isPending) { html += ''; } html += section('1. Accreditation Status'); var aL = { 'yes-agpal': 'Yes \u2014 currently accredited with AGPAL', 'yes-other': 'Yes \u2014 accredited with another organisation', 'no': 'No \u2014 not currently accredited' }; html += row('Status', aL[d.accredStatus] || d.accredStatus); if (d.otherOrgName) html += row('Other Organisation', d.otherOrgName); if (d.otherExpiry) html += row('Expiry Date', d.otherExpiry); if (d.commenceDate) html += row('Desired Commencement Date', (function (iso) { var p = iso.split('-'); return p.length === 3 ? p[2] + '-' + p[1] + '-' + p[0] : iso; })(d.commenceDate)); if (d.standards) html += row('Standards', d.standards); html += section('2. Practice Information'); html += row('Practice Name', d.practiceName); html += row('ABN', d.practiceAbn); html += row('Phone', d.practicePhone); html += row('Fax', d.practiceFax); html += row('Email', d.practiceEmail); html += section('3. Practice Contacts'); html += row('Preferred Contact', [d.contactPrefix, d.contactFirst, d.contactLast].filter(Boolean).join(' ')); html += row('Position', d.contactPosition); html += row('Contact Phone', d.contactPhone); html += row('Contact Email', d.contactEmail); html += row('Preferred Method', d.contactMethod); html += row('Principal GP', [d.gpPrefix, d.gpFirst, d.gpLast].filter(Boolean).join(' ')); html += section('4. Practice Address'); html += row('Street Address', [d.addr1, d.addr2, d.city, d.state, d.postcode, 'Australia'].filter(Boolean).join(', ')); if (!d.postalSameAsStreet) { html += row('Postal Address', [d.postalAddr1, d.postalAddr2, d.postalCity, d.postalState, d.postalPostcode, 'Australia'].filter(Boolean).join(', ')); } else { html += row('Postal Address', 'Same as street address'); } html += section('5. Further Practice Details'); html += row('PHN', d.phn); html += row('PIP ID', d.pipId); var ptL = { gp: 'General Practice', ahs: 'After Hours Service', mds: 'Medical Deputising Service', aboriginal: 'Aboriginal Health Service' }; html += row('Practice Type', ptL[d.practiceType] || d.practiceType); var gaL = { 'yes-corporate': 'Yes \u2014 Corporate Entity', 'yes-group': 'Yes \u2014 Group', 'no': 'No' }; html += row('Group Practices Accredited', gaL[d.groupAccred] || d.groupAccred); if (d.corporateEntity) html += row('Corporate / Group Name', d.corporateEntity); html += section('6. Practitioners'); html += row('Count', d.practitionerCount === 'over-10' ? 'Over 10' : (d.practitionerCount || '\u2014')); if (d.practitioners && d.practitioners.length) { d.practitioners.forEach(function (p, i) { var displayName = [p.title, p.firstName, p.lastName].filter(Boolean).join(' ') || p.name || '\u2014'; html += ''; if (p.title) html += row('Title', p.title); html += row('First Name', p.firstName || '\u2014'); html += row('Last Name', p.lastName || '\u2014'); html += row('Hours / Week', p.hours || '\u2014'); var tL = { vocational: 'Vocational GP', registrar: 'Registrar', other: 'Other' }; html += row('Registrar/VR', tL[p.type] || p.type || '\u2014'); if (i < d.practitioners.length - 1) { html += ''; } }); } html += section('7. Self Assessment Fee'); html += row('Payment Mode', d.paymentMethod === 'cc' ? 'Credit Card' : d.paymentMethod === 'eft' ? 'Bank Transfer' : (d.paymentMethod || '\u2014')); html += '
AGPAL Registration — Accreditation Scope
Submitted: ' + now + '
' + '⚠ PENDING REGISTRATION — Practice not yet operational. DO NOT progress into Site Admin. ' + 'Notify adminbd@agpal.com.au (BCC marketingteam@agpal.com.au). Hold until practice opens.' + '
' + '👤 Practitioner ' + (i + 1) + (displayName !== '\u2014' ? ' \u2014 ' + displayName : '') + '
'; return html; } }; /* end App */ /* ================================================================ Append payment details to ongc_webresponse after Stripe return. ================================================================ */ App.appendPaymentToWebResponse = async function (stripeData, fees, paymentMethodLabel) { if (!App.state.leadId) return; stripeData = stripeData || {}; fees = fees || {}; paymentMethodLabel = paymentMethodLabel || 'Credit Card'; try { /* 1 — Fetch current ongc_webresponse */ var token = await App.waitForToken(); var fetchResp = await fetch( '/_api/leads(' + App.state.leadId + ')?$select=ongc_webresponse', { headers: { '__RequestVerificationToken': token, 'OData-MaxVersion': '4.0', 'OData-Version': '4.0', 'Accept': 'application/json' } } ); var currentHtml = ''; if (fetchResp.ok) { var ld = await fetchResp.json(); currentHtml = (ld && ld.ongc_webresponse) || ''; } /* 2 — Build payment section rows */ var now = new Date().toLocaleString('en-AU', { timeZone: 'Australia/Sydney', dateStyle: 'medium', timeStyle: 'short' }); function prow(label, value) { return '' + '' + label + '' + '' + (value || '\u2014') + '' + ''; } var sL = { succeeded: 'Succeeded', 'pending-cc': 'Pending (Credit Card)', 'pending-eft': 'Pending (EFT)' }; var paymentSection = '8. Payment Details' + prow('Payment Date', now) + prow('Payment Mode', paymentMethodLabel) + prow('Status', sL[stripeData.status] || stripeData.status || 'Succeeded') + prow('Base Amount', fees.base ? '$' + Number(fees.base).toFixed(2) : '\u2014') + prow('CC Surcharge', fees.surcharge ? '$' + Number(fees.surcharge).toFixed(2) : '\u2014') + prow('GST', fees.gst ? '$' + Number(fees.gst).toFixed(2) : '\u2014') + prow('Total Paid', fees.total ? '$' + Number(fees.total).toFixed(2) : '\u2014') + prow('Payment Reference', stripeData.paymentIntentId || stripeData.chargeId || stripeData.paymentId || '\u2014') + prow('Stripe Message', stripeData.message || '\u2014'); /* 3 — Inject before */ var updatedHtml = (currentHtml && currentHtml.indexOf('') !== -1) ? currentHtml.replace('', paymentSection + '') : (currentHtml || '') + '' + paymentSection + '
'; /* 4 — PATCH lead */ token = await App.waitForToken(); var pr = await fetch('/_api/leads(' + App.state.leadId + ')', { method: 'PATCH', headers: { 'Content-Type': 'application/json', '__RequestVerificationToken': token, 'OData-MaxVersion': '4.0', 'OData-Version': '4.0', 'If-Match': '*' }, body: JSON.stringify({ ongc_webresponse: updatedHtml }) }); if (pr.ok || pr.status === 204) { console.log('[AGPAL] Payment section appended to ongc_webresponse'); } else { console.warn('[AGPAL] appendPaymentToWebResponse PATCH failed HTTP', pr.status); } } catch (e) { console.warn('[AGPAL] appendPaymentToWebResponse error:', e.message); } }; /* ================================================================ Stripe return handler ================================================================ */ window.agpalHandleStripeReturn = async function (stripeData) { stripeData = stripeData || {}; App.state.leadId = stripeData.leadId || sessionStorage.getItem(App.state.leadIdKey) || App.state.leadId; App.state.leadRef = stripeData.leadRef || sessionStorage.getItem(App.state.leadRefKey) || App.state.leadRef; try { App.setBusy(true, 'Finalising payment record\u2026'); await App.createPaymentEvidence(stripeData); App.setBusy(true, 'Updating registration record\u2026'); await App.appendPaymentToWebResponse(stripeData, App.calcFees(true)); App.goToSuccess(false); App.showToast('Payment confirmed and registration completed.'); } catch (err) { console.error('[AGPAL] Stripe return failed', err); App.showToast(err.message || 'Unable to finalise payment evidence.', true); App.setBusy(false); App.updateSubmitEnabled(); } }; document.addEventListener('DOMContentLoaded', function () { App.init(); }); })();