# Loyalty Points (LP) Balance – Implementation & Flow

This document describes the full LP balance feature: storage in DB/session, header display, daily sync, refresh button, checkout realtime API, behaviour on API failure, and sync after redeem. API and mobile app continue to use realtime-only (no DB/session).

---

## 1. Overview

| Context | LP source | DB/session update | Notes |
|--------|-----------|-------------------|--------|
| **Web – normal page load** | Session (from `pt_accounts.loyalty_point_balance` or daily sync) | Yes when sync runs (cache miss or not today) | Once per day cache; else sync from API |
| **Web – checkout / LE checkout** | Realtime API via `getLpBalanceForCheckout()` | **Yes** only when API **succeeds**; **no** update when API fails | Header shows last known balance on failure |
| **Web – refresh button** | Realtime API via `refreshLpBalance()` | **Yes** only when API **succeeds**; **no** update when API fails | On failure: toaster with `lp_balance_fetch_error` |
| **Web – after LP redeem** | Thankyou page calls `syncLpBalanceAfterRedeem()` | Yes (sync from API, update DB + session) | Not during booking; only on thankyou load |
| **API / mobile app** | Realtime API only | No (no DB/session usage) | `isLpRealtimeRequest()` = true |

---

## 2. Backend Changes (Files & Roles)

### Core & helpers

| File | Role |
|------|------|
| **application/core/MY_Controller.php** | In `view()`: when method is **`checkout`** or **`leCheckout`** and user is not guest (`existing_instance_type != 4`), calls **`getLpBalanceForCheckout($this->userData->accounts_id)`**, sets **`$data['userLoyaltyPoints']`** and **`$data['lp_redeem_available']`**. Otherwise uses **`loyaltyPointsBalApiSessionData()`** and **`lp_redeem_available = true`**. |
| **application/helpers/instance_helper.php** | **LP helpers:** `isLpRealtimeRequest()`, `getLpBalanceFromDb()`, `syncLpBalanceFromApiToDbAndSession()`, `isLpSyncFresh()`, `getLastKnownLpBalance()`, `getLpBalanceSafeDefault()`, `getLpBalanceForCheckout()`, `syncLpBalanceAfterRedeem()`. **Modified** `getRemainingLoyaltyPoints()`: web = DB/session + daily sync; API/mobile = realtime only. |

### Account (refresh button)

| File | Role |
|------|------|
| **application/modules/account/controllers/User.php** | **`refreshLpBalance()`** (POST): calls **`syncLpBalanceFromApiToDbAndSession()`**. On **success**: updates session, returns JSON `{ success: true, remLP, lp_last_sync_timestamp }`. On **failure**: does **not** update DB or session; returns `{ success: false, message }` where **message** is from lang key **`lp_balance_fetch_error`**. **`getUserLoyaltyPoint()`** uses **`getRemainingLoyaltyPoints(..., true)`** (force realtime). |

### Database

- **Table:** `pt_accounts`
- **Column:** `loyalty_point_balance` (existing) – stores full LP snapshot as **JSON**.
- No new columns or migrations.

---

## 3. Checkout & LE Checkout (Realtime LP, Alert on Failure)

### When checkout/leCheckout runs

- **Controller:** `MY_Controller::view()` detects **`fetch_method() === 'checkout'`** or **`fetch_method() === 'leCheckout'`** and user not guest.
- **Action:** Calls **`getLpBalanceForCheckout($userId)`** which:
  1. Calls **`loyaltyPointsBalAPI()`** (realtime).
  2. **If API succeeds:** Calls **`syncLpBalanceFromApiToDbAndSession()`** (updates **DB** only; session for checkout is not updated here). Returns **`['available' => true, 'userLP' => $userLP]`**.
  3. **If API fails:** **Does not** update DB or session. Uses **`getLastKnownLpBalance($userId)`** (from DB or session) so the **header still shows the last updated balance**. Returns **`['available' => false, 'userLP' => $lastKnownOrSafeDefault]`**.

- **View data:** `$data['userLoyaltyPoints']` and **`$data['lp_redeem_available']`** are passed to the master and then to each checkout view.

### When to show alert vs LP redeem option

- **If `lp_redeem_available` is false and user is not secondary (`is_secondary == 0`):**  
  Show **only** the warning: **`<div class="alert alert-warning ml-3 mr-3">`** with **`lp_redeem_unavailable_message`**. Do **not** show the LP redeem control (checkbox or input + Apply).
- **If `lp_redeem_available` is true and user is not secondary:**  
  Show the normal LP redeem option (checkbox or LE input + Apply).
- **If user is secondary:**  
  Do not show the alert or the LP redeem option.

---

## 4. Modules & Checkout Views (LP Alert / Redeem)

All of the following views receive **`lp_redeem_available`** and **`userLoyaltyPoints`** from **`MY_Controller::view()`** (via master) and implement the same pattern: show alert when LP redeem is unavailable (API failed); otherwise show LP redeem when applicable.

