# Cursor Prompt — Allow Plan-Type Change at BP Renewal / Replacement

**Goal:** Let users change a plan's BenefitPoint plan type when pushing a renewal or replacement (e.g. a Medical PPO renewing as a Medical HMO). BP's API allows this — Plansight's push code currently overrides the user's selection with the parent plan's type. Three changes: backend (allow user override), UI (show the plan-type picker on renewals/replacements, not just on new pushes), and a small confirmation when the type actually changes.

**Repo:** `Plansight APP` (Laravel) · package: `integrations/BenefitPoint/`

---

## Background (so reviewers and Cursor have full context)

Today, when a user pushes a renewal or replacement to BP, both the SOAP path (`Structs/Product.php`) and REST path (`Structs/ProductModel.php`) hardcode `productTypeID` to the parent plan's type:

```php
if (in_array($quote->originationReason, ['renewal', 'replacement'])) {
    // hardcodes parent's type
    $product->productTypeID = $parentProductTypeId;
}
if ($quote->originationReason == 'new') {
    // only for NEW: respects user's UI selection
    $product->productTypeID = $selectedProductTypeId;
}
```

This is a Plansight choice, not a BP requirement. The BP `Product` complex type (`common-v3.2.xsd` line 2830) defines `productTypeID`, `parentProductID`, and `policyOriginationReason` as three independent fields with no cross-validation. Confirmed with the live BP API — renewals with a different `productTypeID` than the parent are accepted.

---

## File 1 — `integrations/BenefitPoint/Structs/Product.php`

**Find this block (around lines 180–204):**

```php
		$copyParentBrokerOfRecord = false;

		if (in_array(data_get($quote, 'originationReason', ''), ['renewal', 'replacement']))
		{
			// If this is a renewal/replacement use the same parent product type ID for the plan/additional-product we are pushing.
			$parentProductTypeId = $quote->getBpData('parentProductTypeId');
			if (!empty($parentProductTypeId) && is_numeric($parentProductTypeId))
			{
				$product->productTypeID = $parentProductTypeId;
			}

			// overwrite the brokerOfRecordAsOf with the parent value if this was chosen
			// (this is the default for renewal/replacement)
			$copyParentBrokerOfRecord = true;
		}

		if (data_get($quote, 'originationReason', '') == 'new')
		{
			// For a new BP plan/additional-product push, try to get a user-selected product type ID.
			$selectedProductTypeId = $quote->getBpData('selectedProductTypeId');
			if (!empty($selectedProductTypeId) && is_numeric($selectedProductTypeId))
			{
				$product->productTypeID = $selectedProductTypeId;
			}
		}
```

**Replace with:**

```php
		$copyParentBrokerOfRecord = false;

		if (in_array(data_get($quote, 'originationReason', ''), ['renewal', 'replacement']))
		{
			// Default the productTypeID to the parent's type. If the user explicitly
			// picked a different type in the push UI (selectedProductTypeId), honor that —
			// BP allows renewals/replacements to change plan type (e.g. PPO renewing as HMO).
			$parentProductTypeId = $quote->getBpData('parentProductTypeId');
			$selectedProductTypeId = $quote->getBpData('selectedProductTypeId');

			if (!empty($selectedProductTypeId) && is_numeric($selectedProductTypeId)
				&& (empty($parentProductTypeId) || (int) $selectedProductTypeId !== (int) $parentProductTypeId))
			{
				$product->productTypeID = (int) $selectedProductTypeId;
			}
			elseif (!empty($parentProductTypeId) && is_numeric($parentProductTypeId))
			{
				$product->productTypeID = (int) $parentProductTypeId;
			}

			// overwrite the brokerOfRecordAsOf with the parent value if this was chosen
			// (this is the default for renewal/replacement)
			$copyParentBrokerOfRecord = true;
		}

		if (data_get($quote, 'originationReason', '') == 'new')
		{
			// For a new BP plan/additional-product push, try to get a user-selected product type ID.
			$selectedProductTypeId = $quote->getBpData('selectedProductTypeId');
			if (!empty($selectedProductTypeId) && is_numeric($selectedProductTypeId))
			{
				$product->productTypeID = $selectedProductTypeId;
			}
		}
```

