---
title: Conversion analytics modernization
description: Build out GA4 + conversion-event instrumentation across the B2B storefront, rotate analytics ownership off a departed consultant, close a HIPAA cookie-consent gap, and surface six storefront bugs discovered during the audit.
status: active
owner: Kyle
updated: 2026-05-11
last_verified: 2026-05-11
verification_type: eyeball
verification_ttl_days: 30
---

# Conversion analytics modernization

> **Owner:** Kyle Salata (kyle@advancedrx.co).
> **Status:** Active. Plan written 2026-05-11 after a full-stack audit of the Dec 2024 Brady/Measure Marketing Pro GA4 implementation.
> **Read together with:** [`refill-conversion/`](../refill-conversion/) (the upstream funnel audit that surfaced the underlying problem), [`magic-link-portal-login.md`](../magic-link-portal-login.md) (the future-state login fix), [`business-context/storefront-data-audit.md`](../../../business-context/storefront-data-audit.md) (the 8-layer storefront map), [`business-context/workflows/portal-refill-funnel.md`](../../../business-context/workflows/portal-refill-funnel.md) (the 12-stage funnel reference).
> **What this is:** the post-audit work plan for what to do about a half-finished analytics implementation that's been frozen for ~10 months under an external consultant's email-on-file ownership.

---

## §1 Executive summary

In December 2024 AdvancedRx signed a $2,550 / 17-hour engagement with **Measure Marketing Pro** (technical implementer: Brady, `analytics@measuremarketing.pro`) for a server-side GA4 implementation using Stape.io. The engagement was scoped and coordinated on the AdvancedRx side by Sarah Lee, Director of Tech. The accepted scope explicitly struck out e-commerce event tracking and lead-form event tracking. **Brady delivered competent infrastructure for what he was paid to build (pageview-only tracking through a HIPAA-compliant server-side container) and then walked away in February 2025.** No conversion events fire today. No revenue is attributable. The Google Sheets → Stape → Google Ads workaround Brady set up for offline conversion data has been **flowing to a Google Ads account that AdvancedRx doesn't control** (Brady's agency account; AdvancedRx has zero Google Ads spend).

The audit work in May 2026 inventoried every component of the tracking stack across three consoles (GTM web container `GTM-N8C8PCKJ`, GTM server container `GTM-5NWGFT4G`, Stape `mzldlvji`, GA4 property `306115267`) plus every storefront-facing LWC, the cart-to-order Apex conversion path, the login flow Apex + LWCs, and the SFMC reminder dispatch flow. The findings split into three coherent work tracks plus six storefront bugs surfaced as collateral damage.

**Headline numbers (as of 2026-05-10, last 7 days):**

