# HotelBeds Activities — Certification Checklist & Test Plan

This document mirrors HotelBeds' certification process for the Activities API and tracks our implementation status. Use it as the single source of truth when filling the certification form and when running the four mandatory test bookings.

Sandbox host: `https://api.test.hotelbeds.com`
Sandbox API key: `be5344ea68c58fae2b03720540dae3e8`
Storefront under test: `https://www.destinaholidays.com/search-activities`
Backend service: `https://www.destinaholidays.com/hotel-services/process.php`
Admin portal: `https://portal.flightomart.com/portal/activity-bookings.php`

The `Activities` PHP class is currently pinned to the sandbox on every host (production included) until certification is granted. After certification we revert `hotel-services/activities.php` to the host‑based switch so `www.destinaholidays.com` hits the LIVE host with the LIVE keys we receive.

---

## 1. "Before starting" form — answers to send HotelBeds

| Field | Answer |
| --- | --- |
| Integration expectative / business model | Direct B2C activities funnel embedded inside Destina Holidays (DH) — same site as our hotels and flights products. Sandbox certification first, LIVE after credentials are issued. |
| Direct integration vs IT developer / platform | **Direct integration**. We own the PHP backend (`hotel-services/process.php` + `activities.php`) and the Angular SSR frontend (`homepage-ssr-app`). |
| Operations implemented | Search, Detail (Full), Pickups, Booking Preconfirm, Booking Confirm, Booking Reconfirm (chained inside our own preconfirm + confirm flow), Booking Detail, Booking List, Booking Cancel, Voucher generation. |
| Development URL | `https://www.destinaholidays.com/search-activities` |
| User / Pass | Public site — no login required to reach the activities funnel. Admin portal credentials available on request. |
| Api-key | `be5344ea68c58fae2b03720540dae3e8` (sandbox) |
| Comments | Cache API not implemented — we read live from Booking API. Multi-day tours render correctly via `dateFrom` / `dateTo`. |

### Implemented operations matrix

| Operation | Implemented | Comments |
| --- | --- | --- |
| Search | YES | Filter types `destination`, `segment`, `text` and `hotel`. Full pagination + ordering. Currency converted server‑side from EUR to user-selected display currency. |
| Availability | YES | Same `/activities` call drives availability; date range, paxes and child ages all forwarded. |
| Details | YES — **Full** | We always request the Full detail (`/activities/details/full`) so that `featureGroups`, `routes`, `cancellationPolicies`, `comments`, `questions`, `sessions` and `languages` are available. |
| Booking Confirm | YES | We use **Preconfirm + Reconfirm** chained inside `Activities::bookInternal`. Preconfirm runs automatically on checkout entry; on Pay & Confirm we run preconfirm again with a fresh ratekey, then reconfirm. |
| BookingDetail | YES | `GET /bookings/{lang}/{ref}` — used by the confirmation page after page refresh. |
| Booking List | YES | `GET /bookings/{lang}` with `start`, `end`, `filterType`, `includedCancelled`. Used by the admin portal. |
| BookingCancel | YES | `DELETE /bookings/{lang}/{ref}?cancellationFlag=...`. Both `SIMULATION` and `CANCELLATION` exposed in admin portal. |
| Cache API | NO | Live calls only. |
| Multi-day tours | YES | `dateFrom`/`dateTo` shown on info, checkout, confirmation pages and voucher. |

---

## 2. Funnel evidence (URLs / screenshots to attach)

For each test below capture: search list, detail page, checkout page (with rate timer + cancellation policies), confirmation page, voucher (HTML or supplier PDF), email voucher, admin portal listing, simulate cancellation, and final cancellation.

