For the complete documentation index, see llms.txt. This page is also available as Markdown.

Adding a Modules

PHPTRAVELS v10 — Module & Supplier Backend Developer Guide

A complete backend guide to how modules work in PHPTRAVELS v10 and how to add a new module or supplier to any service (stays, flights, tours, cars, ferries, esim, bus…). This document is backend-focused: routing, database, credentials, search adapters, pagination, markup, currency, booking, payment, invoicing, and admin wiring.


1. Two Integration Models — Local Inventory vs Live API

Every service on the platform is powered by one of two model types. The mechanics are almost identical; the only real difference is where the inventory comes from.

Local Inventory

Live API Supplier

Source of data

Your own MySQL tables

A third-party API

How it's fetched

Medoo ($db->get / select)

cURL request to the supplier

Credentials

none

required — stored on the modules row (c1c5) and sent with every cURL call

Example

bus, local stays/hotels, cars/cars

hotelbeds, duffel, cartrawler, airalo

modules row

one row per service (e.g. bus)

one row per supplier (e.g. hotelbeds)

Adapter folder

usually none (query tables inline)

modules/{type}/{supplier}/

Local example (query the DB directly):

Live-API example (read credentials from the module row, then cURL):

Both models ultimately return the same normalized result shape to the listing UI, so everything downstream (filters, booking, payment, invoice) is shared and identical.


2. Architecture Overview — Two PHP Apps

There are two separate front controllers, each with its own router:

  1. Main appindex.php (repo root). Handles all normal pages and APIs: home, listing, booking, invoice, admin, /api/.... Routes are registered from app/routes/_routes.php.

  2. Module sub-appmodules/index.php (+ modules/.htaccess). Handles everything under /modules/.... It has its own Router instance and includes each supplier's index.php to register supplier endpoints (search, issue, cancel, etc.). It exposes $db (Medoo) and $pdo ($db->pdo).

⚠️ Critical: a request to modules/{type}/{supplier}/search is served by modules/index.php, not the main app. If you register a supplier endpoint on the main app router it will never fire for a /modules/... URL — it must be included from modules/index.php.

Request lifecycle (booking): search → listing (aggregate suppliers / query DB) → cache draft (logs_bookings) → booking page → submit (create bookings row) → payment → callback (confirm + PNR) → invoice / PDF voucher.


3. Tech Stack, Conventions & Database Connection

  • PHP 8.2, Medoo ORM, Alpine.js + Tailwind on the front, MPDF for PDFs.

  • Medoo $db is created in config.php from .env:

.env keys: DB_TYPE, DB_HOST, DB_DATABASE, DB_USERNAME, DB_PASSWORD. Inside the module sub-app, $pdo = $db->pdo; gives the raw PDO handle.

  • Translations: every visible string uses T::key. The T class throws a fatal "Undefined constant" if the key does not exist — the ?? 'fallback' only covers keys that exist but are null. Always add new keys to app/lang/en.json (and keep the other language files in sync). See §20.

  • Admin lists are built with the crud() fluent builder (see §19).


4. The modules Table — Registering a Module

Every module/supplier is one row in modules. This row controls activation, credentials, markup, currency, and dev/live mode.

Full column reference (SHOW COLUMNS FROM modules):

Column
Type
Meaning

id

int

PK

name

varchar

Unique identifier / adapter folder name (bus, hotelbeds, cartrawler)

active

enum('1','0')

Shown on the site

type

enum

Service group: flights, stays, tours, cars, bus, rail, cruises, visa, insurance, umrah, hajj, events, esim, ferries

status

enum('1','0')

API/module enabled

c1c6

text

Generic credential slots (API key, secret, org id…)

dev_mode

enum('1','0')

Sandbox vs production

payment_mode

enum('1','0')

Supplier settle/payment mode flag

currency

varchar

Currency the supplier returns prices in

module_color

varchar

Tab/badge colour (hex)

order

int

Display order in tabs

prn_type

enum('1','0')

