# konnektive-one

Single-bundle checkout engine for Konnektive / CheckoutChamp. jQuery + sweetalert2.
Everything ships in one file: `dist/engine.js`.

> - New funnel? Read **[docs/frontend-integration.md](docs/frontend-integration.md)** — the minimal-setup guide.
> - Moving an existing funnel off konnektive-engine/konnektive-core? Read
>   **[docs/migration-guide.md](docs/migration-guide.md)** — the step-by-step migration runbook.
> - How it's built + the non-obvious gotchas: **[docs/architecture-notes.md](docs/architecture-notes.md)**.
> - Version history: **[CHANGELOG.md](CHANGELOG.md)**.

---

## 1. Add the framework to a page

One tag. `data-config` points at the operation's config file, which the engine
loads and which calls `PaymentEngine.init({...})`:

```html
<!-- KONNEKTIVE FRAMEWORK -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/konnektive-one@1/dist/engine.js" data-config="assets/js/OPconfig.js"></script>
<!-- KONNEKTIVE FRAMEWORK -->
```

A real page adds the framework tag plus the operation's own scripts. Example:

```html
<!-- KONNEKTIVE FRAMEWORK -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/konnektive-one@1/dist/engine.js" data-config="assets/js/OPconfig.js"></script>
<!-- KONNEKTIVE FRAMEWORK -->

<!-- operation scripts -->
<script src="assets/js/app.js"></script>
<script src="assets/js/page.js"></script>
```

Requirements:
- **jQuery** must be loaded before `engine.js` (it is a webpack external, not bundled).
- The engine tag must be a plain `<script>` (no `async`/`defer`) so it runs before the scripts after it.

## 2. OPconfig.js (per-operation config)

Lives on the operation site. Called by the engine via `data-config`:

```js
PaymentEngine.init({
  provider: 'konnektive',
  couponMessage: 'This coupon is not valid for Subscribe & Save',
  servicePhone: '1-800-000-0000',

  // ZIP autofill + country filtering
  zipCityPlaceholder: 'City',
  zipFeedback: false,
  countries: 'US',            // 'all' | 'US' | 'US,CA'
  countriesExcept: '',

  products: [
    {
      pricePerUnit: 39.99, price: 239.94, retailPrice: 299.95, packageSize: 6,
      name: '6 Jars', termsName: '6 Jars', termsDays: 180,
      shippingID: 2,
      pid_konnektive: 2283, pid_konnektive_ss: 2289, campaignID_konnektive: 209,
    },
    // ...index 1, 2 — order matters: buttons set the hidden #cBillProductIndex to this index
  ],
  // upsells: [...], gifts: [...], shipProtection: [...]
});
```

Only the `*_konnektive` ids (`pid_konnektive`, `pid_konnektive_ss`, `campaignID_konnektive`,
`vid_konnektive`) are sent to the API. Display fields (`price`, `name`, `terms*`, …) are for
your page scripts. `shippingID` is the Konnektive shipping profile.

### Other init() keys worth knowing

| Key | Default | What it does |
|---|---|---|
| `urlPiiTransport` | `'session'` | How customer details cross the shipping → checkout hop. `'session'` parks them in `sessionStorage` and keeps them out of the URL. `'query'` restores the old native GET submit that put name/email/phone/address in the query string — a kill switch, not a feature. |
| `piiFields` | the `cShip*` / `cEmail` / `cPhone2` set | Fields withheld from the URL. A **denylist**: anything not listed passes through, so unknown affiliate/tracking params survive. Setting it **replaces** the default list — restate the defaults you still want. |
| `quickLeadOnBlur` | `false` | Two-page funnels only: keep a partial lead fresh on every shipping-field blur. One-page checkouts (`data-onePageCheckout="true"`) already do this. Opt-in because it issues a `/leads/import` per blur. |
| `pageExtension` | `'.html'` | Extension the engine puts on in-funnel links (`bonus2`, `thankyou`). **Set `''` on hosts that serve extensionless routes** (Cloudflare Workers Assets, Netlify, Vercel), or every navigation eats a redirect. |
| `passThruParams` | `clickid, msclkid, refd, trk` | Params forwarded past checkout to bonus/thankyou. Also **replaces** wholesale — restate the defaults. |

## 3. Config-dependent page scripts

Scripts that read `getConfig()` **at load time** must wait for the config. There
are two signals, and picking the wrong one fails silently:

| Your script… | Wait on |
|---|---|
| only reads `getConfig()` | `PaymentEngine.ready` |
| also reads a field `render()` builds — `#cBillProductIndex`, `#cShipCountry`, `#cShip*` (package/price summaries, `initTotalBox()`) | `PaymentEngine.rendered` |

`ready` resolves as soon as config loads, which is **before** `render()` has
built those hidden inputs. Using it for the second case gives you an empty form,
so the script silently falls back to defaults — the classic symptom is a
checkout summary rendering `$00.00`. When in doubt use `rendered`: it is strictly
later and settles on tracking pages too. Each has a matching DOM event
(`konnektive:ready`, `konnektive:rendered`).

Use `PaymentEngine.ready` (a Promise that resolves with the config) — keep the
dependency visible in the page body:

```html
<!-- runs only after PaymentEngine.ready (config available) -->
<script>
  PaymentEngine.ready.then(function () {
    ['assets/js/buynow.js'].forEach(function (src) {
      var s = document.createElement('script'); s.src = src; document.body.appendChild(s);
    });
  });
</script>
```

There is also a `konnektive:ready` DOM event (`event.detail` = config). Inline
handlers such as `onchange="combineExpDate()"` need no special wiring — every
framework function is a global once `engine.js` loads.

## 4. The checkout form

The engine drives a form by **id** and reads fields by their **id/name**. Build the
form with `id="payment_form"` (one-page) or `id="shipping_form"` + `id="payment_form"`
(two-step). System field ids/names (from a live `checkout.html`):

**Customer / shipping**

| id / name | purpose |
|---|---|
| `cEmail` | email (order confirmation) |
| `cShipFName`, `cShipLName` | first / last name |
| `cShipAddress1`, `cShipAddress2` | address (line 2 optional) |
| `cShipCity` | city |
| `cShipState` | state (`<select>`) |
| `cShipZip` | postal code (drives ZIP autofill) |
| `cShipCountry` | country |
| `cPhone2` | phone |

**Billing** (only when billing differs from shipping): same names with `cBill…`
(`cBillFName`, `cBillLName`, `cBillAddress1`, `cBillCity`, `cBillState`, `cBillZip`, `cBillCountry`).

**Payment**

| id / name | purpose |
|---|---|
| `ccNum` | card number |
| `ccMonth`, `ccYear` | expiry (call `combineExpDate()` on change) |
| `ccCode` | CVV |
| `ccCardType` | detected card type (set by the engine) |

**Order selection (hidden inputs)**

| id / name | purpose |
|---|---|
| `cBillProductIndex` | index into `config.products[]` — which package is selected |
| `cBillPurchaseType` | `onetime` or `subscribe` |
| `cBillPromoCode` | applied coupon code |
| `cShipCoupon` | coupon input field (see the coupon block) |

Each field also has an error slot, e.g. `#cEmail_errorSpot`, that validation writes into.

## 5. Form modes (data attributes on the form)

**One-page checkout** — `data-onePageCheckout="true"`

```html
<form id="payment_form" name="payment_form" method="get" data-onePageCheckout="true">
```

Shipping + billing + payment are all on **one** page. As the customer fills the
shipping fields, the engine emulates/updates the **lead** (partial order) in one
pass (on field blur) — there is **no** separate step. This is the opposite of the
two-step flow, where a shipping-info page is step 1 and a billing-info page is
step 2.

**Cart-abandon prefill** — `data-CA="true"`

```html
<form id="payment_form" name="payment_form" data-CA="true">
```

On load the engine reads the `chash` URL parameter and, if present, pulls the
customer's partial order (`konnektive_get_partial`) to **prefill** the shipping
and billing fields — so a returning cart-abandon visitor sees their info already
filled. (Both attributes can be combined on the same form.)

## 6. Tracking / thank-you pages

Fire a conversion pixel and skip the checkout flow:

```js
PaymentEngine.init({ track: 'sale' }); // 'conversion' | 'sale' | 'pv' | 'bpv' | 'spv'
```

## 7. Coupons

Coupon **validation** lives in the framework; **display** stays per-funnel.

`validateOfferCoupon()` is a global — wire it to your Apply button
(`onclick="validateOfferCoupon()"`). It reads `#cShipCoupon`, calls Konnektive
`/coupon/query` with the selected product's `campaignID_konnektive`, checks
validity (SUCCESS + the coupon is order-wide or restricted to the product's
`pid_konnektive`/`_ss`), and **rejects coupons on subscribe plans** (one-time
only — before hitting the API). It then dispatches a `konnektive:coupon-applied`
DOM event:

```js
// event.detail
{
  valid,           // boolean
  code,            // the coupon code
  discountType,    // 'PERCENT' | (absolute otherwise)
  discountPercent, // couponDiscountPerc
  discountPrice,   // couponDiscountPrice
  productIndex,
  reason,          // invalid: 'empty' | 'subscribe' | 'product' | 'error' | 'network'
  message,         // invalid: text
}
```

The funnel listens and does the display (voucher row + totals):

```js
document.addEventListener('konnektive:coupon-applied', function (e) {
  var d = e.detail;
  if (!d.valid) {
    if (d.reason === 'empty') { /* show "enter code" */ return; }
    throwError('error', getConfig().couponMessage || d.message);
    return;
  }
  // apply d.discountType / d.discountPercent / d.discountPrice to the voucher
  // row + total, set #cBillPromoCode, then recalc your (theme-specific) total box.
});
```

Required DOM hooks: `#cShipCoupon` (input), `.cpnBtn` (Apply button),
`#voucherInfo` / `#voucherInfoCode` / `#voucherInfoDiscount` (discount row),
`#cBillPromoCode` (hidden — carries the applied code into the order).

## Development

```bash
npm install
npm run build       # dev build -> dist/engine.js (with source map)
npm run build:prod  # minified production build
npm run serve       # static server on http://localhost:3000
npm run dev         # watch + serve
```

## Publishing

Update `version` in `package.json` (npm versions are permanent), then:

```bash
npm run release     # build:prod && npm publish
```