| # | Module | Controller method | View file (path under `application/modules/<module>/views/`) |
|---|--------|-------------------|----------------------------------------------------------------|
| 1 | **Flight** | `checkout` | `flight_checkout.php` |
| 2 | **Hotel** | `checkout` | `hotel_checkout.php` |
| 3 | **Cruises** | `checkout` | `cruises_checkout.php` |
| 4 | **Resorts** | `checkout` | `checkout.php` |
| 5 | **Transfers** | `checkout` | `transfer_checkout.php` |
| 6 | **Transfers** | `checkout` | `talixo_checkout.php` |
| 7 | **Stays** | `checkout` | `vacationrental_checkout.php` |
| 8 | **Ground** | `checkout` | `checkout_ground.php` |
| 9 | **Activities** | `checkout` | `activities_checkout.php` |
| 10 | **Activities** | `checkout` | `activities_checkout_cp.php` |
| 11 | **Theclub** | `checkout` | `checkout.php` |
| 12 | **Theclub** | `checkout` | `hotel_checkout.php` |
| 13 | **Trains** | `checkout` | `train_checkout.php` |
| 14 | **Life Experience (LE)** | **`leCheckout`** | `le_checkout.php` |

- **Flight, Hotel, Cruises, Resorts, Transfers, Stays, Ground, Activities, Theclub, Trains:** LP redeem is a **checkbox** (`#apply_loyalty_points`). When unavailable, the alert is shown and the checkbox block is hidden.
- **Life Experience:** LP redeem is an **input + Apply** button (`#loyalty_field`, `#apply_discount_type`). When unavailable, the alert is shown and the whole **block_redeem_points** block is hidden.

---

## 5. Refresh Button (Header)

| File | Role |
|------|------|
| **application/modules/home/views/master.php** | LP block: **`#header_LP`**, **`#header_LP_refresh`**, **`#header_LP_value`**, **`#header_LP_loader`**. Data attributes: **`data-lp-referesh-url`** (POST URL), **`data-lp-referesh-last-sync-ts`**. |
| **assets/js/modules/masterscript.js** | On **`#header_LP_refresh`** click: POST to **`account/user/refreshLpBalance`**. On **success**: update **`#header_LP_value`** and **`data-lp-referesh-last-sync-ts`**. On **failure** (`data.success === false` and **`data.message`**): **`toastr.error(data.message)`** (e.g. "Error while fetching the balance."). |
| **assets/scss/style.scss** | Styles for **`#header_LP`**, **`.header-lp-inner`**, **`.header-lp-refresh-wrap`** (single-line layout). |

- **On success:** DB and session are updated; header value and last-sync attribute are updated in JS.
- **On failure:** DB and session are **not** updated; user sees toaster with message from **`lp_balance_fetch_error`** (translated).

---

## 6. Sync After LP Redeem (Thankyou – No Sync During Booking)

- **When:** User has redeemed LP and is redirected to the module’s **thankyou** page (or **thankyouFree** for Activities).
- **What:** **`syncLpBalanceAfterRedeem($this->userData->accounts_id)`** is called at the start of the thankyou method (only when **`existing_instance_type != 4`**, i.e. not guest). It calls the realtime API, updates **`pt_accounts.loyalty_point_balance`** and **session** `userdata->userLoyaltyPoints`.
- **Why:** So the header shows the updated balance on thankyou and on the next page load, **without** adding LP sync delay during checkout/booking.

**Modules and methods that call `syncLpBalanceAfterRedeem()`:**

| Module | Method(s) |
|--------|-----------|
| Lifeexperience | `thankyou()` |
| Flight | `thankyou()` |
| Hotel | `thankyou()` |
| Cruises | `thankyou()` |
| Cruises_cp | `thankyou()` |
| Theclub | `thankyou()` |
| Activities | `thankyou()`, `thankyouFree()` |
| Stays | `thankyou()` |
| Ground | `thankyou()` |
| Transfers | `thankyou()` |

---

## 7. Key Helpers (Quick Reference)

| Helper | Purpose |
|--------|--------|
| **`isLpRealtimeRequest()`** | True when `uri->segment(1)` is `api` or `mobileapp`. |
| **`getLpBalanceFromDb($userId, $downgradeDate)`** | Reads **`pt_accounts.loyalty_point_balance`** JSON, recalc **`remLP`** from current redeemed in **`tbl_loyalty_points_redeem`**, returns **`userLP`** array or **null**. |
| **`syncLpBalanceFromApiToDbAndSession($userId, $downgradeDate)`** | Calls **`loyaltyPointsBalAPI()`**, builds **`userLP`**, **updates** **`pt_accounts.loyalty_point_balance`** with JSON. Returns **`userLP`** or **null** on failure. Does **not** set session; caller updates session if needed. |
| **`isLpSyncFresh($lpLastSyncTimestamp)`** | True if timestamp is today (same calendar day). |
| **`getRemainingLoyaltyPoints($userId, $downgradeDate, $forceRealtime)`** | **Web:** use DB cache if fresh, else sync and return. **API/mobile or forceRealtime:** realtime API only, no DB. |
| **`getLastKnownLpBalance($userId, $downgradeDate)`** | Returns last known **`userLP`** from **DB** (via **`getLpBalanceFromDb`**) or from **session** `userdata->userLoyaltyPoints`. Used when API fails so header can show previous balance. Returns **null** if none. |
| **`getLpBalanceSafeDefault()`** | Returns a safe default **`userLP`** array (e.g. **remLP** 0) when no cached balance exists. |
| **`getLpBalanceForCheckout($userId, $downgradeDate)`** | Used only on **checkout** and **leCheckout**. Calls **`loyaltyPointsBalAPI()`**. **Success:** calls **`syncLpBalanceFromApiToDbAndSession()`**, returns **`['available' => true, 'userLP' => $userLP]`**. **Failure:** does **not** update DB/session; returns **`['available' => false, 'userLP' => getLastKnownLpBalance() ?? getLpBalanceSafeDefault()]`**. |
| **`syncLpBalanceAfterRedeem($userId)`** | Sync from API to DB and **update session** **`userdata->userLoyaltyPoints`**. Called in each module’s **thankyou** (and **thankyouFree** for Activities) when user is not guest. |