| Step | URL pattern |
| --- | --- |
| Search results | `https://www.destinaholidays.com/search-activities?destination={dest}&from={from}&to={to}&adults={a}&children={c}&childAges={ages}` |
| Detail page | `https://www.destinaholidays.com/search-activities/activity-info/{activityCode}?from=...&to=...&adults=...&children=...&childAges=...` |
| Checkout page | `https://www.destinaholidays.com/search-activities/checkout` (in-memory selection state) |
| Confirmation page | `https://www.destinaholidays.com/search-activities/confirmation/{reference}` |
| Voucher (own) | `https://www.destinaholidays.com/hotel-services/process.php?activityVoucher=true&ref={reference}&format=pdf&lang=en` |
| Admin booking | `https://portal.flightomart.com/portal/editActivity.php?ref={reference}` |

---

## 3. Mandatory information — implementation map

### Search page (`activity-results`)

| Required attribute | Where it is shown |
| --- | --- |
| `/activities/name` | Card title in `activity-results.component.html`. |
| `/activities/content/description` (clipped + "More information" link) | Card body — HTML stripped via `shortDescription()`, then a "More information" link routes to `activity-info`. |
| `/activities/content/media/images[]` | Hero image inside each card (largest-available size resolved). |
| `/activities/amountsFrom[]` (Adult/Child) | Price block per card with `from` label, currency symbol, code as superscript, and "per adult". |

### Detail page (`activity-info`)

| Required attribute | Where it is shown |
| --- | --- |
| `/activities/name` | Page H1 + browser title. |
| `/activities/content/description` | Full HTML description, sanitized via `DomSanitizer`. |
| `/activities/content/media/images[]` | Image gallery. |
| `/activity/country/destinations/name` | Breadcrumb + meta description + checkout summary. |
| `/activity/content/featureGroups[]` | "Features" section. |
| `/activity/amountsFrom[]` (Adult/Child) | "From" price section. |
| `/activity/currencyName` | Displayed alongside currency code in price summary. |
| `/activity/operationDays` | "Operation days" chip list with weekday-name mapping for `MON`/`TUE`/... codes. |
| `/activity/modalities/name` | Modality selector. |
| `/activity/modalities/amountsFrom[]` | Modality price chip. |
| `/activity/modalities/comments[CONTRACT_REMARKS].text` | "Important info" block on detail and again on checkout/confirmation. |
| `/activity/modalities/rates/rateDetails/paxAmounts[]` | Per-pax price breakdown. |
| `/activity/modalities/questions/text` | Required answer fields rendered in checkout when present. |
| `/activity/modalities/rates/rateDetails/operationDates/cancellationPolicies` | "Cancellation policy" block on detail, checkout, confirmation, voucher and email. |
| `/activity/modalities/rates/shortDescription` | Shown when type = `GENERIC`. |
| `/activity/modalities/rates/rateDetails/sessions/name` | "Session" selector. |
| `/activity/modalities/rates/rateDetails/languages/description` | "Language" selector. |
| `/activity/content/routes[]` | "Itinerary / routes" block. |

### Confirmation page (`activity-confirmation`)

| Required attribute | Where it is shown |
| --- | --- |
| Activity name | Page header. |
| Date of service (`from`/`to`) | "When" block. |
| Modality / Rate / Session / Language | Summary block. |
| Price paid + currency | Totals block (with original currency note when display currency differs). |
| Pax distribution + child ages | "Travelers" table (age column included for children). |
| Question / Answer | "Activity requirements" block — only shown if questions were required. |
| Comments (CONTRACT_REMARKS) | "Important info" block. |
| Cancellation policies | "Cancellation policy" block. |

### Voucher (`buildVoucherHtml` + `emitPdf`) and confirmation email

The voucher includes every certification field:
- Reference (`booking.reference`)
- Activity name (`activity.content.name`)
- Creation date (`booking.creationDate`)
- Lead pax (`holder`)
- From / to (`activity.dateFrom`, `activity.dateTo`)
- Modality (`activity.modality.name`)
- Pax distribution + age (children show age)
- Destination (`activity.contactInfo.country.destinations.name`)
- Remarks (`activity.comments[CONTRACT_REMARKS].text`)
- Supplier name + VAT (`activity.supplier.name` / `vatNumber`)
- Provider info (`activity.providerInformation.name` + `bookingReference`)
- Language (`activity.modality.rates[0].rateDetails[0].languages[0].code`)
- Cancellation policies table
- Total + currency