---

## File 2 — `integrations/BenefitPoint/Structs/ProductModel.php`

**Find this block (around lines 221–245):**

```php
		$copyParentBrokerOfRecord = false;

		if (in_array(data_get($quote, 'originationReason', ''), ['renewal', 'replacement']))
		{
			// If this is a renewal/replacement use the same parent product type ID for the plan/additional-product we are pushing.
			$parentProductTypeId = $quote->getBpData('parentProductTypeId');
			if (!empty($parentProductTypeId) && is_numeric($parentProductTypeId))
			{
				$product->productTypeID = $parentProductTypeId;
			}

			// overwrite the brokerOfRecordAsOf with the parent value if this was chosen
			// (this is the default for renewal/replacement)
			$copyParentBrokerOfRecord = true;
		}

		if (data_get($quote, 'originationReason', '') == 'new')
		{
			// For a new BP plan/additional-product push, try to get a user-selected product type ID.
			$selectedProductTypeId = $quote->getBpData('selectedProductTypeId');
			if (!empty($selectedProductTypeId) && is_numeric($selectedProductTypeId))
			{
				$product->productTypeID = $selectedProductTypeId;
			}
		}
```

**Replace with the identical updated block from File 1** (same logic — both files implement the same product-construction flow, just one for SOAP and one for REST).

---

## File 3 — UI: `integrations/BenefitPoint/Scripts/BP.js`