PNR handling flag

markup_type_b2c

enum('percentage','fixed')

Public markup type

markup_b2c

int

Public markup value

markup_type_b2b

enum('percentage','fixed')

Agent markup type

markup_b2b

varchar

Agent markup value

icon

varchar

Material Symbols icon name

check_balance

enum('1','0')

Wallet/balance check flag

content_import / import_database

enum / tinyint

Static-content import flags

logging_enabled

tinyint

Per-module request logging

host, database, username, password

varchar

For DB-import suppliers (direct DB feed)

tax, tax_type

int / enum('fixed','percentage')

Per-module tax

ancillaries_enabled, emd_enabled

tinyint

Flights extras (bags/seats, EMD)

Local module row (one per service):

Live supplier row (one per supplier; credentials filled later in the admin):

Active modules are cached: index.php loads $GLOBALS['modules'] via appCacheRemember('modules:active', 60, …) (60s APCu cache). New rows appear within a minute (or clear the cache).


4A. Feature Columns by Module Type — What to Enable for Which Module

The modules table is shared by every service, so it carries the union of all features. Any single module only uses a subset. The admin edit screen (app/views/admin/settings/modules/manage.php) shows every toggle, so when you add a module you must enable only the columns that make sense for its type (and for whether it's local or live). Enabling an irrelevant feature is usually harmless (the code that reads it never runs for that type) but confusing — enabling the wrong one (e.g. a supplier DB import for a live-only API) can cause errors.

Columns every module uses (all types, local + live)

Column
Purpose

name, type, icon, module_color, order

Identity + how it appears in tabs

active

Show/hide on the public site

status

Module/API enabled

currency

Currency the module's prices are in

markup_b2c / markup_type_b2c, markup_b2b / markup_type_b2b

Selling markup (public + agent)

tax / tax_type

Optional per-module tax

payment_mode

Enable/disable taking payment for this module

Columns only LIVE-API suppliers use (never for local inventory)

Column
Purpose
Notes

c1c6

API credentials (key, secret, org id…)

Labelled per supplier in modules-settings.php (§18). Empty for local.

dev_mode

Sandbox ↔ production

Meaningless for local.

logging_enabled

Log API request/response

Only meaningful when there is an API.

check_balance

Verify supplier wallet/deposit before booking

For GDS/wallet suppliers (e.g. flight consolidators).

Type-specific feature columns

Column
Belongs to TYPE
What it does

ancillaries_enabled

flights

Enable ancillary services (extra bags, seat selection). Read by isAncillariesEnabled() and the flights booking page.

emd_enabled

flights

Enable EMD (Electronic Miscellaneous Document) issuance. isEmdEnabled().

prn_type

flights (and ferries)

PNR handling mode for the supplier.

content_import

stays

Enable importing static hotel content (names, photos, geo, facilities) from the supplier — separate from live availability/pricing.

import_database

stays

Store that imported content in a separate database (recommended for large hotel catalogues so it doesn't bloat the main DB).

host, database, username, password

stays (only when import_database = 1)

Connection for that separate content database.

Airalo Packages tab / rules

esim

Per-country package availability + commission rules (esim-specific admin tab).

The admin edit tabs (which appear when)

The module edit screen is split into tabs, each mapping to files under app/views/admin/settings/modules/:

  • General (manage.php) — identity, active/status/dev_mode/payment_mode, plus the feature toggles above. All types.

  • Credentials (credentials.php) — the c1c5 inputs (labelled via modules-settings.php) and the Test button. Live suppliers only.

  • Database (database.php) — host/database/username/password for the separate content DB. Stays suppliers with content_import + import_database.

  • Markup (markup.php) — B2C/B2B markup + tax. All types.

  • Airalo Packages (airalo-packages.php) — country/package/commission rules. eSIM (Airalo) only.

Quick "what to enable" by type

  • flights (live GDS/NDC): credentials c1…, dev_mode, logging_enabled, and as the supplier supports — ancillaries_enabled, emd_enabled, prn_type, check_balance. No content/database import.

  • stays (live hotel API): credentials, dev_mode, and if you import static content, content_import (+ import_database and the DB connection). No ancillaries/EMD/PRN.

  • stays / cars / bus (local inventory): no credentials, dev_mode, content/database, ancillaries, EMD, PRN, or check-balance — only currency, markup_*, status, active, payment_mode.

  • cars / tours / ferries (live): credentials, dev_mode, logging_enabled. Ancillaries/ EMD/PRN/content-import do not apply.

  • esim (Airalo): credentials, dev_mode, plus the Packages tab for country/commission rules. No content-import/ancillaries/PRN.

Rule of thumb: start from the type. content_import/import_database/DB fields are a stays concept; ancillaries_enabled/emd_enabled/prn_type are flights; the Packages tab is esim. c1–c6, dev_mode, logging_enabled, check_balance are live-supplier concepts (leave them off for local inventory).


5. Folder & File Placement (Where Everything Lives)

Three locations matter:

Local modules typically skip modules/{type}/{supplier}/ entirely (they query tables in app/routes/api/{type}/listingRoutes.php). A local module still creates a modules/{type}/{name}/index.php if it needs admin booking actions (see §17).


6. URL Structure & Routing

Main app uses vendor/qaxim/php-router. Routes are registered in app/routes/_routes.php (order matters — the router returns on the first match).

Typical registration order for a module (note catch-all placement):

.htaccess rewrites any non-file/non-dir request to index.php?url=$1; the router derives the path from REQUEST_URI minus the base dir.

Listing SEO URL examples:

Module sub-app (/modules/...) has its own router and includes each supplier index.php. Supplier endpoints are registered like:

Then add one line to modules/index.php:


7. Search / Listing Backend & Supplier Aggregation

The listing results are produced by aggregating all suppliers of a type (live) and/or querying local tables (local), then merging into one normalized array.

Supplier aggregation pattern (from app/routes/cars/listingRoutes.php):

The frontend then POSTs to each supplier's search endpoint in parallel: POST /modules/{type}/{supplier}/search.

Supplier search.php adapter contract:

  • Reads $_POST search params (origin, destination, dates, pax, page, per_page).

  • Reads credentials from its modules row ($db->get('modules', '*', ['name' => '…'])).

  • Calls the supplier API via cURL.

  • Normalizes each result to the shared shape and applies markup (§9).

  • Returns JSON (array of results, or {results, total, page, total_pages}).

Local listing (from app/routes/api/bus/listing) queries tables directly and returns the same shape — no supplier loop:

Normalized result shape (what every adapter must return per item): service_name / operator, origin, destination, times, duration, price (marked-up), currency, image, amenities, rating, source, and an id to book.


8. Pagination (Progressive Loading)

Suppliers are paginated per supplier, loaded progressively ("load more").

Backend contract (supplier search.php):

  • Accepts $_POST['page'] (default 1) and $_POST['per_page'] (default 20–25).

  • Returns that page of results, plus either a JSON envelope {results, total, page, total_pages} or a plain array, and may set response headers such as X-Grand-Total, X-Current-Page, X-Per-Page.

Frontend loop (stays reference, app/views/modules/stays/listing/stays.php):

  • Keeps currentPage[supplier] and hasMorePages[supplier].

  • fetchSupplier(supplier, page)POST modules/stays/{supplier}/search with page + per_page.

  • loadMoreHotels() increments the page for every supplier that still hasMorePages and fetches the next page in parallel, appending to the list.

  • hasMore is derived from page < total_pages (envelope) or "got a full page of results" (array) — an empty array means no more pages.

For a local module, pagination is simple SQL LIMIT/OFFSET inside the listing route, or you can return everything at once if the volume is small.


9. Markup & Pricing — the MARKUP() Function

Selling price = supplier/base price + module markup. Use the shared helper:

Behaviour:

  • Reads markup_b2c / markup_type_b2c (public) or markup_b2b / markup_type_b2b (logged-in agents) from the modules row.

  • Supports percentage and fixed markup.

  • Honours agent custom markup (users.apply_markup = 'custom'markup_value / markup_type) which overrides the module markup.

  • Optionally converts currency in the same call ($fromCurrency$toCurrency).

$module may be the module name (it will look up the row) or the already-fetched module array. Related helpers: calculateCompletePrice() (base + tax + markup) and CURRENCY_CONVERT() (see §10).

Local modules apply the same markup. Example (bus listing):


10. Currency & Conversion

Currencies live in the currencies table:

Column
Meaning

name

3-letter code (USD)

rate

Exchange rate relative to the default currency

default

1 on exactly one row — the base/settlement currency

status

1/0 active

Three currencies to keep straight:

  1. Module/base currency — what the supplier returns (modules.currency, e.g. supplier returns EUR). Payments are settled in the site default currency.

  2. Default currency — the currencies row with default = 1.

  3. Display currency — the visitor's choice, in $_SESSION['app_currency'].

Conversion helper:

Formula: converted = ($price / fromRate) * toRate (both rates from currencies, status = 1). If from/to are omitted it converts base → session display currency. The invoice summary and booking summary convert display amounts on the fly while storing the base amount in bookings.price_markup / currency_markup.


11. Cache Booking (Draft) — logs_bookings

Before a real booking is created, the selection is cached so the booking page has a stable, shareable URL and the checkout survives refreshes.

Flow (all modules):

  1. POST /api/{type}/booking/save-draft → insert into logs_bookings:

  2. Redirect to /{type}/booking/{hash} — the booking page loads the draft from logs_bookings by hash.

  3. On confirm, the submit endpoint reads the draft, creates the real bookings row, and deletes the draft.

logs_bookings: hash, data (JSON draft), user_id, created_at.


12. Booking / Checkout Page & Shared Components

The checkout page (app/views/modules/{type}/booking/index.php) reuses shared includes — do not rewrite these:

  • views/includes/booking/booking-auth.phpGuest Details card (login / guest). Requires a $countries array defined by the page before the include, or it fatals and silently truncates the rest of the page:

  • views/includes/booking/payment-methods.php — payment gateway selector (binds formData.selected_payment).

  • views/includes/booking/loading.php — full-page loader (showBookingLoader).

  • Standard layout: left form column + sticky Booking Summary sidebar, a Booking Options card (special requests, terms), and the submit button at the page bottom.

Submit → POST /api/{type}/booking/submit.


13. Creating the Real Booking — the bookings Table

The submit endpoint validates the guest, computes pricing, and inserts one row into bookings. Key columns to set:

Column
Notes

invoice_id

8-char public id (strtoupper(substr(bin2hex(random_bytes(4)),0,8)))

module / module_type

The service (bus / bus, or hotelbeds / stays)

booking_status

pendingconfirmed (on payment)

payment_status

unpaidpaid / refunded

price_original

base (supplier) total — before markup

price_markup

customer total — after markup (settlement currency)

commission

price_markup − price_original → this is the admin Earning

currency_markup

Currency of the stored totals

first_name/last_name/email/phone/…

Lead guest

adults/childs/infants

Pax

booking_data

JSON snapshot of the draft (trip, dates, etc.)

travellers

JSON passenger list

pnr

Generated on confirmation (local) or returned by supplier

payment_gateway

Selected gateway id

Earnings depend on this: the admin bookings list shows commission. If you store the customer total in both price_original and price_markup, earning shows 0. Always set price_original = base, price_markup = customer total, commission = difference.


14. Payment Flow

  1. Invoice page Make PaymentprocessPayment() posts to /payment/process (invoice_id, gateway_id).

  2. /payment/process creates a payment token + transaction log and redirects to the secure /payment/{hash} page, which renders the gateway.

  3. The gateway redirects back to the module-aware callback URL. The prefix map lives in app/routes/globalRoutes.php — add your module:

  4. The module invoice route handles ?payment_status=success|cancel|failure&token=… by calling handle_payment_callback($token, $action, $extra) (with module_type/module), then sets a $_SESSION['payment_notice'].

  5. On success: mark paid + confirmed, record transaction_id, and generate the PNR (local inventory has no supplier PNR, so generate one). Decrement inventory here.


15. Invoice Page & Shared Summary

app/routes/{type}/invoiceRoutes.phpGET /invoice/{type}/{id} loads the booking (scoped by module_type) and renders app/views/modules/{type}/invoice/index.php.

The right column is the shared component — include it, don't rebuild it:

It renders status chips, totals (with currency conversion), the gateway selector, and the Make Payment / Download Invoice / Resend / Request Cancellation buttons. The view must also define the standard JS the summary calls: processPayment(), downloadInvoice(), requestCancellation() (copy from any module, swap the module name in the endpoints).


16. PDF Voucher — GENERATE_BOOKING_PDF()

GENERATE_BOOKING_PDF($invoiceId) (in app/lib/functions.php) selects a per-module HTML template and renders it with MPDF to uploads/invoices/invoice_{id}.pdf. Add your module to the template map:

Create app/views/notifications/emails/{type}/booking_pdf.php (copy the cars template; it's the simplest). The template receives $booking, $bookingData, $travellersData, $settings, $invoiceId.

Download route: GET /api/{type}/booking/download-invoice/{id}clear the output buffer before sending the PDF (while (ob_get_level()) ob_end_clean();) or a stray UTF-8 BOM corrupts the %PDF- header.


17. Module Actions — Issue / Cancel / Void / Refund

The shared admin booking editor (app/views/admin/bookings/edit.php) posts action buttons to /modules/{module_type}/{module}/{action} — handled by the module sub-app.

  • Live suppliers implement these by calling the supplier API (issue ticket, cancel PNR, request refund) — see modules/cars/cartrawler/cancel.php, modules/esim/airalo/{issue,cancel}.php.

  • Local modules implement them against the DB. Register the routes on the module sub-app router and add the include to modules/index.php:

Each handler: admin-guard → load booking (scoped by module_type) → mutate status → return JSON {status:true, success:true, message:…}. On cancel/void, restore inventory (seats, availability) if it was decremented. Mark Paid / Mark Confirmed are handled generically by POST /admin/bookings/action/{invoice_id} (module-agnostic, already works).


18. Admin — Settings → Modules (Status, Credentials, Currency, Markup)

Settings → Modules (/admin/settings/modules, app/routes/admin/modulesRoutes.php) is where an admin manages every module row without SQL:

  • Toggle active / status (enable the module / its API).

  • Toggle dev_mode (sandbox ↔ production).

  • Set currency, markup_b2c/b2b and their types.

  • Enter credentials (c1c5), test them, and save.

Naming the credential fields (c1–c5 rename) — for LIVE suppliers

The generic c1c5 columns are re-labelled per supplier in app/views/admin/settings/modules-settings.php via the $moduleCredentials map. This is where you tell the admin UI what each credential means for your API:

  • Each mapped cN renders a labelled input on the module edit screen; unmapped slots are hidden.

  • api_testing / env toggle the "Test credentials" button and sandbox/live switch.

  • The Test button posts to your modules/{type}/{supplier}/creds endpoint (creds.php), which validates the keys against the supplier (e.g. requests an OAuth token) and returns success/failure JSON.

  • Saved credentials land in modules.c1…c5; your search.php/booking.php read them via $db->get('modules', '*', ['name' => '{supplier}']).

Local modules simply set the credential map to false (no fields shown) and are configured only for markup/currency/status.


19. Admin — CRUD Lists, Sidebar, Bookings & Cancellations

  • Admin lists use the crud() builder: ->table(), ->col(), ->label(), ->row() (string templates or function($row){…} formatters), ->actions(), ->custom_button(), ->col_width(), and ->extra_fetch('col1,col2') to make extra columns available to row formatters. Row-format expressions like {{myHelper(id)}} are eval'd — the parser skips identifiers that are real PHP functions.

  • Sidebar: add the module menu, shown when the module is active.

  • Bookings list (app/views/admin/bookings/bookings.php) is module-agnostic. It shows status badges and a "Cancellation Requested" badge when cancellation_request = 1 and cancellation_status = 0.

  • Dashboard counts pending cancellations with cancellation_request = 1 AND cancellation_status = 0 AND booking_status != 'cancelled'.


20. Translations (i18n)

  • Use T::key everywhere. T::key ?? 'Fallback' only guards null values — an undefined key throws a fatal, blanking the page.

  • Add every new key to app/lang/en.json and mirror it into the other language files (ar, tr, ru, fr, ch, de, es, it, pl, bg, vn) so counts stay in sync.

  • Audit before shipping: grep each T:: in your files against en.json.


21. Notifications, Emails & Webhooks

  • NOTIFY::booking($module, $booking, $data) and NOTIFY::payment(...) send the confirmation/payment emails (templates under app/views/notifications/emails/{type}/).

  • triggerWebhook('{type}/{event}', '{type}.{event}.name', $payload) fires outbound webhooks (e.g. booking.draft_created, payment.completed).


22. Step-by-Step: Add a LIVE API Supplier

Example: add supplier acme to stays.

  1. Register the module row (or add it via Settings → Modules → Add):

  2. Create the adapter folder modules/stays/acme/ with:

    • index.phprequire the files below.

    • creds.php — register stays/acme/creds: validate c1/c2 against the API, return JSON.

    • search.php — register stays/acme/search: read $_POST params + page/per_page, read c1…c5 from the module row, cURL the supplier, normalize + MARKUP(), return paginated JSON.

    • booking.php / issue.php / cancel.php / refund.php as needed.

  3. Wire it into the sub-app: add include __DIR__ . '/stays/acme/index.php'; to modules/index.php.

  4. Label the credentials: add an acme entry under stays in app/views/admin/settings/modules-settings.php (c1 => ['label'=>'API Key', …]).

  5. Confirm the listing aggregates it: stays listing globs modules/stays/*/search.php, so the new supplier is picked up automatically; the frontend fetches modules/stays/acme/search per page.

  6. Actions: implement issue/cancel/refund so the admin editor buttons work.

  7. Test: enter credentials in Settings → Modules → Test, run a search, book, pay in sandbox, check the invoice/PDF, and try Cancel/Refund.

The rest is free — booking cache, checkout page, payment, invoice, PDF, currency, and markup are shared and already work once search.php returns the normalized shape.


23. Step-by-Step: Add a LOCAL Inventory Module

Example: a new local module {type} (the bus/local-cars pattern).

  1. Inventory tables — create your {type} tables (+ append to install/db.sql), e.g. a listing table, a variants/routes table, a per-date calendar, and a settings table.

  2. Module row — insert a single local row (name = {type}, type = {type}, currency, markup). No credentials.

  3. Routes (app/routes/{type}/…): homeRoutes, locationSuggestionRoutes (optional), listingRoutes (catch-all last), bookingRoutes (save-draft + /{type}/booking/{hash}

    • submit), invoiceRoutes; register them in _routes.php before the catch-all.

  4. Listing API (app/routes/api/{type}/listing): query your tables with Medoo, apply the module markup, return the normalized shape. Guard past/unscheduled dates.

  5. Views (app/views/modules/{type}/…): search widget, listing, booking (reuse shared includes + $countries), invoice (reuse includes/invoice/summary.php).

  6. Admincrud() lists for your inventory, sidebar entry, and manage the module's markup/currency via Settings → Modules.

  7. Actions — create modules/{type}/{type}/index.php + actions.php registering issue/cancel/void/refund on the sub-app router; add the include to modules/index.php.

  8. Payment/PNR/PDF — add your module to the callback prefix map (§14), generate a PNR on payment success, decrement inventory, and add the PDF template (§16).


24. Testing & Go-Live Checklist

  • php -l every touched file; validate app/lang/en.json.

  • All routes registered in _routes.php; catch-all is last.

  • Every T:: key exists in all language files.

  • Edge cases: past dates, unscheduled dates, sold-out, empty results, expired draft hash, missing guest fields.

  • Payment sandbox round-trip → paid + confirmed + transaction_id + PNR + inventory decrement.

  • Invoice loads; PDF downloads (valid %PDF- header); Make Payment / Resend / Cancel work.

  • Admin actions (Issue/Cancel/Void/Refund) return JSON and mutate correctly; cancellation request surfaces on list + dashboard.

  • Earnings (commission) correct on the bookings list.

  • install/db.sql contains the new tables + module row for a fresh install.


25. Appendix — Quick Reference

Key tables: modules (registration + c1–c5 + markup), bookings (final booking), logs_bookings (draft cache), currencies (rate/default), payment_gateways, booking_logs, plus your module's inventory tables.

Key files/paths:

  • config.php — Medoo $db.

  • app/routes/_routes.php — main-app route registration order.

  • app/routes/{type}/ — module routes.

  • app/views/modules/{type}/ — module views.

  • modules/index.php — module sub-app (add supplier includes here).

  • modules/{type}/{supplier}/ — live adapter (index/creds/search/booking/issue/cancel).

  • app/views/admin/settings/modules-settings.phpc1–c5 credential labels per supplier.

  • app/views/includes/booking/* and app/views/includes/invoice/summary.php — shared UI.

  • app/lib/functions.php — helpers below.

Helper signatures:

  • MARKUP($price, $module, $db, $fromCurrency = null, $toCurrency = null) — apply markup (B2C/B2B/agent) + optional conversion.

  • calculateCompletePrice($basePrice, $moduleName, $db, $from = null, $to = null) — base + tax + markup.

  • CURRENCY_CONVERT($price, $db, $fromCurrency = null, $toCurrency = null)['price','converted','conversion_rate'].

  • GENERATE_BOOKING_PDF($invoiceId) — render the per-module PDF voucher.

  • handle_payment_callback($token, $action, $extra) — process a gateway callback.

Normalized listing result (per item): source, route_id/id, service_name/ operator, origin, destination, departure_time, arrival_time, duration, price (marked-up), currency, img, amenities[], rating, plus any id needed to build the booking draft.


26. Technical Reference (Conventions, Constants, Boilerplate)

26.1 Naming conventions

  • Route files: {action}Routes.php in app/routes/{type}/homeRoutes.php, listingRoutes.php, detailRoutes.php, bookingRoutes.php, invoiceRoutes.php, locationSuggestionRoutes.php. API routes live in app/routes/api/{type}/.

  • View folders: app/views/modules/{type}/ with {type}-search.php, index.php, listing/, booking/, invoice/, details/.

  • Admin views: app/views/admin/{type}/ ({plural}.php for lists, manage.php / {singular}.php for forms). PDF templates: app/views/notifications/emails/{type}/booking_pdf.php.

  • Supplier adapters: modules/{type}/{supplier}/ with index.php, creds.php, search.php, booking.php, issue.php, cancel.php, refund.php, void.php.

  • Sub-app routes are named without a leading slash and without modules/: $router->post('{type}/{supplier}/search', …). The public URL is /modules/{type}/{supplier}/search.

  • Helper functions: shared cross-cutting helpers are UPPER_SNAKE_CASE (MARKUP, CURRENCY_CONVERT, GENERATE_BOOKING_PDF, ADMIN_AUTH, NOTIFY), module-local helpers are camelCase (busRoutesForBus, busModuleAction). Always guard definitions with if (!function_exists('name')) in files that may be included twice.

  • DB tables: module inventory is prefixed by type — bus, bus_routes, bus_routes_calendar, bus_settings; stays, stays_rooms, stays_rooms_calendar.

  • Translation keys: snake_case in app/lang/*.json, referenced as T::snake_case.

26.2 Global constants & route-file boilerplate

Defined in config.php:

Constant
Value
Use

root

site base URL (e.g. http://localhost/v10/)

build URLs / redirects

admin

'admin'

admin URL segment → root . admin . '/bookings'

views

__DIR__ . '/app/views/'

require_once views . 'includes/header.php'

uploads

__DIR__ . '/uploads/'

file paths / uploads in templates

Every route file follows this shape. $router, $db (Medoo) and $SECURE are already in scope (the file is require_once'd from _routes.php):

Admin routes must call ADMIN_AUTH() first (see 26.5).

26.3 Router inclusion order (app/routes/_routes.php)

Files are require_once'd top-to-bottom; the router returns on the first matching route, so specific paths must be included before catch-alls, and the generic module gateway is last:

Sub-app: register the supplier include in modules/index.php alongside the others:

26.4 Router error / JSON response contract

The main router has a global error handler; the module sub-app returns a structured JSON 404: {"error":true,"code":404,"message":"Endpoint not found","path":"…","method":"…"}. Adapter/action endpoints should always answer with:

edit.php's handleAction() treats status === true || success === true as success.

26.5 Security — CSRF & admin auth

  • ADMIN_AUTH() (in functions.php) redirects to login unless $_SESSION['user_id'] is set and $_SESSION['user_role'] === 'admin'. Call it at the top of every admin route.

  • For JSON admin endpoints in the sub-app (no redirect), guard inline:

  • CSRF (app/lib/Csrf.php): put <?= CSRF::tokenField() ?> in admin forms and verify with CSRF::validateToken($_POST['csrf_token']) (also CSRF::getToken(), CSRF::generateToken(), CSRF::verifyRequest()). Public booking/listing AJAX endpoints typically use their own hash/session validation rather than CSRF tokens.

26.6 Session keys & caching

  • $_SESSION['user_id'], $_SESSION['user_role'] (admin / agent / user), $_SESSION['admin_logged_in'] (true), $_SESSION['app_currency'] (display currency), $_SESSION['app_language'] (i18n), $_SESSION['payment_notice'] (invoice flash), $_SESSION['booking_data'] / $_SESSION['booking_hash'] (checkout).

  • Cache helper: appCacheRemember($key, $ttl, callable $resolver) (APCu, seconds). Active modules use key modules:active (60s). Clear/expire it after changing module rows.

26.7 crud() admin builder — method reference

echo crud()->table('…') … ->render(); Chainable methods:

Notes: displayed columns are validated against real table columns (invalid → exception); the status/featured toggles POST to crud's own toggle_status handler; row-format expressions are eval'd after substituting {{col}} values, and identifiers that are real PHP functions are skipped (so {{myHelper(id)}} calls your function).

26.8 Full table schemas (for fresh installs / reference)

bookings (the final booking record):

logs_bookings (draft cache): id, hash, data longtext, created_at, updated_at, user_id.

currencies: id, name varchar(3), country, default enum('1','0'), status enum('1','0'), rate.

payment_gateways: id, status, name, c1–c5 text, dev_mode, currency, order, active, note, type enum(credit_card,…,module_gateway), module, default enum('1','0'). (Gateways use the same c1–c5 credential pattern as modules; the module_gateway type is for supplier-hosted payment.)

booking_logs (supplier action/error trail): id, booking_id, action varchar(100), details longtext, created_at. Referenced by the admin editor's "Booking Issue Logs" panel.

26.9 Code skeletons

Supplier search.php (modules/{type}/{supplier}/search.php):

Admin action handler (modules/{type}/{module}/actions.php, local module):

Admin editor action map (app/views/admin/bookings/edit.php, for reference): buttons post Mark Paid/Mark Confirmed/admin/bookings/action/{invoice_id} (generic), and Issue/Void/Cancel/Refund/modules/{module_type}/{module}/{action} (sub-app).

Last updated