### `vouchers[]` array handling (mandatory rule)

`hotel-services/process.php → sendActivityVoucherEmail()` and the admin portal `emailActivityVoucher()` now branch on `activity.vouchers[]`:

- `vouchers[]` is empty/missing → attach our own generated PDF (HTML fallback if `wkhtmltopdf` is missing).
- `vouchers[]` has entries → fetch the supplier PDF from the first URL (PDF preferred, otherwise first URL) and attach **only** the supplier file. We do **not** generate our own voucher in that case.

The activity-confirmation page already uses `hasSupplierVoucher()` / `supplierVoucherUrl()` so the customer always gets the supplier voucher when the API returns one (Test 3).

---

## 4. Certification test bookings — execution plan

Pax distribution for every test: **2 adults + 2 children (ages 1 and 17)** unless the test specifies otherwise. Use upcoming dates approximately 7–10 days out so HotelBeds returns availability.

### Test 1 — Standard funnel + cancellation (Barcelona)

1. Open `https://www.destinaholidays.com/search-activities` → set destination **Barcelona (BCN)**, From/To, 2 adults + 2 children (1 / 17), click **Search**. Capture the results page and confirm you see name + description + image + price-from on every card.
2. Click any card → land on the detail page. Capture the detail page; confirm: gallery, description, features, operation days, currency, modality picker, contract remarks, session/language pickers (if returned), cancellation policies, full pax-amount breakdown.
3. Click **Continue to checkout**. The page automatically runs **preconfirm** (rate timer starts at 20:00). Capture the checkout page showing: pax names form, lead-passenger form, billing/CC form, original-currency notice, cancellation policies. Fill all fields, including a sandbox CC.
4. Click **Pay & Confirm Booking** → backend chains **preconfirm + reconfirm**. Capture the confirmation page → URL contains the booking reference.
5. Voucher: open `?activityVoucher=true&ref={ref}&format=pdf&lang=en` → save PDF. Verify the email arrived in `confirmations@destinaholidays.com` and the lead pax email with the PDF attached.
6. Booking Detail: refresh the confirmation page → it calls `bookingDetail` and re-renders. Capture.
7. Cancellation: open the admin portal `editActivity.php?ref={ref}` → first **Simulate cancellation** (capture the JSON response showing simulated penalty), then **Cancel booking**. Capture the cancelled status badge in `activity-bookings.php`.

### Test 2 — Sessions + languages (Paris)

1. Search **Paris (PAR)** for the same dates. Find activity `E-E10-A1ALNO0185` (or any activity returning sessions + languages).
2. Open the detail page. Pick a specific `rateDetails` with a particular `session` + `language`. Confirm the rate-key in the network tab corresponds to the chosen session/language combo.
3. Continue to checkout → `preconfirm` runs with that ratekey. Confirm the chosen session and language are reflected on the checkout summary.
4. **Pay & Confirm** → confirmation page must show the session and language. Voucher email must include both. Capture all.
5. Cancel via admin portal → `CANCELLATION` flag.

### Test 3 — Direct-integration / supplier-voucher product (Madrid)

1. Search **Madrid (MAD)** and look for `E-E10-MADTEST` (or any activity for which `vouchers[]` is populated after confirm). If unavailable, contact HotelBeds.
2. Open the detail page (capture).
3. Continue to checkout → `preconfirm` → **Pay & Confirm**.
4. Confirmation page detects `activity.vouchers[]` and shows a "Download supplier voucher" button instead of the in-house voucher. The confirmation email attaches **only** the supplier PDF. No own-generated voucher is sent. Capture both.
5. Cancel via admin portal.

### Test 4 — Activity with required questions (Barcelona)