**Goal:** When the user is pushing a renewal or replacement, show the plan-type picker (defaulting to the parent's plan type) alongside the existing parent-plan selector. When the user picks a type that differs from the parent, render a small inline notice: *"Plan type will change from {parent type} to {selected type} on push."*

**Where:** the BP push modal builds rows per quote. The plan-type picker is currently only rendered when `originationReason === 'new'` (look around lines 1478–1493 and the row-render near 1644–1700, and the `bpPlanAndProductTypes` consumers around 1504 / 1531). The picker UI element to reuse: the same `<select>` used for `originationReason === 'new'`, populated from `bpPlanAndProductTypes[insuranceType]`.

**Acceptance criteria:**

- For renewals/replacements, the plan-type `<select>` is rendered and pre-selected to the parent's `productTypeID`.
- If the user changes the selection, the change persists via the existing `POST /benefitPoint/group/{groupId}/products/push/saveSelectMenuValues` endpoint (key `selectedProductTypeId` on the quote's `bpData`). No new endpoint required.
- When the dropdown value differs from the parent's `productTypeID`, show inline copy: *"Plan type will change from {parent} to {selected} on push."* (Make the copy non-blocking — informational, not a confirmation modal.)
- For renewals/replacements, the picker should never be empty: it must default to the parent's type, and the "select a type…" empty-state should not appear.
- For "additional products" (`isAdditionalProduct`), keep the existing behavior — no plan-type swap (these are non-plan products like Telehealth, COBRA, etc.).

---

## File 4 — Backend safety: log when type changes

**In `integrations/BenefitPoint/Jobs/PushProduct.php`** — right after `ProductModel` (or `Product` for SOAP path) is constructed and before the API call, add a debug log when the constructed `productTypeID` differs from the parent's:

```php
if (in_array($quote->originationReason ?? '', ['renewal', 'replacement'])) {
    $parent = $quote->getBpData('parentProductTypeId');
    if (!empty($parent) && (int) $parent !== (int) $product->productTypeID) {
        Log::bpDebug(sprintf(
            'BP push: plan-type CHANGE on %s — quote=%s, parentType=%s, newType=%s',
            $quote->originationReason, $quote->id, $parent, $product->productTypeID
        ));
    }
}
```

This makes type-changes searchable in `bpDebug` logs for the next 6 weeks of incident-watching after rollout.

---

## Tests to add

1. **Unit:** `Product::fromQuote()` and `ProductModel::fromQuotes()` — given a quote with `originationReason='renewal'`, `parentProductTypeId=110`, `selectedProductTypeId=100`, assert `$product->productTypeID === 100`.
2. **Unit:** Same shape but `selectedProductTypeId` empty → assert it falls back to `parentProductTypeId=110`.
3. **Unit:** `selectedProductTypeId === parentProductTypeId` → still uses parent; `selectedProductTypeId` is treated as no-op (lets the condition stay simple).
4. **Integration (manual):** In a sandbox brokerage, push a Medical PPO renewal as a Medical HMO. Confirm BP creates the renewal with `productTypeID=100`, `parentProductID` pointing at the original PPO, and `policyOriginationReason='Renewal'`. Round-trip via `GET /rest/V4_4/products/{id}` to verify.
5. **Manual UX:** Push the same renewal twice — once keeping the type the same (no inline notice should appear), once changing it (notice appears, log line appears).

---

## Non-goals / out of scope

- Mapping benefit-summary attributes when the type changes. The new plan type's attribute structure (from `getBenefitSummaryStructure(planTypeID)`) is a different set than the parent's. Whatever attribute values existed on the parent that don't have a slot in the new type will silently be lost on push (BP's behavior). A future ticket can pre-warn the user about which fields they'll lose; for now we just allow the change.
- "Additional products" (`isAdditionalProduct`) — leave alone, no UI change for those.

---

## Cursor prompt — copy/paste this into Cursor on the Plansight APP repo

```
Implement the change described in Plansight Dev/Apps/integrations.plansight.com/cursor-prompts/bp-renewal-allow-plan-type-change.md.

Goal: allow users to change a BenefitPoint plan's productTypeID when pushing a renewal or replacement (currently we hardcode it to the parent's type, which is a Plansight restriction — BP itself allows the change).

Apply all four changes in the doc:
1. integrations/BenefitPoint/Structs/Product.php — replace the renewal/replacement branch around lines 180–204 with the version in the doc that prefers selectedProductTypeId over parentProductTypeId when they differ.
2. integrations/BenefitPoint/Structs/ProductModel.php — same change to the equivalent block around lines 221–245.
3. integrations/BenefitPoint/Scripts/BP.js — render the plan-type <select> for renewals/replacements (currently shown only for "new"), defaulted to the parent's productTypeID, persisting via the existing saveSelectMenuValues endpoint. When the selection differs from the parent's type, show inline copy: "Plan type will change from {parent} to {selected} on push." See acceptance criteria in the doc.
4. integrations/BenefitPoint/Jobs/PushProduct.php — add the bpDebug log line shown in the doc when productTypeID differs from parent on a renewal/replacement push.

Add the unit tests listed in the "Tests to add" section. After implementing, run `php artisan test --filter=BenefitPoint` (or the project's standard test entry point) and report any failures.

Do NOT change behavior for originationReason='new' or for isAdditionalProduct quotes — leave those code paths alone.

When done, summarize: which files changed, how many lines per file, and any test failures or surprises (e.g. if the JS picker logic needed bigger refactoring than expected).
```

---

## Rollback

Each change is isolated to the BP integration package — no migrations, no schema changes, no contract changes. Rollback = revert the commit. The only persisted state introduced is the additional `selectedProductTypeId` value (which already exists in `quote.bpData` for "new" plans), and the bpDebug log lines (no-op on revert).

---

## Update the integrations site after this ships

Once the change is live, update `integrations.plansight.com/benefitpoint-sync-map.html`:

- Add a new entry under `benefitType:"Plans & Products"` describing the renewal-with-type-change capability, OR
- Add a "Renewal flexibility" section to `benefitpoint.html` documenting that PPO→HMO and similar type changes are supported via the push UI.

Re-deploy via `vercel --prod` from `integrations.plansight.com/`.