---

## 8. Language Keys

| Key | Purpose | Example (English) |
|-----|--------|--------------------|
| **`lp_redeem_unavailable_message`** | Message shown in checkout/LE checkout when LP API failed (alert warning). | "Currently, you cannot redeem Loyalty Points due to balance fetching issue. You can continue booking without redeeming LP." |
| **`lp_balance_fetch_error`** | Message shown in toaster when refresh button is clicked and LP API fails. | "Error while fetching the balance." |
| **`lp_refresh_balance`** | Label for refresh action (e.g. tooltip). | "Refresh balance" |

These keys exist in: **english**, **france**, **german**, **spanish**, **italian**, **hungarian**, **korean**, **portuguese**, **romanian**, **russian** (under **`application/language/<lang>/information_lang.php`**).

---

## 9. Data in `pt_accounts.loyalty_point_balance` (JSON)

Stored fields (example): **`balanceUnlockedLP`**, **`balanceLockedLP`**, **`accumulatedLP`**, **`redeemedLP`**, **`rebilingLP`**, **`statusLP`**, **`remLP`**, **`lp_last_sync_timestamp`** (e.g. `Y-m-d H:i:s`).

When **reading** from DB, **`remLP`** is recalculated using **current** redeemed from **`tbl_loyalty_points_redeem`** so the value stays correct after new redemptions.

---

## 10. End-to-End Flow (Summary)

### Web – normal page (not checkout)

1. **`MY_Controller::view()`** sets **`$data['userLoyaltyPoints'] = $this->loyaltyPointsBalApiSessionData()`**.
2. **`loyaltyPointsBalApiSessionData()`** uses **`getRemainingLoyaltyPoints($userId)`**: if cache in **`pt_accounts.loyalty_point_balance`** is **today**, use it; else call **`syncLpBalanceFromApiToDbAndSession()`** and update session.
3. Header shows **`$userLoyaltyPoints['remLP']`** from session.

### Web – checkout or leCheckout

1. **`view()`** calls **`getLpBalanceForCheckout($userId)`** (realtime API).
2. **Success:** DB updated (session not updated here); **`lp_redeem_available = true`**; view shows LP redeem option when applicable.
3. **Failure:** No DB/session update; **`userLP`** = last known balance (header correct); **`lp_redeem_available = false`**; view shows **alert** with **`lp_redeem_unavailable_message`** and hides LP redeem.

### Web – refresh button

1. POST **`account/user/refreshLpBalance`** → **`syncLpBalanceFromApiToDbAndSession()`**.
2. **Success:** DB + session updated; JSON with **remLP** and **lp_last_sync_timestamp**; JS updates **`#header_LP_value`** and last-sync attribute.
3. **Failure:** No DB/session update; JSON **`success: false`**, **message** from **`lp_balance_fetch_error`**; JS shows **`toastr.error(data.message)`**.

### Web – after LP redeem

1. User completes booking with LP redeem; redirect to **thankyou**.
2. Thankyou method calls **`syncLpBalanceAfterRedeem($userId)`** → sync from API, update DB + session.
3. Header shows updated balance on thankyou and on next load.

### API / mobile

- **`isLpRealtimeRequest()`** is true; **`getRemainingLoyaltyPoints()`** always uses **`loyaltyPointsBalAPI()`** only; no DB/session read or write for LP balance.

---

## 11. One-Line Summary

- **Web:** Header LP comes from **session** (filled from **pt_accounts.loyalty_point_balance** or sync). **Once per day** cache is used; otherwise sync from API and save to **loyalty_point_balance**. **Checkout/leCheckout** call realtime API; on success DB is updated and LP redeem is shown; on failure DB/session are **not** updated, last balance is shown in header, and only the **alert** is shown. **Refresh button:** on success update DB + session and header; on failure show **toaster** and keep last balance. **After redeem**, thankyou runs **syncLpBalanceAfterRedeem** to update DB + session.
- **API / mobile:** Always **realtime** API; no DB/session usage for LP balance.