1. Search **Barcelona (BCN)**.
2. Open the detail of `E-E10-A1AANO0485` ("Hot Air Balloon Trip over Barcelona") — alternative `E-E10-A1AANO0484` has more questions.
3. Continue to checkout. The questions render as required fields (e.g. `HOTEL`, `PHONENUMBER`). Fill realistic answers (e.g. `Hotel TEST`, phone with country code).
4. **Pay & Confirm**. The booking payload sends the answers under `activity.answers[]` matching the `activity.questions[]` array. Capture the request payload (Network tab) and the confirmation page (which echoes the answers).
5. Cancel via admin portal.

---

## 5. Technical review checklist

- **GZIP compression**: All HotelBeds calls send `Accept-Encoding: gzip` and use `CURLOPT_ENCODING => 'gzip'`. Verified in `Activities::headers()` and `Activities::request()`.
- **One request per destination**: `search()` issues a single `POST /activities` per destination/page combination — no per-product fan-out at search time.
- **Detail loaded once per product**: `activity-info.component.ts` calls `detail()` exactly once on init.
- **Authentication**: `Api-key` and `X-Signature` (SHA256 of `apiKey + secret + epoch`) added per-request in `Activities::headers()`.
- **Currency**: All HotelBeds responses translated server-side via `$session->fetchCurrencyPrice()` from EUR to user-selected display currency. Original currency preserved in `originalCurrency` / `originalAmount` and surfaced on the checkout page so the customer sees the actual charge currency.
- **Persistence**: `apitude_activities_bookings` row written on every confirm with full request + response payloads; cancellations stamp `cancellation_date` / `cancellation_amount`.

---

## 6. Things to do **before** sending the certification email

- [ ] Run the four test bookings above end-to-end on `www.destinaholidays.com`. Save screenshots / PDFs into `/cert-evidence/test-1`, `/cert-evidence/test-2`, `/cert-evidence/test-3`, `/cert-evidence/test-4`.
- [ ] Save a copy of the email-voucher receipt (HTML + attached PDF) for each test.
- [ ] In the admin portal, after each test, capture the simulate-cancellation response and the final cancellation response.
- [ ] Reply to `integrations.btb@hotelbeds.com` with:
  - The "Before starting" + operations matrix from sections 1 of this doc.
  - The test URLs from section 2.
  - The four test references and the supporting evidence bundles.
  - Confirmation that we will swap the sandbox keys for LIVE keys in `Activities::__construct()` immediately after they are issued.

## 7. Post-certification go-live checklist

Cert feedback (`query 54696081`, Jorge Alfredo Perez, HB Client Technical Solutions):

1. ✅ `mandatoryApplyAmount` — when true, our selling price is coerced up to `boxOfficeAmount` if the API-returned `amount` is lower. Enforced in `Activities::applyAmountList()` and `Activities::translateActivity()` so it applies to every response body (search, detail, booking, bookingList).
2. ✅ Voucher must include destination — voucher template now reads `activity.contactInfo.city` with fallbacks to `contactInfo.country.destinations[0].name` and the meeting-point country/destination. See `buildVoucherHtml()`.
3. ⏳ One final LIVE booking (below).

### 7.1 Live credentials

Received in the HB portal on 2026-07-17.

- Alias: `SKYBOOKER.COM LTD (278543)`
- API-Key: `074a2f16d2df4a09698d15306ee4e8ab` (hardcoded fallback in `Activities::__construct()`)
- Shared secret: fetch from `https://developer.hotelbeds.com` under user `nishant_d` inside 14 days of issuance, then set on the production server:

  ```bash
  # /etc/systemd/system/php-fpm.service.d/override.conf   (or wherever env vars are set for php-fpm)
  Environment="HB_ACTIVITIES_SECRET_LIVE=<paste-secret-here>"
  Environment="HB_ACTIVITIES_API_KEY_LIVE=074a2f16d2df4a09698d15306ee4e8ab"
  ```

  Or drop into an `.env` picked up by our loader, or export it in the Apache/Nginx unit file. Do **not** commit the secret.