- **12K** active GA4 users / **18K** events / **1.7K** new users — pageview-only traffic
- **0** Key Events configured with stream data (`purchase` is configured but never fires)
- **1.5** events per user — confirms pageview-only behavior
- **17** customer-facing storefront LWCs, **0** instrumented for analytics
- **3** Stape Connections (Google Sheets, Google Ads, Microsoft Advertising) all signed in as `analytics@measuremarketing.pro`
- **6** storefront bugs surfaced during the audit (one charge-without-order hazard, two pricing/attribution bugs, one login-lockout pattern, one consent-gating gap, one missing reminder→Order FK)
- **38%** combined drop-off in the login funnel (`/login` → `/passwordless-challenge` → `/my-home`)
- **62%** drop-off from `/my-home` → `/cart` (the refill wizard is invisible to GA4 today — it's modal-launched, no URL change)
- BAA with Stape: **signed** ✓ (verified 2026-05-11 with Kyle)

---

## §2 Audit summary — current state

**What's working:**

- Stape sGTM pipeline is live, healthy, and HIPAA-conscious. `/anonymize/g/collect` confirms the Anonymizer Power-Up is actively scrubbing PHI before forwarding to GA4. Four Power-Ups enabled (Custom Loader, Cookie Keeper, Anonymizer, Bot Detection). Server container `GTM-5NWGFT4G` is hosted on Stape at `stape.advancedrx.net`, US East (South Carolina), $20/month Pro tier.
- Web container `GTM-N8C8PCKJ` has the `GA4 - Config` tag routing through `{{Server URL}}` — `transport_url` and `first_party_collection` both set correctly. Architecture is textbook dual-container sGTM.
- GA4 property `Advanced Rx` (ID `306115267`) under account `222259042` receives pageview events from advancedrx.net (and `advancedrx.co`, which 301-redirects to `.net`). Measurement ID `G-FM7L49Y3DL`.
- Salesforce LWR site CSP whitelists `stape.advancedrx.net` across six directives — Brady did the Trusted Sites + CSP work properly.
- The `purchase` Key Event is configured in GA4 (per Brady) — waiting for `dataLayer.push({event: 'purchase', ...})` events that don't exist anywhere on the site.
- Two custom dimensions from the Bot Detection Power-Up (`Bot`, `Bot Score`) flow into GA4 — bot filtering is possible today.
- Five UTM-related custom dimensions on the `file_view` event (likely for PDF download attribution).

**What's not working:**

- **Zero conversion events fire from the storefront.** No `purchase`, no `add_to_cart`, no `begin_checkout`, no lead-form events. The container has 1 working pageview tag plus 1 paused "Contact Form Submission" tag (paused by Brady himself on 2026-02-20 when no dataLayer push existed to fire it).
- **The refill wizard is invisible in GA4.** It's modal-launched inside `/my-home` (no URL change), so all 5 wizard screens produce zero pageviews. The biggest source of cart entries cannot be measured today.
- **All three Stape Connections are authenticated as `analytics@measuremarketing.pro`.** Brady's email. He left in February 2025.
- **The Google Ads connection is silently flowing offline-conversion data to a Google Ads account AdvancedRx does not control.** AdvancedRx has zero Google Ads spend; Brady's agency does. This is data-exfiltration shaped, not just ownership-shaped.
- **The Microsoft Advertising Stape connection is deprecated** — needs migration to the new connector. AdvancedRx may not even use Microsoft Ads.
- **GA4 → Google Ads link: unconfigured** (verified 2026-05-11 via Kyle's screenshot showing "Google Ads 1 unlinked account") — confirms no Google Ads spend to attribute conversions to.
- **GA4 BigQuery export: unconfigured** (verified 2026-05-11 — "No links yet"). Raw event-level data is unavailable for advanced analysis.

**Ownership reality:**

- Web + server GTM containers: published by `analytics@measuremarketing.pro`. Kyle and Sarah have user-level access (verified 2026-05-11 when Kyle navigated into both containers).
- Stape `mzldlvji` container: Kyle has admin access; payment on Kyle's Visa \*\*\*\*4013 / $20 monthly / renews 2026-06-07.
- GA4 property `306115267`: Kyle has admin access.
- BAA with Stape.io: **signed** (verified by Kyle 2026-05-11).
- SFMC: Kyle currently locked out as of 2026-05-11; reminder email URL template unverified.

---

## §3 The funnel as currently visible in GA4

Using GA4 Pages and Screens (Engagement → Pages and screens, last 28 days):

| Stage                 | Page                      | Active users (28d) | Drop from previous | Cumulative drop |
| --------------------- | ------------------------- | -----------------: | -----------------: | --------------: |
| Top of funnel         | `/login`                  |             10,195 |                  — |               — |
| Email + DOB submitted | `/passwordless-challenge` |              7,933 |            **22%** |             22% |
| Authenticated         | `/my-home`                |              6,658 |                16% |         **38%** |
| Cart entered          | `/cart`                   |              2,535 |            **62%** |         **75%** |

**Two observations explained by code:**

1. **The 22% drop at `/login` → `/passwordless-challenge`** mixes (a) DOB-mismatch failures, (b) invalid-email errors, (c) "no unique user found" errors, (d) the "Not logged in yet" first-attempt-rejection-by-design pattern in [`B2BCustomLoginHandler.verifyUser`](../../../force-app/main/default/classes/B2BCustomLoginHandler.cls), and (e) users who simply abandoned the form. Today they're indistinguishable. Login funnel instrumentation (§7.4) splits them.

2. **The 16% drop at `/passwordless-challenge` → `/my-home`** is Hypothesis A from Kyle ("users get the code email and never input it"). Code-verification failures are _silent_ in GA4 today — [`b2bLoginCodeComponent.handleCodeSubmit`](../../../force-app/main/default/lwc/b2bLoginCodeComponent/b2bLoginCodeComponent.js) catches the error and sets `this.error = 'Invalid code, please try again'` with no redirect, no page change, no event. The 16% includes users who never tried, users who tried once and gave up, users who tried 5× and walked away, and users who clicked "Resend code." All collapsed into one number.

3. **The 62% drop at `/my-home` → `/cart`** is Hypothesis B ("logged in then disappear"). This is the biggest drop in the funnel. It's also the murkiest — `/my-home` traffic includes patients who came to check order history, manage their profile, or simply browse without intent. Crucially, the refill wizard _launches inside `/my-home` as a modal_ — patients who started the wizard but abandoned mid-flow show no `/cart` pageview, but they also produce no signal of having engaged with the wizard. Wizard-step instrumentation (§7.3) cracks this open.

**For sizing**: 1,275 users / 28 days lost at the code-entry step alone (Hypothesis A). 4,123 users / 28 days lost between `/my-home` and `/cart` (Hypothesis B, with caveats above).

---

## §4 Bugs surfaced during the audit

The instrumentation audit incidentally surfaced six bugs. Two are revenue-affecting, one is an active data leak, one is a HIPAA-adjacent gap, two are operational hazards. **Each gets its own ticket; this plan tracks the cleanup as Track C.**

| #     | Where                                                                                                                | What                                                                                                                                                                                                                                                                                                                                     | Severity                                                                                                                                                                                                                                   | Affected population                           |
| ----- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| **1** | [`B2BRefillFlowController.cls:27`](../../../force-app/main/default/classes/B2BRefillFlowController.cls)              | Wizard cart-add reads `UnitPrice` from `Standard Price Book` (4,198 entries), not the storefront's `Advanced RX Store Price Book` (949 entries). If the two pricebooks have different prices for the same product, customers added via the refill wizard are charged the wrong amount.                                                   | **Revenue-affecting if pricebooks diverge.** Latent today; needs prod verification                                                                                                                                                         | Wizard-path orders only                       |
| **2** | [`B2BRefillFlowController.toCartItem`](../../../force-app/main/default/classes/B2BRefillFlowController.cls)          | Wizard cart-add path does NOT set `Prescription_Link__c`, `RefillId__c`, or `Sig__c` on the CartItem (the parallel `addPrescriptionsToCart` invocable method DOES set them). Wizard-added items lose Rx attribution at the CartItem level.                                                                                               | **Operational + analytics impact** — downstream automation depending on these fields may silently fail; revenue attribution by Rx is unavailable                                                                                           | All wizard-path carts                         |
| **3** | [`B2BCustomLoginHandler.getUserDetails:139-149`](../../../force-app/main/default/classes/B2BCustomLoginHandler.cls)  | Hardcoded `Profile.Name = 'Advanced RX Customer Community Plus Login User'` in user-lookup query. Users on the other two community profiles (`B2B Reordering Portal Buyer Profile`, `Customer Community Plus Login User`) cannot log in — they get "No unique user found" on every attempt.                                              | **Silent customer lockout.** Permanent until profile is changed                                                                                                                                                                            | Any user not on the primary community profile |
| **4** | [`B2BPaymentMethodsController.cls:282-285`](../../../force-app/main/default/classes/B2BPaymentMethodsController.cls) | `convertCartToOrder` catch block throws but does not refund. If anything fails after `processPayments` succeeds (flow failure, OrderSummary query failure, OrderPaymentSummary insert failure, OrderPayment\_\_c insert failure), the customer's card has already been charged. No reversal logic.                                       | **Charged-but-no-order hazard.** Operational ticket — query `B2B_Custom_Exception__c WHERE Category__c='Payment'` for historical occurrences. Fault email goes to `helpdesk@advancedrx.co` (may have institutional knowledge of frequency) | Any payment-stage cart                        |
| **5** | [`b2bCookieComp.js`](../../../force-app/main/default/lwc/b2bCookieComp/b2bCookieComp.js)                             | Cookie consent banner just sets a `cookieConsent=accepted` cookie when the user clicks Accept. Does not gate GTM/Stape tag firing. No "Decline" option. No GA4 Consent Mode v2 integration. Analytics fire regardless of consent state. Stape's Anonymizer mitigates PHI exposure but does not satisfy consent-management best practice. | **HIPAA-adjacent compliance gap.** Counsel review recommended                                                                                                                                                                              | All portal sessions                           |
| **6** | (Schema gap, not a code bug)                                                                                         | `Order` has no FK to `Refill_Reminder__c`. Reminder→Order attribution today requires either UTM/click-trail joining or net-new instrumentation. Magic-link plan's `Magic_Link_Auth_Event__c` will solve this structurally when built                                                                                                     | Schema design choice                                                                                                                                                                                                                       | All reminder-driven orders                    |

**Note on Bug #4**: surface email goes to `helpdesk@advancedrx.co` per the `Send_Error_Email` action in [`B2B_Cart_To_Order.flow-meta.xml`](../../../force-app/main/default/flows/B2B_Cart_To_Order.flow-meta.xml). Helpdesk has likely been quietly reconciling these. Ask them how often.

---

## §5 Three-track plan

The work splits cleanly along three independent tracks. Different stakeholders, different urgencies, different cost profiles.

| Track                                 | Scope                                                                                                                                                                                                                                                                           | Urgency                                                                                                                                               | Owner candidate                                                                                                                           |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **A — Ownership & continuity**        | Rotate Stape Connections off `analytics@measuremarketing.pro`. Disconnect Google Ads (active data leak). Migrate deprecated Microsoft Ads connector (or disconnect if unused). Get Kristen as second GTM Admin. Restore SFMC access. Enable BigQuery export.                    | **High** — Google Ads connection is leaking conversion data to Brady's agency                                                                         | Kyle (no consultant needed)                                                                                                               |
| **B — Conversion analytics buildout** | dataLayer schema → ecommerce events → refill wizard events → 12 login funnel events → GA4 Key Events config → server-side `purchase` from Apex. Mostly single-line `dataLayer.push()` insertions in existing LWCs; one Apex callout class for the server-side `purchase` event. | **Medium-high** — revenue measurement enabler; multi-week calendar scope but light per-day effort                                                     | **Kyle + Claude.** No consultant. ~25-35 hours of Kyle's time across the calendar, mostly evening LWC edits with Claude pair-programming. |
| **C — Storefront bug cleanup**        | One ticket per bug from §4. Each independent.                                                                                                                                                                                                                                   | **Mixed** — Bug #3 (login lockout) and #4 (charged-but-no-order) are operational priorities; #5 (consent) needs counsel; #1, #2, #6 are roadmap items | Kyle + Claude (varies by bug)                                                                                                             |

The refill-conversion audit's Phase 2 (intervention design) becomes a _consumer_ of Track B (it can't design interventions without measurement) and structurally benefits from Bug #6 / magic-link completion.

---

## §6 Track A — Ownership & continuity

**Goal:** Eliminate the consultant-dependent ownership pattern across every component of the tracking stack. No item in this track requires external resources.

### §6.1 Disconnect the Google Ads Stape connection — **urgent**

The connection is signed in as `analytics@measuremarketing.pro` and Stape logs show `Google ADS Offline Conversion` events firing through it daily. AdvancedRx has zero Google Ads spend (verified 2026-05-11 — GA4 Advertising panel shows "Google Ads 1 unlinked account"). **Conversion data from your storefront is therefore flowing to Brady's agency Google Ads account, not yours.**

Steps:

1. Sign in to Stape `app.stape.io/container/mzldlvji/connections`.
2. Find the Google Ads connection (signed in as `analytics@measuremarketing.pro`).
3. Click "Sign out."
4. Do NOT re-connect under any AdvancedRx account — there's no AdvancedRx Google Ads property to connect to.
5. Verify Stape Logs (last 24h) no longer show "Google ADS Offline Co..." events.

**Effort**: 5 minutes. **Owner**: Kyle.

### §6.2 Disconnect or migrate the Microsoft Advertising Stape connection

The deprecated connector is signed in as `analytics@measuremarketing.pro`. The new connector (right next to it) is unsigned. Decision required: does AdvancedRx use Microsoft Ads?

- **If no** (likely): sign out of the deprecated connector, leave the new one unsigned. Symmetric with Google Ads.
- **If yes**: sign into the new connector under an AdvancedRx-owned Microsoft Ads admin account; sign out of the deprecated connector.

**Effort**: 5 minutes if disconnect; 15 minutes if migrate. **Owner**: Kyle.

### §6.3 Identify and migrate the Google Sheets Stape connection

The Stape Google Sheets connection is signed in as `analytics@measuremarketing.pro`. Brady set this up for offline conversion data export. Where the sheet lives (his Drive or AdvancedRx's) is unknown.

Steps:

1. Click into the Google Sheets connection in Stape → "View configuration" or equivalent → identify the sheet's URL.
2. Open the sheet. Check the owner: if `@measuremarketing.pro`, the data is in Brady's Drive (data exfiltration); if `@advancedrx.co`, it's yours.
3. If Brady's: download a copy, then disconnect.
4. If AdvancedRx's: sign in as an AdvancedRx Workspace account, disconnect Brady's auth.
5. **Once §7 ships `purchase` server-side, this entire pipeline can be retired.** Treat as temporary.

**Effort**: 15-30 minutes. **Owner**: Kyle.

### §6.4 Get a second AdvancedRx admin on the GTM containers

Today both containers (`GTM-N8C8PCKJ` web + `GTM-5NWGFT4G` server) show `analytics@measuremarketing.pro` as the publishing user for the most recent version. Kyle has user access. Kristen (`kristen@advancedrx.co`) and Sarah (tech director) should be added as Admin to both containers as redundancy.

Steps (Kyle, in Tag Manager):

1. Open `tagmanager.google.com` → AdvancedRx account → Admin → User Management.
2. Add `kristen@advancedrx.co` and Sarah's email with Admin (account-level) + Publish (both containers).
3. Verify both can sign in and edit.

**Effort**: 10 minutes. **Owner**: Kyle.

### §6.5 Restore SFMC access for Kyle

Kyle is currently locked out of SFMC (verified 2026-05-11). This blocks:

- Verification of the reminder email URL template (Phase 0 question — does it already carry attribution params?)
- Any future SFMC content edits needed for the magic-link plan
- Audit of SFMC user list for stale `analytics@measuremarketing.pro` access

Steps:

1. Coordinate with whoever administers the SFMC tenant — Marissa? Sarah?
2. Reset Kyle's SFMC password / re-add to the tenant.
3. Verify access by opening Content Builder → finding the refill reminder email template → "View HTML" → checking the portal link URL.

**Effort**: 30 minutes (admin time). **Blocked by**: SFMC admin availability. **Owner**: Kyle to escalate.

### §6.6 Enable GA4 BigQuery export

Free with GA4. Gives raw event-level data exported nightly. Unlocks advanced analysis (cohort analysis, sessions analysis, advanced attribution) that the GA4 UI samples or summarizes.

Steps:

1. Create a Google Cloud Project (free tier if none exists) — `advancedrx-analytics` or similar.
2. GA4 Admin → Property `Advanced Rx` → BigQuery Links → Link → select project.
3. Choose "Daily" export frequency, "Standard" data freshness.
4. First export lands ~24 hours later.

**Effort**: 30 minutes. **Owner**: Kyle. **No ongoing cost** if BigQuery storage stays under free-tier limits (typically true for AdvancedRx's traffic volume).

### §6.7 Verify Stape BAA scope

BAA is signed (confirmed 2026-05-11). Worth one more check: does it cover the actual data flow today? Specifically:

- Pageview metadata (URL, referrer, user-agent) flowing through `/g/collect` — covered
- Anonymizer pseudonymization — covered
- Cookie Keeper extending session cookies — covered
- The Google Sheets / Google Ads / Microsoft Ads connections — these write to _external_ destinations; the BAA may not extend to those downstream receivers. **Recommend reading the BAA's scope statement.**

**Effort**: 30 minutes to read + flag to counsel. **Owner**: Kyle + compliance counsel.

---

## §7 Track B — Conversion analytics buildout

**Goal:** ship the conversion-event instrumentation that Brady's accepted scope explicitly omitted. Six phases, each independently shippable.

**Recommended sequencing**: B1 → B2 in parallel with B3 + B4 → B5 → B6.

### §7.1 Phase B1 — dataLayer schema design

**Purpose:** define every event name, every parameter, every type _before_ writing any push calls. Once events start firing without a schema, inconsistencies are painful to fix.

Deliverable: a schema file (`docs/plans/conversion-analytics-modernization/dataLayer-schema.md` — to be authored as a sibling artifact) covering:

- **Standard ecommerce events** (per GA4 schema): `view_item`, `view_item_list`, `add_to_cart`, `remove_from_cart`, `begin_checkout`, `add_shipping_info`, `add_payment_info`, `purchase`, `refund` — full `items[]` shape per GA4 spec
- **Custom refill events**: `refill_wizard_start`, `refill_wizard_step_1_viewed`...`refill_wizard_step_5_viewed`, `refill_wizard_exit`, `refill_wizard_completed`
- **Custom login events** (12 — see §7.4): `login_view`, `login_dob_email_submitted`, `login_dob_mismatch`, `login_invalid_email`, `login_user_not_found`, `login_first_attempt_rejected`, `login_dob_passed`, `login_code_view`, `login_code_submitted`, `login_code_failed`, `login_code_resend_requested`, `login_success`
- **Custom support events**: `contact_support_form_submitted` (resurrects the paused GTM tag with no new GTM work)
- **Custom dimensions**: `refill_reminder_id`, `wizard_source`, `community_profile`, `attempt_number`, plus the 7 already in GA4

**Effort**: 2-3 hours of schema writing. Most events are GA4's standard ecommerce shape — copy from Google's documented recommended schema. **Owner**: Kyle + Claude.

### §7.2 Phase B2 — Core ecommerce events

| Event                    | LWC                                                                                                                             | Fire point                                                                                        | Source data                                                               |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `view_item`              | [`b2bMyMedicationsTable.js`](../../../force-app/main/default/lwc/b2bMyMedicationsTable/b2bMyMedicationsTable.js)                | Per-Rx click in `handleShowFillHistory`                                                           | `Rx.Id`, `Rx.Name`, price (from medication record)                        |
| `view_item_list`         | [`b2bMyMedicationsTable.js`](../../../force-app/main/default/lwc/b2bMyMedicationsTable/b2bMyMedicationsTable.js)                | After `@wire(GET_ACCOUNT_MEDS)` resolves, in `wiredPrescriptions.data` branch                     | `allPrescriptions[]` array                                                |
| `add_to_cart`            | [`customProductCarousel.js:162`](../../../force-app/main/default/lwc/customProductCarousel/customProductCarousel.js)            | Inside `addToCart()` after `ADD_PRODUCTS_TO_CART.then()`, BEFORE `window.location.href = "/cart"` | `_products[]` with id/name/price/quantity; product details already loaded |
| `begin_checkout`         | [`b2bCheckoutShippingAddress.js`](../../../force-app/main/default/lwc/b2bCheckoutShippingAddress/b2bCheckoutShippingAddress.js) | Inside `setCartSummary` after `data` populated, gated by `this.beginCheckoutFired` flag           | Cart total, items, postal code                                            |
| `add_shipping_info`      | [`b2bCheckoutShippingAddress.js`](../../../force-app/main/default/lwc/b2bCheckoutShippingAddress/b2bCheckoutShippingAddress.js) | In `stageAction(REPORT_VALIDITY_SAVE)` success                                                    | Selected address ID, postal code                                          |
| `add_payment_info`       | [`b2bCheckoutPayment.js`](../../../force-app/main/default/lwc/b2bCheckoutPayment/b2bCheckoutPayment.js)                         | In `handleSelectPaymentMethod`                                                                    | CardPaymentMethod ID, card type, is_saved_card                            |
| `purchase` (client-side) | [`b2bCustomOrderConfirmation.js`](../../../force-app/main/default/lwc/b2bCustomOrderConfirmation/b2bCustomOrderConfirmation.js) | In `@wire(CurrentPageReference)` after orderNumber arrives                                        | Just `orderNumber` and `email` — limited (see §7.6 for full payload)      |

**Effort**: 6-10 hours of LWC edits (7 events × ~1 hour each including DevTools verification). Each event is ~5 lines of `dataLayer.push()` at an existing handler. **Owner**: Kyle + Claude. **Required**: dataLayer schema from B1.

### §7.3 Phase B3 — Refill wizard custom events

Crucial because the wizard is invisible in GA4 today (modal-launched, no URL change). One event per screen + entry + exit.

| Event                                             | LWC                                                                                                                               | Fire point                                                      |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `refill_wizard_start`                             | [`b2bMyMedicationsTable.handleStartRefill`](../../../force-app/main/default/lwc/b2bMyMedicationsTable/b2bMyMedicationsTable.js)   | When the wizard modal opens; capture `contactId`                |
| `refill_wizard_step_1_viewed`                     | [`b2bRefillFlowScreen1.connectedCallback`](../../../force-app/main/default/lwc/b2bRefillFlowScreen1/b2bRefillFlowScreen1.js)      | Gated by `this.stepViewed` flag to dedupe back-button re-mounts |
| `refill_medications_selected`                     | [`b2bRefillFlowScreen1.handleContinueStageOne`](../../../force-app/main/default/lwc/b2bRefillFlowScreen1/b2bRefillFlowScreen1.js) | After `selectedRows` populated; capture selected Rx count + IDs |
| `refill_wizard_step_2_viewed` ... `step_5_viewed` | screens 2-5                                                                                                                       | Each `connectedCallback`                                        |
| `refill_wizard_exit`                              | [`b2bMyMedicationsTable.closeModalRefill`](../../../force-app/main/default/lwc/b2bMyMedicationsTable/b2bMyMedicationsTable.js)    | Capture `currentStageName` for which step they bailed on        |
| `refill_wizard_completed`                         | Implied by `add_to_cart` from `customProductCarousel.addToCart` success (Phase B2 covers this)                                    | —                                                               |

**Effort**: 3-5 hours. Each event is one `connectedCallback` push. **Owner**: Kyle + Claude. **Required**: dataLayer schema; works alongside B2.

### §7.4 Phase B4 — Login funnel events

The big diagnostic win. 12 events split across 2 LWCs. Today, your 38% combined drop-off across two login pages is a black box.

[`b2bCustomLoginComponent.js`](../../../force-app/main/default/lwc/b2bCustomLoginComponent/b2bCustomLoginComponent.js):

| Event                          | Fire point                                                                | Why                                                                      |
| ------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `login_view`                   | `connectedCallback`                                                       | Top of funnel                                                            |
| `login_dob_email_submitted`    | `handleEmailSubmit` before `getLogin()`                                   | Form attempted                                                           |
| `login_dob_mismatch`           | `catch` where `error.body.message == 'The date of birth does not match.'` | DOB wrong (the literal error string from Apex)                           |
| `login_invalid_email`          | `catch` where `error.body.message == 'Please enter a valid email.'`       | Email malformed                                                          |
| `login_user_not_found`         | `catch` where `error.body.message == 'No unique user found.'`             | Email not on the active community profile (also catches Bug #3 — see §8) |
| `login_first_attempt_rejected` | `catch` where `error.body.message == 'Not logged in yet.'`                | The by-design first-attempt rejection                                    |
| `login_dob_passed`             | `.then(result)`                                                           | DOB ok, code email sent, redirecting to `/passwordless-challenge`        |

[`b2bLoginCodeComponent.js`](../../../force-app/main/default/lwc/b2bLoginCodeComponent/b2bLoginCodeComponent.js):

| Event                         | Fire point                                  | Why                                      |
| ----------------------------- | ------------------------------------------- | ---------------------------------------- |
| `login_code_view`             | `connectedCallback`                         | Reached code-entry page                  |
| `login_code_submitted`        | `handleCodeSubmit` before `getCodeVerify()` | Code attempt                             |
| `login_code_failed`           | `catch` of `getCodeVerify()`                | Wrong/expired code                       |
| `login_code_resend_requested` | `sendNewCode()` invocation                  | User clicked Resend                      |
| `login_success`               | `.then(result)` of `getCodeVerify()`        | Authenticated, redirecting to `/my-home` |

**Effort**: 2-3 hours. Literally 12 single-line `dataLayer.push` insertions inside existing if-branches. **Owner**: Kyle + Claude. **Required**: dataLayer schema. **Highest analytical lift per hour invested** of any phase.

### §7.5 Phase B5 — GA4 Key Events + custom dimensions

In GA4 Admin → Property → Key Events:

- `purchase` is already configured (Brady).
- Mark `add_to_cart`, `begin_checkout`, `login_success`, `refill_wizard_completed`, `contact_support_form_submitted` as additional Key Events.

In GA4 Admin → Property → Custom Definitions → Custom Dimensions: register all custom event parameters that need to be slice-able (Refill_Reminder_Id, wizard_source, community_profile, attempt_number, error_reason).

In GA4 Admin → Property → Attribution settings: set the attribution model (data-driven if eligible, otherwise last-click) and the lookback window (7-day for SMS-driven, 30-day for email-driven — though for refill cadence both default to 7d).

**Effort**: 1-2 hours of console work. **Owner**: Kyle + Claude. **Required**: events from B2-B4 must be firing first.

### §7.6 Phase B6 — Server-side enrichment via Apex

The client-side `purchase` event (B2) has thin payload — only `orderNumber` and `email` are available from [`b2bCustomOrderConfirmation`](../../../force-app/main/default/lwc/b2bCustomOrderConfirmation/b2bCustomOrderConfirmation.js). For full revenue attribution, fire a redundant server-side `purchase` event from inside [`B2BPaymentMethodsController.convertCartToOrder`](../../../force-app/main/default/classes/B2BPaymentMethodsController.cls):285 (after the `OrderPayment__c` insert, before the return statement).

```apex
// New: after OrderPayment__c insert, before return summary.OrderNumber
List<OrderItem> items = [
  SELECT Product2Id, Product2.Name, Product2.Family, Quantity, UnitPrice, TotalLineAmount
  FROM OrderItem WHERE OrderId = :orderId
];

// New: HTTP POST to Stape /g/collect via Named Credential (e.g., callout:Stape_GA4_MP)
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Stape_GA4_MP');
req.setMethod('POST');
req.setBody(JSON.serialize(new Map<String, Object>{
  'client_id' => '<from session token>',
  'events' => new List<Map<String, Object>>{
    new Map<String, Object>{
      'name' => 'purchase',
      'params' => new Map<String, Object>{
        'transaction_id' => summary.OrderNumber,
        'value' => summary.TotalAmount,
        'currency' => 'USD',
        'items' => items.list of item maps,
        'account_id' => summary.AccountId,
        'payment_method' => paymentMethod.CardType
      }
    }
  }
}));
new Http().send(req);
```

GA4 dedupes by `transaction_id`, so the redundant client-side fire from B2 is fine. Server-side fire is unblockable by ad-blockers, fires only on actual database success, and includes full line items not available client-side.

**Effort**: 10-15 hours. The only substantial phase. Includes Named Credential setup for the GA4 Measurement Protocol endpoint, Apex callout class with sharing keyword + try/catch + `Error_Log__c` routing per CLAUDE.md §4, Apex test class, validate-only deploy. **Owner**: Kyle + Claude. **Required**: B5 (Key Events must be configured to receive the event), and a `client_id` source — server-side typically passes through GA4's session via a custom dimension or generates a deterministic ID per AccountId.

---

## §8 Track C — Storefront bug cleanup

Each bug from §4 gets its own GitHub Issue per the `.github/ISSUE_TEMPLATE/` conventions in CLAUDE.md. Tracked as separate work; this plan only links to them.

| Bug                                      | Recommended issue template                    | Owner candidate               |
| ---------------------------------------- | --------------------------------------------- | ----------------------------- |
| #1 Wizard pricebook                      | `04-bug.yml`                                  | Dev                           |
| #2 Wizard Rx attribution drop            | `04-bug.yml`                                  | Dev                           |
| #3 Profile-name-gated login              | `04-bug.yml` (high — silent customer lockout) | Dev                           |
| #4 Charged-but-no-order hazard           | `04-bug.yml` (high — operational)             | Dev + Ops (helpdesk has data) |
| #5 Cookie consent doesn't gate analytics | `05-security-concern.yml`                     | Counsel + Dev                 |
| #6 Order has no Refill_Reminder FK       | (folded into magic-link plan §7.2)            | —                             |

**Verification queries for prod sizing** (run these before writing the issues to size the bug impact):

```sql
-- Bug #1: Pricebook divergence
SELECT Product2Id,
       MAX(CASE WHEN Pricebook2.Name = 'Standard Price Book' THEN UnitPrice END) std_price,
       MAX(CASE WHEN Pricebook2.Name = 'Advanced RX Store Price Book' THEN UnitPrice END) store_price
FROM PricebookEntry
WHERE Pricebook2.Name IN ('Standard Price Book', 'Advanced RX Store Price Book')
GROUP BY Product2Id
HAVING std_price != store_price

-- Bug #3: Locked-out user count
SELECT Profile.Name, COUNT(Id) cnt
FROM User
WHERE IsActive = TRUE AND UserType = 'PowerCustomerSuccess'
GROUP BY Profile.Name

-- Bug #4: Charged-but-no-order historical occurrence
SELECT CreatedDate, Exception_Message__c, Related_To_Number__c
FROM B2B_Custom_Exception__c
WHERE Category__c = 'Payment' AND Subcategory__c = 'Payment Methods'
  AND CreatedDate = LAST_N_DAYS:90
ORDER BY CreatedDate DESC
```

---

## §9 Magic-link coordination

The [`magic-link-portal-login.md`](../magic-link-portal-login.md) plan is the structural fix for the login funnel. When built, it:

1. Eliminates `/login` and `/passwordless-challenge` from the refill funnel for reminder traffic (the 38% drop disappears for those users).
2. Creates `Magic_Link_Auth_Event__c` — a per-bridge auth audit log. Joinable to `Magic_Link_Short_Code__c` → `Refill_Reminder__c`. Bug #6 (missing FK) is solved structurally.
3. Provides reminder-level attribution as a SOQL JOIN, no GA4 dependency required.

**Implications for Track B prioritization:**

- **Login funnel events (Phase B4) are still worth shipping** — they apply to non-refill traffic (direct logins, manually-typed portal URL visitors) and they provide a _baseline_ against which to measure the magic-link lift after launch.
- **Server-side `purchase` (Phase B6) becomes more valuable post-magic-link** because the magic-link bridge can stamp the originating `Refill_Reminder__c.Id` onto the session as a cookie or session variable, which `convertCartToOrder` could then attach to the GA4 event for full reminder→purchase attribution without UTM dependence.
- **Magic-link prereqs from its §5:** SFMC credential rotation (in flight per MARISSA-HANDOFF.md), compliance counsel sign-off on no-Shield audit design (depending on Track A.7 BAA scope review), BAA confirmation for Experience Cloud (BAA with Stape is done; the Experience Cloud BAA is a separate question Kyle needs to verify with Salesforce).

**Sequencing recommendation**: Track A can ship immediately, parallel to magic-link prereq work. Track B Phase B4 (login events) should ship _before_ magic-link launches so post-launch lift is measurable. Phase B6 (server-side `purchase`) can ship after magic-link launches and pick up the `Refill_Reminder__c.Id` from the bridge.

---

## §10 Phase 0 — verifications still needed

These need to happen before or during early Track B execution. Most are 5-30 minutes of Kyle's or someone's time across various consoles.

| #   | Question                                                                                              | Where to check                                                                               | Blocked by                                           |
| --- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| 1   | Does the SFMC refill reminder email URL already include `?rrid=...&utm_source=...`?                   | SFMC Content Builder → refill reminder email template → View HTML                            | Kyle's SFMC access (currently locked out — see §6.5) |
| 2   | What is the `b2bCheckoutQuestions` `questionLanguage` design property value?                          | Experience Builder → page hosting `b2bCheckoutQuestions` LWC → component properties          | Kyle (10 min)                                        |
| 3   | Pricebook divergence query (Bug #1 sizing)                                                            | Prod SOQL — query in §8                                                                      | Kyle (5 min)                                         |
| 4   | Profile distribution query (Bug #3 sizing)                                                            | Prod SOQL — query in §8                                                                      | Kyle (5 min)                                         |
| 5   | `B2B_Custom_Exception__c` Payment errors query (Bug #4 sizing)                                        | Prod SOQL — query in §8                                                                      | Kyle (5 min)                                         |
| 6   | Does `B2BUtils.getWebStoreBaseURL()` return `https://www.advancedrx.net`?                             | Prod Anonymous Apex: `System.debug(B2BUtils.getWebStoreBaseURL());`                          | Kyle (2 min)                                         |
| 7   | What email address does the `B2B_Cart_To_Order` fault path actually deliver to?                       | Check helpdesk@advancedrx.co inbox for "Cart to Order Error for Cart" emails in last 90 days | Helpdesk team (10 min)                               |
| 8   | Does the BAA with Stape cover the downstream destinations (Google Sheets, Google Ads, Microsoft Ads)? | Read BAA scope statement                                                                     | Kyle + counsel                                       |
| 9   | Is Experience Cloud covered by AdvancedRx's Salesforce BAA?                                           | Verify with Salesforce account team                                                          | Kyle                                                 |

---

## §11 Sequencing & resource estimates

### Immediate (this week)

| Item                                        | Effort       | Owner |
| ------------------------------------------- | ------------ | ----- |
| §6.1 Disconnect Google Ads Stape connection | 5 min        | Kyle  |
| §6.2 Disconnect/migrate Microsoft Ads       | 5-15 min     | Kyle  |
| §6.3 Identify Google Sheets owner           | 15-30 min    | Kyle  |
| §6.4 Add Kristen + Sarah as GTM Admins      | 10 min       | Kyle  |
| §6.5 Escalate SFMC access restoration       | 30 min admin | Kyle  |
| §6.6 Enable GA4 BigQuery export             | 30 min       | Kyle  |
| Phase 0 prod SOQL queries (#3, #4, #5, #6)  | 20 min total | Kyle  |

**Total immediate Track A effort**: ~2-3 hours of Kyle's time.

### Sprint 1 (2 weeks calendar time)

| Item                              | Effort     | Owner         |
| --------------------------------- | ---------- | ------------- |
| Phase B1 — dataLayer schema       | 2-3 hours  | Kyle + Claude |
| Phase B2 — Core ecommerce events  | 6-10 hours | Kyle + Claude |
| GitHub Issues for Bugs #1, #2, #4 | 1 hour     | Kyle          |

### Sprint 2 (2 weeks calendar time)

| Item                             | Effort    | Owner         |
| -------------------------------- | --------- | ------------- |
| Phase B3 — Refill wizard events  | 3-5 hours | Kyle + Claude |
| Phase B4 — Login funnel events   | 2-3 hours | Kyle + Claude |
| Bug #3 fix (profile-name lookup) | 4-8 hours | Kyle + Claude |

### Sprint 3 (2 weeks calendar time)

| Item                                             | Effort                   | Owner          |
| ------------------------------------------------ | ------------------------ | -------------- |
| Phase B5 — GA4 Key Events config                 | 1-2 hours                | Kyle + Claude  |
| Phase B6 — Server-side `purchase`                | 10-15 hours              | Kyle + Claude  |
| Bug #4 fix (refund logic)                        | 8-16 hours               | Kyle + Claude  |
| Bug #5 (consent gating) — counsel review + scope | 2-3 hours + counsel time | Kyle + Counsel |

### Total estimate

- **Track A**: ~3 hours, Kyle only
- **Track B**: **~25-35 hours total**, Kyle + Claude pair-programming. Mostly single-line `dataLayer.push()` insertions in existing LWCs (Phases B1-B5 = ~14-23 hours combined). The only substantial Apex work is Phase B6 (server-side `purchase`) at 10-15 hours.
- **Track C**: ~15-30 hours, Kyle + Claude (varies by bug) + counsel time for #5

**Cost: $0 in cash.** No consultant. The only third-party cost is the existing $20/month Stape Pro subscription (already paid). Kyle's time + Claude's pair-programming covers the entire scope. **Calendar shape: roughly 3 sprints (6 weeks) of evening/spare-time effort, not a 50-hour heads-down engagement.**

**Why no consultant**: AdvancedRx's track record with consultants (Brady, Stack Intelligence) is poor — bugs left in production, documentation gaps, ownership lock-in. Track B is well-bounded LWC editing that Kyle can drive directly with Claude pair-programming, end up with clean documentation in this repo, and full ownership of every artifact.

---

## §12 References

### Internal — read these in order if joining mid-flight

1. [`CLAUDE.md`](../../../CLAUDE.md) — full org operating context.
2. [`business-context/storefront-data-audit.md`](../../../business-context/storefront-data-audit.md) — 8-layer storefront map.
3. [`business-context/workflows/portal-refill-funnel.md`](../../../business-context/workflows/portal-refill-funnel.md) — 12-stage funnel reference.
4. [`docs/plans/refill-conversion/`](../refill-conversion/) — Phase 1 audit that surfaced the underlying funnel problem.
5. [`docs/plans/magic-link-portal-login.md`](../magic-link-portal-login.md) — the structural login fix.

### Sources of fact for this plan (audit conversation, 2026-05-11)

The plan synthesizes a deep audit conversation that inventoried:

- GTM web container `GTM-N8C8PCKJ`: 2 tags, 1 trigger, 4 variables, 4 versions; last published 2026-02-26 by `analytics@measuremarketing.pro`
- GTM server container `GTM-5NWGFT4G`: 1 client (GA4), 1 trigger, 1 tag
- Stape container `mzldlvji`: Pro tier $20/mo, 4 Power-Ups (Custom Loader, Cookie Keeper, Anonymizer, Bot Detection), 3 Connections (Google Sheets, Google Ads, Microsoft Advertising — all under `analytics@measuremarketing.pro`)
- GA4 property `Advanced Rx` (306115267): G-FM7L49Y3DL, 12K active users / 18K events / 7d, 1 Key Event (`purchase`, no data), 7 custom dimensions
- 17 customer-facing storefront LWCs
- Apex conversion path: `b2bCheckoutPayment` → `B2BPaymentMethodsController.processPayments` (AuthorizeNet) → `B2BPaymentMethodsController.convertCartToOrder` → `B2B_Cart_To_Order` flow → `OrderSummary` + `OrderPaymentSummary` + `OrderPayment__c`
- Apex login path: `B2BCustomLoginHelper.getLogin` → `B2BCustomLoginHandler.verifyUser` → `verifyDOB` → `getCode` (sends email via `UserManagement.initPasswordlessLogin`) → `verifyCode`
- SFMC dispatch flow [`JBSystemFlow_Refill_Reminder_c`](../../../force-app/main/default/flows/JBSystemFlow_Refill_Reminder_c.flow-meta.xml) — EventDataConfig payload sends `Refill_Reminder__c.Id` to SFMC

### External (Google + Stape)

- GA4 Measurement Protocol — server-side event firing spec (used in Phase B6)
- GA4 Consent Mode v2 — required for Bug #5 fix
- Stape sGTM documentation — Power-Up configuration reference
- AuthorizeNet Connect API — for understanding the payment flow gotchas