Host switching is host-based: `www.destinaholidays.com` / `destinaholidays.com` → `api.hotelbeds.com` with LIVE creds. Everything else (`stage.flightomart.com`, MAMP, etc.) stays on `api.test.hotelbeds.com` with the sandbox key.

### 7.2 Final LIVE booking (as requested by HB)

> "Please confirm a reservation in the live environment (dates six months in advance, 2 adults / 2 children) and send us both vouchers and logs. Use real names — avoid `test` / `trial` — and use a refundable rate. Any cancellation charges will be billed to your account. Wait for our confirmation before cancelling."

Concrete plan:

1. **Load the secret** on prod. Verify it's picked up:
   ```
   https://www.destinaholidays.com/hotel-services/process.php?activitiesStatus=true
   ```
   Response should show `"environment":"live","host":"https://api.hotelbeds.com","apiKey":"074a2f..."`. If `apiKey` prefix is `be5344`, the secret env isn't loaded and the code fell back to sandbox — fix env vars and restart PHP-FPM before continuing.

2. **Switch logging to `test-live`** so the LIVE booking is captured in its own file:
   ```
   https://www.destinaholidays.com/hotel-services/process.php?setActivityTest=test-live
   ```
   (Same logger works on prod — file lands at `hotel-services/include/activity-logs/test-live.log`.)

3. **Run the funnel on `https://www.destinaholidays.com/search-activities`**:
   - Destination: **Barcelona (BCN)** (matches what HB reviewed for cert).
   - Dates: **six months from today** (e.g. today + 180 days for both from and to; keep it a single-day activity).
   - Pax: **2 adults + 2 children (ages 1 and 17)**.
   - **Use real names.** Lead pax should be your legal name and a real email you monitor.
   - Pick a **refundable rate** — verify the detail-page cancellation-policy block shows a `dateFrom` in the future and a non-full penalty (otherwise cancellation charges will hit us). Confirm on the log side that `activities[0].modality.rates[0].freeCancellation === true` OR the cancellationPolicies penalty is less than the total.
   - Answer any required questions with real, sensible values.
   - Billing: real CC on the same card we use for live bookings.
   - **Pay & Confirm**.

4. **Capture** on prod:
   - Confirmation page screenshot (reference `102-XXXXXXXX`).
   - Voucher PDF: `https://www.destinaholidays.com/hotel-services/process.php?activityVoucher=true&ref=<reference>&format=pdf&lang=en`.
   - Confirmation email + attached voucher PDF.
   - Booking-detail refresh screenshot.
   - Downloaded log: `?getActivityTestLog=test-live&download=1`.

5. **Do NOT cancel yet.** Reply to query `54696081` with the reference, voucher, and log. Wait for HB to confirm they've reviewed it. Only after their green light, run **Simulate cancellation** first (to make sure the refundable window is still open with 0 or minimal penalty) and then **Cancel booking** via `https://portal.flightomart.com/portal/editActivity.php?ref=<reference>`. Send them the cancellation confirmation as the last artifact.

6. **Un-hide the Activities tab**: `homepage-ssr-app/src/app/home-page/home-page.component.ts` → set `displayActivities = true`, redeploy.

### 7.3 Sanity checks the reviewer will look for in `test-live.log`

- `Activities Search` request against `api.hotelbeds.com` (not `api.test.hotelbeds.com`).
- `activity.modalities.rates.rateDetails.totalAmount.mandatoryApplyAmount` — if `true`, the returned `amount` in our translated response is ≥ `boxOfficeAmount`.
- `Booking Preconfirm` + `Booking Reconfirm` succeed with `status: CONFIRMED`.
- Cancellation policies present in the reconfirm response and the booking-detail response.
- **Do not** send the cancel request in this test-live log until HB says so.
