HOMESTAY ROOM BOOKING FOR WOOCOMMERCE (v4.6.0)
=================================================
Developed by Srijani

WHAT THIS PLUGIN DOES
----------------------
- Fully self-contained: creates and manages its own WooCommerce products,
  pricing, and availability. Does NOT require WooCommerce Bookings or any
  other booking/room plugin to work.
- Supports ANY NUMBER of homestays, each configured independently from
  WP Admin > Homestay Booking:
    - Its own name (becomes the WooCommerce product name/title)
    - Its own auto-created WooCommerce product
    - Its own shared room pool
    - Its own pricing rules (base price, extra adult fee, extra child fee)
    - Its own two child age bands: a FREE infant band (e.g. 0-5 years,
      no charge) and a PAID child band (e.g. 6-12 years, charged the
      extra child fee) -- both age cutoffs are configurable
    - Its own occupancy rules (max adults/room, max children/room, max
      infants/room, and a combined max total occupancy/room)
- Automatically books MULTIPLE rooms within a homestay when a group
  can't fit in one room.
- Payment is collected through WooCommerce's normal checkout.

CLOSING DATES -- FULL OR PARTIAL
------------------------------------
You can close a homestay's dates two ways:
  1. FULL CLOSE: block every room for a date range (e.g. the whole
     property is closed for a week of renovation).
  2. PARTIAL CLOSE: block only SOME rooms, leaving the rest bookable
     (e.g. close 1 of your 3 rooms for repair while the other 2 stay
     open for guests).
This is controlled by a "Rooms to close" number on the closing form --
set it to your total room count for a full close, or a smaller number
for a partial one. Closed rooms reduce the available pool for that date
on top of whatever is already booked; a new booking is only allowed if
enough UN-closed, UN-booked rooms remain. Both WP Admin > Homestay
Booking > Calendar and the Homestay Manager portal (see below) support
this, using the exact same underlying logic.

THE HOMESTAY MANAGER ROLE & PORTAL
--------------------------------------
This version adds a dedicated WordPress user role, "Homestay Manager",
and a completely separate, modern-styled frontend management panel for
that role -- so staff who only need to manage bookings, the calendar, and
orders don't need (or get) access to the full WP Admin dashboard.

SETTING IT UP:
  1. Go to WP Admin > Users > Add New (or edit an existing user).
  2. Set their Role to "Homestay Manager" (or "Homestay Admin" -- see
     below for the difference).
  3. Give them the portal URL shown at the top of WP Admin > Homestay
     Booking (looks like: https://yoursite.com/hb-dashboard/).
  4. They log in there directly with their normal WordPress username and
     password -- it's a real WordPress login (via wp_signon), not a
     separate account system.
  5. IMPORTANT: go to WP Admin > Homestay Booking > Edit (for each
     homestay) and use the "Assign Users" checklist near the bottom of
     the form to grant that user access to that specific homestay. A
     Manager/Admin with NO homestays assigned will see an empty portal
     -- this assignment step is required.

  FASTER ALTERNATIVE: if you set up one "Group Admin" account first
  (step 1-2 above, but choose the "Group Admin" role), that person can
  then create and assign ALL other Homestay Manager/Admin accounts
  themselves from the portal's "Team" section -- no further WP Admin
  visits needed for day-to-day team management.

MULTI-VENDOR ISOLATION (three roles):
  - HOMESTAY MANAGER: Dashboard, Bookings, Calendar, Check-in/Check-out,
    Orders -- for whichever homestay(s) are assigned to them only.
  - HOMESTAY ADMIN: everything a Manager can do, PLUS a "Homestay
    Settings" section to edit that homestay's own name, room count,
    pricing, and occupancy rules directly from the portal -- again, only
    for homestay(s) assigned to them.
  - GROUP ADMIN: sees and manages EVERY homestay (unscoped, not limited
    to assignments), PLUS a "Team" section to create new Homestay
    Manager/Homestay Admin accounts and control their homestay
    assignments -- runs the entire group's operations from the portal
    without ever needing WP Admin access. Use this role for whoever
    oversees the whole portfolio of homestays.
  A Homestay Manager/Admin assigned only to "Riverside Cottage" can
  NEVER see, book-manage, or edit "Hilltop Villa"'s data, even by
  guessing IDs in the URL -- every portal action re-checks the
  assignment server-side, not just menu visibility. This is what makes
  true multi-vendor management possible: each vendor/property manager
  only ever sees their own property. Administrators and Shop Managers
  (anyone with the standard WooCommerce "manage_woocommerce" capability)
  remain UNSCOPED and continue to see everything, exactly as before.

WHAT THE PORTAL INCLUDES:
  - Dashboard: today's check-ins/check-outs and occupancy at a glance,
    across the user's assigned homestay(s).
  - Bookings: the full booking list (scoped to assigned homestays),
    showing the guest's name alongside the Order ID, filterable, with
    an inline "Extend Stay" action per booking and a "+ New Booking"
    shortcut.
  - New Booking: create a booking directly for a walk-in or phone guest
    who isn't checking out through WooCommerce themselves. Enter the
    guest's name/email/phone, dates, and guest counts, then choose
    either the homestay's REGULAR calculated price or a CUSTOM total you
    enter yourself. Creates a real WooCommerce order, so it behaves
    identically to any other booking everywhere else in the plugin
    (calendar, availability, check-in/out, invoices).
  - Calendar: the same month-grid view as WP Admin, colour-coded by
    availability -- green (fully free), amber (partially booked/closed),
    red (fully booked or fully closed) -- with full/partial date closing
    and reopening. Past dates can no longer be selected.
  - Check-in / Check-out: three lists -- Arrivals (due to check in
    today or earlier, not yet checked in), Departures (checked in, due
    to check out today or earlier, not yet checked out), and Currently
    In-House -- each with one-click Check In / Check Out buttons, plus
    "Undo Check-in" / "Undo Check-out" actions and a "Recently Checked
    Out (today)" list, so a mistaken tap can always be reversed.
  - Extend Booking: lets a guest's stay be extended to a later check-out
    date. Validates that enough rooms are free for the ADDITIONAL nights
    only (not re-checking nights already booked), then creates a BRAND
    NEW order for the extra nights, billed to the SAME guest (their
    billing/customer details are copied over automatically). The
    original order/invoice is left exactly as it was; the extension
    gets its own order and its own invoice, and each references the
    other via an order note. It does NOT attempt to silently re-charge a
    payment method -- collect payment for the new order through your
    normal process.
  - Orders: WooCommerce orders that contain a homestay booking, with a
    safe, limited status-change control (on-hold / processing /
    completed / cancelled -- refunds are intentionally excluded and
    should go through WooCommerce/your payment gateway directly), plus
    a "Print Invoice" button per order that opens a standalone,
    print-optimized invoice (booking details, guest info, line items,
    totals) with a Print button.
  - Homestay Settings (Homestay Admin, Group Admin, + unscoped users):
    edit a homestay's name, room count, pricing, and occupancy rules
    right from the portal, without needing WP Admin access at all.
    Group Admin/unscoped users also see a "+ Add New Homestay" button
    here to create entirely new properties from the portal.
  - Team (Group Admin + unscoped users only): create new Homestay
    Manager/Homestay Admin accounts (username, email, password, role)
    and choose which homestay(s) each can access, or edit an existing
    team member's homestay assignments -- all from the portal.
  - License (Group Admin + unscoped users only): activate/deactivate the
    single license that covers every homestay on this site, entirely
    from the portal -- the same one otherwise managed via WP Admin >
    Homestay Booking > License. Plain Homestay Manager/Admin accounts
    don't see this page, since licensing is a whole-site concern now.
    Also has a "Per-Homestay Premium Override" table to manually force
    Premium on or off for a specific homestay (e.g. one that isn't
    renewing) -- see "PER-HOMESTAY PREMIUM OVERRIDE" below.
  - Its own login screen and logout, styled independently of your site's
    theme. On mobile, the sidebar becomes a slide-in menu opened via a
    hamburger icon in the mobile top bar, with Logout always reachable
    (either from the mobile top bar directly, or inside the menu).

FREE VS PREMIUM
------------------
This plugin ships as ONE codebase with a built-in Free/Premium split,
gated by a license key (WP Admin > Homestay Booking > License, OR from
the Manager Portal itself -- see below). The Manager Portal is
available on BOTH Free and Premium; Premium unlocks specific features
within it.

FREE VERSION INCLUDES:
  - 1 homestay
  - The full guest-facing booking form, pricing engine, and occupancy logic
  - The entire Manager Portal (/hb-dashboard/) -- Dashboard, Bookings,
    Calendar, Check-in/Check-out, Orders, Guest Requests, New Booking,
    Team management, Homestay Manager/Admin/Group Admin roles with
    full multi-vendor isolation
  - WP Admin > Homestay Booking: homestay settings, Bookings list, Calendar
  - Full-property date closing (the whole property closes, not individual rooms)
  - Extend Booking, printing/emailing/WhatsApp-sharing invoices, portal
    login branding, manager-created walk-in bookings with custom pricing

PREMIUM UNLOCKS (one license, covers every homestay on the site):
  - Unlimited homestays
  - Partial room closing (close 1 of 3 rooms, leave the rest bookable)
  - The customer "My Bookings" self-service tab's request workflow
    (date-change/extension/cancellation requests, and the cancellation
    policy engine that calculates their charge) -- viewing bookings and
    printing invoices stays available to guests either way

HOW LICENSING WORKS:
This plugin activates against the License Manager Server running at
sitss.in, over its REST API at /wp-json/license-manager/v1/. That
server issues license keys (e.g. LMS-6135-8770-4319-A055) two ways --
automatically when a customer completes a WooCommerce purchase on
sitss.in, or manually from that server's License Manager > Licenses >
"Issue License Manually" screen (useful for comps, support cases, or
selling outside the normal checkout). Any key works the same way
wherever it's entered.

This plugin's product slug is "homestay-booking-premium" -- it must
already be registered on the License Manager Server under this exact
slug (License Manager > Plugins) for activation to succeed.

WHAT HAPPENS ON ACTIVATION: the plugin POSTs license_key, domain (this
site's hostname), and product_slug to sitss.in's /activate endpoint.
The server checks the key exists for this product and this site hasn't
exceeded its activation limit, then returns a status:
  active         Normal, fully active license.
  grace          Past its term but still within the server's configured
                 grace period (typically 30 days) -- Premium still
                 works, but a renewal notice is shown.
  expired        Past both the term and the grace period -- Free mode.
  revoked        Manually revoked on the server -- Free mode.
  invalid        Key not found, or not valid for this product.
  limit_reached  This key is already activated on as many sites as its
                 activation limit allows.
Both "active" and "grace" count as Premium-active.

ONE LICENSE COVERS EVERY HOMESTAY: as of v4.3.2, there is a SINGLE
license per site (no separate per-homestay license keys -- an earlier
v4.2.0-4.3.1 per-homestay licensing feature was removed after further
consideration, in favor of this simpler model). Duration and grace
period are entirely set on the sitss.in server for the key itself
(e.g. a 365-day term with a 30-day grace period baked into the key) --
the plugin doesn't configure or track duration locally at all, it just
asks the server "is this still good?" once a day and trusts the answer.
If the license isn't renewed, once its term AND grace period both
lapse on the server, every homestay on the site automatically reverts
to Free -- no manual action needed for that to happen.

Group Admin (and unscoped WP Admin users) can activate/deactivate this
license from either WP Admin > Homestay Booking > License, or from the
Manager Portal > License page -- both manage the exact same license,
so status is always consistent regardless of where it's changed from.
Plain Homestay Manager/Homestay Admin accounts do not see the License
page in the portal at all (it's Group Admin-only), since licensing is
now a whole-site concern, not a per-property one.

PER-HOMESTAY PREMIUM OVERRIDE: as of v4.4.0, the portal's License page
(Group Admin only) also has a "Per-Homestay Premium Override" table --
one row per homestay, with three buttons: Force On, Force Off, and
Reset to Default. This is a manual admin switch, NOT a license -- no
key, no server call, takes effect the instant it's clicked. It sits on
top of the single site-wide license above:
    Default (inherit)   Follows the site-wide license exactly, same as
                         if the override didn't exist. This is what
                         every homestay starts as.
    Forced ON            This homestay gets Premium features even if
                          the site-wide license is inactive/expired.
    Forced OFF            This homestay is locked to Free features even
                           if the site-wide license is active and every
                           other homestay has Premium.
The main use case is cutting off ONE non-renewing property without
touching the license that covers everyone else on the site (Forced
OFF) -- the reverse (Forced ON, e.g. a comped property) is also
supported. Most homestays are expected to just stay on "Default"
indefinitely; this is meant as an occasional exception, not something
every homestay needs configured. It affects the same two things the
site-wide license already gates -- partial room closing and the
customer self-service request workflow -- but now evaluated per
homestay again (site-wide license status combined with that specific
homestay's override).

NOTE FOR DEVELOPERS: the hb_homestays database table still has unused
license_key/license_status/license_expires_at/etc. columns left over
from the removed per-homestay feature. They're harmless and intentionally
left in place rather than risking a destructive column-drop migration --
nothing in the plugin reads or writes them anymore.

BACKGROUND CHECKS: once the license is activated, a daily WP-Cron job
re-validates it automatically (no admin action needed), keeping cached
status fresh so an expired or revoked license eventually stops working
on its own. If a single day's check fails to reach sitss.in (e.g. a
brief outage), a previously-good status keeps working for a 5-day local
buffer before falling back to Free -- this protects against short blips
without needing to contact the server. A confirmed answer from the
server (even a bad one, like "expired") always takes priority over that
buffer -- the buffer only ever covers "we couldn't reach the server to
check," never "the server told us it's not valid."

DEACTIVATING: frees up this site's activation slot on sitss.in (so the
same key can be activated on a different site later) and immediately
reverts every homestay on this site to Free mode.

CONFIGURE BEFORE SELLING:
  1. Confirm "Homestay Room Booking Premium" is registered on sitss.in
     (License Manager > Plugins) with slug "homestay-booking-premium"
     matching exactly, and that the keys you issue have the term/grace
     period you intend (set on the server, not in this plugin).
  2. WP Admin > Homestay Booking > License (or Manager Portal > License
     as a Group Admin) > set the Licensing Server URL to https://sitss.in
     (just the base URL -- the plugin appends the API path itself) and
     your Upgrade/Purchase URL.
  3. Test the full activate -> use premium features -> deactivate ->
     features lock again cycle on a staging site before selling, using
     both a purchased key and a manually-issued one, from both WP Admin
     and the portal.

CUSTOMER SELF-SERVICE: MY BOOKINGS, REQUESTS & CANCELLATION POLICY
------------------------------------------------------------------------
Every logged-in customer gets a "My Bookings" tab on their WooCommerce
My Account page (next to Orders), showing every booking they've made,
with:
  - Full details: homestay, dates, nights, guest counts, total, status.
  - A "Print Invoice" button (opens a standalone printable invoice, plus
    an "Email Me a Copy" option).
  - "Request Date Change", "Request Extension", and "Request
    Cancellation" buttons (only shown while the booking's order is in a
    live status -- pending/on-hold/processing/completed). Only one
    pending request is allowed per booking at a time.
  - For cancellation requests, the exact charge % and amount (and refund
    due) is shown to the customer BEFORE they submit, calculated live
    from the homestay's cancellation policy and today's date.

IMPORTANT: submitting a request does NOT change the booking. It creates
a pending request that a manager/admin must review and approve in the
portal's "Guest Requests" section before anything actually happens.

CONFIGURING THE CANCELLATION POLICY & RESCHEDULE FEE:
Available in TWO places (same underlying setting, so they always match):
  - WP Admin > Homestay Booking > Edit Homestay
  - Portal > Homestay Settings (Homestay Admin / Group Admin / unscoped users)
Set up to 5 tiers, each: "Days before check-in" and "Charge %". Tiers
are checked from the most days to the fewest, so list them in that
order. Example (the default):
    7 days before check-in or more  -> 0% charge (free cancellation)
    3 days before check-in or more  -> 50% charge
    0 days before check-in or more  -> 100% charge (no refund)
A guest cancelling 5 days before check-in would fall into the "3 days or
more" tier (50% charge), since it's the highest threshold their days-
before-checkin still satisfies. Leave a tier's "Days" field blank to
skip it if you want fewer than 5 tiers.
The "Reschedule (date change) fee" is a single flat % of the ORIGINAL
booking total, applied as an extra fee line if a date-change request is
approved. Set to 0% for free rescheduling.

REVIEWING REQUESTS (manager/admin side):
Portal > Guest Requests shows every pending request for homestays you
have access to (a red badge on the nav item shows the count), plus a
"Recently Reviewed" history. Each request shows the customer, the
homestay, what's being requested, and -- for cancellations -- the exact
charge/refund. Click Approve or Reject:
  - Approve actually performs the change (see the changelog above for
    exactly what happens per request type).
  - Reject just records the decision (optionally with a note, shown to
    the customer on their My Bookings page).
If an approval can't actually be completed (e.g. the requested new dates
are no longer available), the request stays pending-looking to you but
the specific error is shown so you know why -- nothing partially applies.

PORTAL LOGIN BRANDING
------------------------
WP Admin > Homestay Booking > Portal Branding lets you customize the
login screen everyone sees at your portal URL:
  - Logo: upload an image under Media > Add New, then paste its URL
    into the Logo URL field. Leave blank to use the default mark.
  - Heading: the main title on the login card (default "Homestay
    Manager").
  - Subheading: a smaller line underneath (defaults to your site name
    if left blank).
  - Footer text: an optional line at the bottom of the login card, e.g.
    a support phone number.
This is a single site-wide setting (not per-homestay), since everyone
logs into the same portal.

INVOICE SHARING -- PRINT, EMAIL, WHATSAPP
----------------------------------------------
Every invoice (opened via "Print Invoice" wherever it appears) has three
small icon buttons at the top:
  - PRINT: opens your browser's print dialog with a print-optimized
    layout.
  - EMAIL: sends a complete HTML invoice to the order's billing email
    using WordPress's built-in mail sending. Greys out if the order has
    no email on file. IMPORTANT: many hosting providers' default PHP
    mail() function is unreliable or gets marked as spam -- for
    dependable delivery, install an SMTP plugin (e.g. "WP Mail SMTP")
    and connect it to a real mail provider (Gmail, SendGrid, Mailgun,
    your hosting provider's SMTP, etc.).
  - WHATSAPP: opens WhatsApp (web or app) with a pre-filled message
    summarizing the invoice (homestay, total, status), addressed to the
    order's billing phone number. Greys out if no phone is on file. This
    uses WhatsApp's free "wa.me" link format -- no account, API key, or
    approval process needed, but it can only pre-fill a TEXT message
    (opened for you to review and hit send) -- it cannot auto-send or
    attach a PDF. If you want fully automatic sending with a PDF
    invoice attached, that requires the paid WhatsApp Business API
    (a Meta Business account, phone number verification, and approved
    message templates) -- this is a separate, more involved integration
    your developer can add on request.

ARCHITECTURE NOTE (for future development): the portal does not
duplicate booking logic. It reads from the exact same shared classes as
WP Admin -- HB_Bookings_Data, HB_Availability, HB_Blackout,
HB_Orders_Data, HB_Homestay, HB_Guest_Status, HB_Booking_Extend -- with
homestay-visibility scoping applied uniformly through HB_User_Access.
Any future feature built on top of those shared classes will
automatically appear in both WP Admin and the portal, correctly scoped
per user, by design.

Administrators automatically have portal access too (in addition to
their normal WP Admin access) and see every homestay unscoped, so you
can preview the portal yourself at any time using your own admin login.

INSTALLATION
------------
1. WordPress admin: Plugins > Add New > Upload Plugin > choose the zip
   > Install > Activate. (WooCommerce must already be active.)
2. Go to WP Admin > Homestay Booking to add your first homestay (or see
   your migrated homestay if upgrading from an earlier version).
3. Set up a Homestay Manager account as described above, if needed.
4. Go to WP Admin > Homestay Booking > Bookings / Calendar to manage
   day-to-day operations from WP Admin, or use the portal URL instead.

UPGRADING FROM AN EARLIER VERSION
-------------------------------------
- From v1.x (single homestay): your existing product/settings migrate
  automatically into your first homestay entry.
- From v2.x: your existing homestays, bookings, and closed dates carry
  over unchanged. Existing full-day closes are preserved as full closes
  (rooms_blocked = that homestay's total room count) the first time this
  version runs.
- The Homestay Manager role is registered automatically on activation/
  upgrade -- no manual setup needed beyond assigning it to a user.

HOW THE BOOKING FORM WORKS FOR GUESTS
--------------------------------------
On each homestay's own product page, guests see:
   - Check-in / check-out dates
   - Adults (up to that homestay's configured max)
   - Children [paid age band] (charged the extra child fee)
   - Infants [free age band] (no charge, but still counted toward a
     room's total occupancy/space)
Live price and availability are checked as the guest adjusts these
fields; "Add to Cart" stays disabled until a valid, available
combination is selected.

IMPORTANT NOTES / LIMITATIONS
-------------------------------
- Multi-room splitting fills rooms to capacity in order, not an even
  spread -- standard hotel-engine behavior.
- If a group has more children/infants than can be paired with an
  adult-occupied room, the plugin opens an extra room and bills it at
  the normal minimum rate rather than a discounted rate, to avoid
  under-charging. Rare in practice with sensible occupancy settings.
- Deleting a homestay sets its WooCommerce product to Draft (not
  deleted), so past orders remain intact.
- The Orders section in the portal deliberately excludes refund actions
  -- process refunds through WooCommerce/your payment gateway directly
  for proper accounting.
- Extending a booking creates a new order rather than processing a new
  payment automatically -- collect payment for that new order through
  your normal process (cash, UPI, a payment link, sending it to the
  guest to pay online, etc.).
- The plugin does not sync with Airbnb/Booking.com/MakeMyTrip calendars.
- Tested for logic and PHP syntax correctness, but has not been run
  against a live WordPress + WooCommerce install. Please test thoroughly
  on staging first -- especially:
    - After activating/upgrading, confirm https://yoursite.com/hb-dashboard/
      loads the portal login (if it 404s, visit WP Admin > Settings >
      Permalinks and click Save once to force a rewrite-rules refresh).
    - Assign a test user the Homestay Manager role to ONE homestay, and
      confirm they see ONLY that homestay everywhere in the portal (and
      cannot access another homestay by editing the URL).
    - Assign a second test user the Homestay Admin role, confirm they
      see the "Homestay Settings" tab and can save changes, and confirm
      a plain Homestay Manager does NOT see that tab.
    - Create a Group Admin account, confirm they see ALL homestays
      unscoped and can create/assign a Homestay Manager from the "Team"
      section, and confirm a plain Homestay Manager/Admin does NOT see
      the Team tab.
    - Extend a real test booking and confirm a NEW order is created with
      the same guest's billing details, the extra nights show up
      correctly in the calendar, and the original order is untouched.
    - Open "Print Invoice" on an order and confirm it renders correctly
      and prints cleanly (Print Preview in your browser).
    - Check a guest in and out from the portal, confirm they move
      between the Arrivals / In-House / Departures lists correctly.
    - Resize your browser below ~900px (or use a phone), confirm the
      hamburger menu opens/closes the sidebar and Logout is reachable.
    - Book 2 adults + 2 infants and confirm it stays in 1 room (previously
      incorrectly required 2).
    - Create a manual "New Booking" with a custom price, confirm it
      appears in Bookings/Calendar/Orders exactly like a normal booking,
      and confirm the invoice shows the custom amount you entered.
    - Look at the Calendar for a homestay with a mix of free, partially
      booked, and fully booked/closed dates, and confirm the colours
      (green/amber/red) match what you'd expect.
    - Set a logo/heading/footer under Portal Branding, log out, and
      confirm the login screen reflects your changes.
    - Open an invoice with a valid billing email and phone, and test
      both the Email button (check the inbox, and check your spam
      folder if it doesn't arrive -- see the SMTP note above) and the
      WhatsApp button (confirm it opens with the message pre-filled).
      Also open an invoice with a missing email or phone and confirm
      that button is greyed out instead of erroring.
    - After activating/upgrading, confirm My Account > My Bookings loads
      (if it 404s, visit WP Admin > Settings > Permalinks and click Save
      once, same as the /hb-dashboard/ fix above).
    - As a customer, submit a cancellation request and confirm the
      charge/refund preview shown matches your configured policy tiers.
      Then approve it from the portal's Guest Requests and confirm the
      order is cancelled and the dates free up on the calendar.
    - As a customer, submit a date-change request to dates that overlap
      an existing booking at that homestay, and confirm the portal
      correctly refuses to approve it (with a clear error) and leaves
      the original booking untouched.
    - As a customer, submit an extension request and approve it, and
      confirm a new order is created for the extra nights (same as the
      manager-side Extend Booking feature).
    - On a fresh Free install, confirm: only 1 homestay can be created
      (the "Add New" form shows an upgrade notice after that), the
      portal URL shows "Upgrade to Premium" instead of a login form, and
      the Calendar's "Rooms to close" field is disabled at full-property-
      only. Then activate a valid license and confirm all of the above
      unlock immediately. Then deactivate and confirm they lock again.
    - Confirm "homestay-booking-premium" is registered on sitss.in
      before testing, or activation will correctly fail with "License
      key not found for this product." Activate with a real key from
      sitss.in (purchased or manually issued -- both should work
      identically) and confirm Premium unlocks. Try an invalid key and
      confirm a clear error. If you can, let a license reach its grace
      period on the server and confirm the plugin shows the renewal
      notice but still keeps Premium features unlocked.
    - With the site-wide license ACTIVE, Force Off one homestay via the
      Per-Homestay Premium Override table and confirm only that
      homestay drops to Free (partial closing disabled, guest requests
      hidden) while every other homestay keeps Premium. Then Reset to
      Default and confirm it returns to Premium immediately. Also try
      Force On with the site-wide license INACTIVE and confirm that one
      homestay gets Premium while the rest correctly stay Free.



5.21.0 (Tour Package Room-Count Fix, Manual Room Count, Car Allocation by Group Size)
- Reported two bugs: (1) "I have done a tour package booking with celeste
  tamang homestay for 5 adults as per room capacity it should block 2
  rooms but it blocks 1 room & in invoice also shown 1 room. for manual
  homestay or hotel addition in packeage room count entry option need
  to ba added at the backend after entering the homestay name, & same
  count should be visible in the invoice." (2) "car choice is fixed to
  hatchback as per package, car choice will be as per group, as per
  group size car need to be added like for 2-3 hatchback 3-4 sedan
  more than 4 SUV & backend should have option to allocate cars to
  override this default choice."
- ROOM COUNT BUG root-caused to every itinerary homestay-stay step
  hardcoding 1 room and 1 adult on every blocking row it wrote,
  regardless of the booking's actual adults/children -- present in
  THREE places: HB_Tour_Package_Availability::check()'s availability
  test (always passed rooms_needed=1 to is_range_available()),
  HB_Order::insert_tour_package_booking_rows() (the checkout path,
  always inserted exactly 1 row per date), and
  HB_Tour_Package_Availability::finalize_step() (the admin
  post-purchase assignment path, same hardcoding). None of the three
  ever computed a real room count from the group size the way every
  other room-wise booking type in this plugin already does via
  HB_Pricing::rooms_needed_for_group(). Fixed all three to compute it
  properly (added two shared helpers,
  HB_Tour_Package_Availability::rooms_needed_for_component() and
  rooms_needed_for_itinerary(), so the three callers can't drift back
  out of sync with each other) and to write one blocking row PER ROOM
  per date (matching insert_category_booking_rows()'s own pattern)
  instead of always exactly one. Also found insert_tour_package_booking_rows()
  never set _hb_rooms_needed on the order item at all, which is why
  the invoice always fell back to the item's own quantity (always 1)
  for a Tour Package's Rooms column -- now set from the same
  computation, so the invoice shows the real count.
- MANUAL ROOM COUNT: an externally-typed homestay/hotel entry (ref_kind
  'manual', no record in this plugin to compute occupancy against) had
  no way to record how many rooms it represents at all. Added a
  manual_rooms column (component template) and reused the resolved
  booking's own rooms_needed column for it (dbDelta self-heal, same
  pattern as every other schema addition here), a "Number of Rooms"
  field wherever a manual entry can be typed (the Add/Edit step forms,
  and the finalize-a-pending-step form), and wired it through
  decode_ref_choice() -> HB_Tour_Package_Component::create()/update()
  and HB_Tour_Package_Availability::finalize_step() so it reaches the
  same rooms_needed_for_component()/_for_itinerary() helpers and,
  from there, the invoice. Also fixed a pre-existing display bug found
  while touching this code: the Itinerary listing table's "Item"
  column checked `! $c->ref_id` to decide "Unspecified (finalize
  later)" -- but a manual entry ALSO has ref_id 0, so it was
  incorrectly showing as unspecified instead of its typed name; now
  checks ref_kind first and shows the name plus its room count.
- CAR ALLOCATION BY GROUP SIZE: added a new per-car-step rules table
  (hb_tour_package_car_rules: component_id, min_guests, max_guests,
  car_id, optional price override) and class
  (HB_Tour_Package_Car_Rule), deliberately scoped to a single car
  COMPONENT rather than the whole package, since a package can have
  more than one car step (e.g. an arrival pickup and a departure
  drop) that may each need a different group-size-to-car mapping.
  Wired into HB_Tour_Package_Availability::resolve_itinerary() --
  every caller (availability check, blocking, invoicing) already goes
  through this one function, so overriding a car component's ref_id
  (and, if the matching rule sets one, its price) there means nothing
  downstream needed its own resolution logic. A guest count matching
  no rule, or a car step with no rules at all, keeps using the step's
  own single default car and price, unchanged. Management UI lives on
  the car step's own Edit Step screen ("Car Allocation by Group
  Size"), mirroring the existing Pricing Tiers UI's list-plus-add/edit-form
  shape. Also cascaded car-rule deletion into
  HB_Tour_Package_Component::delete()/delete_for_package() -- the same
  orphaned-row gap the old per-step pricing tiers table had before it
  was cascaded, avoided here from the start.
- Verified: standalone PHP simulation of both
  HB_Pricing::rooms_needed_for_group() (5 adults at 3-per-room needs 2
  rooms; at 2-per-room needs 3; 2 adults still needs 1) and the car
  rule guest-count ladder (2-3 -> hatchback, 4 -> sedan, 5+ -> SUV, 1
  guest falls through to the step's own default) against hand-picked
  cases before wiring them in. Full-plugin `php -l` sweep and
  class-hb-portal.php's HTML tag-balance check (form/table/div/select/
  script/tr/td) both clean after all edits. No live/browser
  verification was possible this session (browser tooling
  unresponsive, as in earlier sessions) -- worth specifically
  re-checking the Celeste Tamang Homestay booking mentioned in the
  bug report once this is live. Note: a Tour Package step already
  sitting in pending_finalization from BEFORE this update was deployed
  won't have its real adults/children recorded (that also wasn't
  written before this fix), so its room count at finalize time will
  still fall back to the old 1/0 defaults -- only bookings made after
  this update are fully correct end to end; this is a one-time gap for
  already-pending bookings, not an ongoing issue.

5.20.0 (Per-Head Rate Visibility, Invoice Unit Price, Cars/Activities Edit)
- Reported: "while selecting the guest count applicable per head price
  should be visible to admin or customer, in invoice amount is shown,
  room wise, guest wise or package wise unit price is also need to be
  given for better clarity." Then, mid-session: "for cars & activities
  in tour packages price edit option need tobe added." Three separate
  pieces of work, released together.
- RATE VISIBILITY: since 5.18.0's package-level pricing tiers, neither
  the customer product page nor the admin "+ New Booking" Tour Package
  form showed which rate a guest-count selection actually resolved to
  -- only the final total. Fixed on both sides:
    - Customer side: HB_Tour_Package_Availability::check()'s per_head
      branch now returns the actually-resolved rate_per_adult/
      rate_per_child (the matched tier's rates, or the package's own
      flat rate when no tier matches) alongside the total.
      ajax_check_tour_package() passes these through, and
      hb-frontend.js's runTourPackageCheck() renders them under the
      total, e.g. "Rs.1800.00/adult x 3 + Rs.900.00/child x 1", updating
      live as Adults/Children change (the existing change-event
      bindings already covered this, no new binding needed).
    - Admin side: render_new_tour_package_booking_form()'s static
      "Priced at X/adult" note is replaced with a live preview driven
      by a small inline script -- the package's tiers are embedded as
      JSON at render time, and the script picks the matching band (or
      falls back to the flat rate) as the Adults/Children inputs
      change, with no extra AJAX round-trip needed since the tier data
      is already known at page-render time.
- INVOICE UNIT PRICE: added a "Unit Price" column to the invoice's
  items table, computed by resolve_item_info() from the line's own
  frozen amount (never recomputed against live/current pricing, so a
  price change after booking doesn't alter a past invoice):
  Room-wise/Category -> "Per Room/Night" (amount / (rooms x nights));
  Per-Head Tour Package and guest-wise Package -> "Per Adult"
  (amount / adults); flat Tour Package -> "Per Package"
  (amount / quantity). While wiring this column in, found and fixed
  two pre-existing off-by-one colspan bugs on the "Includes" and
  additional-fee sub-rows (present since 5.16.0 added the Guests/Rooms
  columns, unnoticed because a short colspan on those two rows just
  renders one column narrower rather than visibly breaking) --
  colspans re-derived by explicitly enumerating the header's actual
  column count (9 without Room No., 10 with, after adding Unit Price)
  rather than incrementing the old values by one.
- CARS & ACTIVITIES EDIT: render_cars_activities_section() only ever
  supported Add + Remove for a homestay's Cars and Activities --
  correcting a price meant deleting the resource and adding a new one,
  which orphans any itinerary step or booking that already references
  the old ID. Found HB_Car::update() and HB_Activity::update() already
  existed as complete, working methods, just never called from
  anywhere -- only create() was wired up. Added an "Edit" link per row
  (?edit_car=<id> / ?edit_activity=<id>) that pre-fills the existing
  Add form (including a Status select, shown only while editing) and
  switches its submit button to Save; save_car/save_activity's POST
  handlers now look up an existing car_id/activity_id belonging to the
  same homestay and call update() instead of create() when one is
  found.
- Verified: full-plugin `php -l` sweep clean across every .php file.
  HTML tag-balance check on class-hb-portal.php clean (form 59/59,
  table 22/22, div 380/380, select 39/39, script 2/2, tr 54/54,
  td 141/141) after all three pieces of work. hb-frontend.js syntax-
  checked with `node --check`. No live/browser verification was
  possible this session (browser tooling unresponsive, as in earlier
  sessions) -- code-level lint, tag-balance, and manual trace of the
  tier-resolution and colspan arithmetic were used instead.

5.19.0 (Bookings List: Sortable Columns + Drop Orders That No Longer Exist)
- Reported (with a screenshot of the Room-Wise Bookings list): bookings
  should show latest booking ID at the top, sorting should ideally be
  available on every column defaulting to Booking ID, and old
  cancelled bookings whose order no longer exists should be cleared
  from the page.
- The screenshot's own rows made the third issue concrete: several
  rows showed "—" for Customer (order_id 1752, 1751, 1750, 1749,
  1744) -- render_section_bookings()'s Room-Wise view builds its rows
  straight from HB_Bookings_Data::get_rows_for_homestays() (the raw
  hb_room_bookings table) with no check that the underlying WooCommerce
  order still exists, so a permanently-deleted order's booking rows
  never leave the list, showing get_customer_name()'s '—' fallback
  forever. Confirmed the sibling Tour Package Bookings list
  (render_tour_package_bookings_list()) already guards against exactly
  this with `if ( ! $order ) { continue; }` -- another instance of the
  recurring pattern in this plugin where a fix lands in one list but
  not its sibling. Applied the same guard to the Room-Wise list.
- Previous sort was a single fixed `ORDER BY checkin DESC` at the SQL
  level, and there was no way to sort by anything else. Rebuilt
  render_section_bookings() to resolve every row's full display data
  up front (homestay name, type label, resolved customer name, etc.
  -- fields the original per-row loop only computed at render time,
  which meant they couldn't be sorted on) into a plain PHP array,
  dropping any row without a resolvable order in the same pass, then
  sorts that array in PHP with usort() against an $orderby/$order pair
  read from the query string (whitelisted against the known column
  keys, default order_id/desc so the highest -- i.e. newest -- booking
  ID is on top by default, matching what was asked). Numeric columns
  (order_id, nights, rooms, adults, children, infants) compare
  numerically via <=>; text columns (homestay, type, customer,
  checkin, status) compare with strcasecmp() -- checkin sorts
  correctly as plain ISO Y-m-d string comparison.
- Every data column header (Homestay, Type, Order, Customer, Check-in,
  Nights, Rooms, Adults, Children, Infants, Status) is now a clickable
  sort link, WP-Admin-list-table style: clicking an unsorted column
  sorts it ascending, clicking the already-active column reverses it,
  with an up/down arrow next to the active column's label. Extend
  Stay and Actions stay plain headers since they're not row data.
  Sort links carry the current Homestay filter forward (portal_url()
  builds each link fresh rather than merging the existing query
  string, so this has to be explicit) so sorting doesn't reset the
  filter.
- Verified the sort comparator directly with a standalone PHP
  simulation across order_id desc (default), checkin asc, and adults
  desc against a small sample set -- each produced the correct order.
  Full lint + form/table/div/select tag-balance check (form 59/59,
  table 22/22, div 378/378, select 37/37) clean after the rewrite.

5.18.0 (Pricing Tiers Redesigned as Package-Level Per-Head Bands)
- Reported, right after 5.17.0 shipped: "you have done the pricing
  of package wrong. pricing tire & step is not linked, steps are
  only for itienary. pricing tire should be with package at the
  very beginning where price per adult option given there shoub be
  this pricing tire should be added. like price per adult with
  minimum 2 guest, price per adult for 3-4 guest, price per adult
  5-6 guests, price per adult 6+ guests. this adult count will be
  drop down & pricing will be auto calculatd as per the tire after
  guest selects the guest count." This is a correction to the
  underlying model, not a bug in the 5.15.0-5.17.0 per-step tiers
  feature: that feature let a STEP resolve to a different
  resource/price by headcount (useful for Flat-priced packages), but
  never gave a Per-Head package a tiered per-adult RATE -- which is
  what he actually wanted, and it should never have depended on
  itinerary steps at all ("steps are only for itienary").
- Asked him directly (AskUserQuestion) whether to keep the old
  per-step tiers feature for Flat packages or remove it entirely
  now that pricing tiers live at the package level -- he said remove
  it entirely.
- Removed the per-step pricing tiers feature completely:
  deleted class-hb-tour-package-component-tier.php
  (HB_Tour_Package_Component_Tier) and the
  hb_tour_package_component_tiers table's creation call; removed its
  three POST handlers (add/update/delete_tour_package_component_tier)
  and the render_tour_package_pricing_tiers() UI that listed tiers
  per step; removed the tier-resolution block from
  HB_Tour_Package_Availability::resolve_itinerary() (a step's own
  ref_id/ref_kind/manual_label/price are always used exactly as
  configured now, never overridden by a per-guest-count tier); removed
  the delete_for_component() cleanup call from
  HB_Tour_Package::delete(). Left the old hb_tour_package_component_tiers
  DB table itself in place on any site that already has it (dropping
  a live table on a plugin update is unnecessary risk) -- it's just
  never read or written anymore.
- Added the new model instead: a package-level guest-count pricing
  tier table, hb_tour_package_pricing_tiers (package_id, min_guests,
  max_guests, price_per_adult, price_per_child), created by
  HB_Install::create_tour_package_pricing_tiers_table() and managed
  by the new HB_Tour_Package_Pricing_Tier class (create/update/
  delete/get_for_package/resolve_for_guest_count -- same shape as
  the old per-component tier class, just keyed by package_id and
  carrying price_per_adult/price_per_child instead of a resource +
  single price).
- HB_Tour_Package_Availability::check()'s existing per_head total
  calculation (previously a flat price_per_adult x adults +
  price_per_child x children) now first resolves a matching package
  tier by TOTAL guest count (adults + children) via
  resolve_for_guest_count(), and uses that tier's own
  price_per_adult/price_per_child when one matches; a guest count
  outside every tier (or a package with no tiers at all) falls back
  to the package's own flat price_per_adult/price_per_child exactly
  as before -- fully backward compatible for every existing Per-Head
  package. resolve_itinerary() no longer takes any tier into account
  for the itinerary/component display, matching the instruction that
  steps only describe the itinerary.
- New render_tour_package_pricing_tiers( $package ) on the portal's
  package edit page: renders right after the Price Per
  Adult/Price Per Child fields (only when Pricing = Per Head),
  listing every band (Guests / Price Per Adult / Price Per Child /
  Action) with Edit/Remove, plus one Add/Edit form below (Min Guests,
  Max Guests -- with a note that 999 makes an open-ended "6+" band --
  Price Per Adult, Price Per Child). New add/update/delete_tour_
  package_pricing_tier POST handlers, gated the same
  can_manage_group()+can_access_homestay() way as every other
  package-management action, nonce hb_portal_tour_package_pricing_
  tiers_<package_id>. The per-step Edit screen's "Default Price"
  field description was reworded since it no longer has any tier
  qualifying it.
- Verified the exact banding he described (2 guests / 3-4 / 5-6 /
  6+) resolves correctly with a standalone PHP simulation of
  resolve_for_guest_count()'s matching logic across guest counts
  1,2,3,4,5,6,7,10 -- each lands in the right band, and a
  count below every band's minimum (1 guest, with the lowest band
  starting at 2) correctly falls through to "no tier matched, use
  the flat rate" rather than picking the nearest band and guessing.
- Full lint sweep (every .php file), duplicate-method-name grep, and
  form/table/div/select tag-balance check (form 59/59, table 22/22,
  div 378/378, select 37/37) all clean after removing the old
  per-step tier code and adding the new package-level tier code.

5.17.0 (Pricing Tiers Moved to Package Main Page + Tier Edit)
- Reported: couldn't find Pricing Tiers at all ("not showing yet
  whare to get this?") -- walked him through the actual click path
  (Tour Packages -> open a package -> Edit a specific itinerary step
  -> scroll down), which surfaced the real problem: it was nested two
  levels deep inside one step's own Edit screen, easy to miss
  entirely, and not where he expected a "2/4/6 guest" package-level
  concept to live (matches his original framing of this feature: "3
  type of price band should be there starting with 2 then 4 & 6").
  Also asked for an Edit option on existing tiers (previously only
  Add/Remove existed). Then asked directly: "pricing tire should be
  on the tour package no on the steps, remove from step & bring it
  to package main page."
- Moved Pricing Tiers off render_tour_package_component_edit_form()
  (the per-step Edit screen) entirely and onto
  render_tour_package_edit_form() (the package's own main page),
  right after the Itinerary section -- so it's visible as soon as
  you open a package, no step click required. New consolidated
  render_tour_package_pricing_tiers( $package, $owning_homestay )
  lists every tier across EVERY step in one flat table (columns:
  Step, Guests, Resource, Price, Action), with a single Add/Edit
  form beneath it. Deleted the old per-step renderer entirely
  (render_tour_package_component_tiers()) rather than leave a dead
  copy lying around.
- The consolidated Add/Edit form has a "Step" dropdown so the admin
  picks which itinerary step a new tier applies to; each option
  carries the step's component_type in a data-type attribute, and a
  small onchange script shows only the matching resource picker
  (homestay/car/activity) -- since which resource fields make sense
  depends on the step type, and now one form has to handle all three
  instead of the old per-step version only ever needing its own
  type's picker.
- Added the requested Edit option: added HB_Tour_Package_Component_
  Tier::update() (mirrors create()'s field handling) and a new
  update_tour_package_component_tier POST handler, gated the same
  can_manage_group()+can_access_homestay() way as every other tier
  action. Clicking "Edit" on a tier row reloads the SAME package
  page with ?edit_tier=<id>, which pre-fills the Add/Edit form with
  that tier's values, locks the Step field (disabled -- moving a
  tier to a different step is delete-and-re-add, same as before),
  and switches the submit action/button to Save Tier + adds a
  Cancel link back to the plain package page.
- Full lint sweep, duplicate-method-name check, and
  form/table/div/tr/td/select tag-balance check all clean after
  removing the old method and adding the new one.

5.16.1 (Portal Cache-Hardening)
- Reported: (1) a normal room-wise booking created via the portal's
  "+ New Booking" screen wasn't showing up on the Bookings list,
  though the order itself existed fine; (2) a 3-night Tour Package
  invoice showing 4 nights, "probably taking the old 4-night package
  not the updated 3-night one". Line-by-line audited every step of
  both: HB_Manual_Booking::create()'s direct $wpdb->insert() into
  hb_room_bookings (schema matches, no silent failure mode found),
  HB_Bookings_Data::get_rows_for_homestays()/get_rows() and
  HB_Homestay::get_all_product_ids() (correct, and both the New
  Booking homestay dropdown and the Bookings list's homestay
  scoping already draw from the exact same HB_User_Access::
  get_allowed_homestays(), so they can't disagree about which
  homestays a given user can see), and separately
  save_tour_package_line_item_meta()'s itinerary-span computation
  (already correctly prefers the homestay component's own span over
  every component's, specifically to avoid exactly this kind of
  inflation -- a fix already in place from an earlier pass). Found
  no code defect in either path on direct review. Attempted to
  verify live via the Chrome browser extension but the connected
  browser was unresponsive across several attempts (timed out on
  every screenshot/page-read), so couldn't observe the live state
  directly this pass.
- Asked him directly rather than keep guessing: confirmed the Tour
  Package "4 nights" invoice was an OLDER booking placed BEFORE he
  edited that package's homestay step down to 3 nights -- an order
  correctly freezes what was actually booked at purchase time, so
  this is expected behavior, not a bug. Confirmed the missing
  room-wise booking's order does exist, just isn't appearing in the
  Bookings list.
- Given that combination (order genuinely created, list genuinely
  not showing it, no logic bug found across the whole chain), and
  that the site runs the SpeedyCache plugin (visible in an earlier
  screenshot), the strongest remaining explanation is page caching:
  the portal (/hb-dashboard/) is a raw front-end page rendered via
  template_redirect, not a wp-admin screen, so it has none of the
  usual "don't cache wp-admin" special-casing a caching plugin
  applies automatically -- only its logged-in-user detection (which
  not every caching plugin implementation covers the same way, and
  some only skip caching for the exact page a user is authenticated
  against). Hardened defensively: the portal now calls
  nocache_headers() and defines DONOTCACHEPAGE on every request,
  right after the login/access gate and before any output --
  respected by WP Rocket, W3TC, WP Super Cache, LiteSpeed Cache, and
  most others. This can only help regardless of whether caching
  turns out to be the actual cause; if the room-wise booking is
  still missing after this update (hard-refresh the Bookings page
  first to rule out a browser-side cached copy too), the next step
  is a live look at the actual "Filter by Homestay" state and the
  hb_room_bookings row itself, which needs DB/live access this pass
  didn't have.
- Full lint sweep clean after the change.

5.16.0 (Invoice Itinerary Fixes + Guests/Rooms on Invoice)
- Reported: pricing tiers "not available till now" -- checked the
  Edit Step screen, the Pricing Tiers card beneath it, both POST
  handlers (add/delete_tour_package_component_tier), and the dbDelta
  self-heal for hb_tour_package_component_tiers end to end; all of
  it is correctly wired and has been since 5.15.0. This looks like
  a deployment gap (the live site not yet updated past whatever it
  was running) rather than a code bug -- nothing to fix here, flagged
  back to him to confirm 5.15.1/5.16.0 actually deployed before
  reporting this one again.
- Fixed: a 3-night package's invoice itinerary page was rendering as
  a 5-day span with the extra day(s) shown as an unexplained "Free
  day". Root cause: a component's end_date is EXCLUSIVE (the day
  after the last day it covers -- e.g. a homestay stay's checkout
  date, or a car's day_offset+duration), but the itinerary page's
  day-map bounds computation treated it as inclusive, adding one
  extra calendar day per span component; taking the MAX across every
  component (the homestay stay, a departure-day car, etc.) then
  stacked those off-by-ones into a visibly wrong extra day at the
  end. Fixed by normalizing every span component's end_date to its
  actual last inclusive day (end_date minus one) before computing
  the day-map's overall latest date, and did the same to the
  checkin/checkout fallback path. The per-day line-population loop
  further down was already handling the exclusive/inclusive split
  correctly (it wasn't touched) -- only the frame/bounds computation
  had the bug. Verified against three simulated cases directly (a
  plain 3-night stay, a 3-night stay plus a same-day departure car,
  and a 3-night stay with a last-night activity) before shipping.
- Added: the Celeste logo now also appears at the top-right corner
  of the invoice's day-wise itinerary page (page 2) -- previously
  only the main invoice page (page 1) had it. Reuses the same
  site_logo_url() fallback chain and print-color-adjust handling as
  the main page's logo box.
- Added: the invoice's line-items table now has Guests and Rooms
  columns for every booking line (not just Room No., which stays
  optional/homestay-controlled). Guests reads _hb_adults/_hb_children
  item meta, set consistently across every booking type. Rooms reads
  _hb_rooms_needed item meta where it exists (Homestay/Category
  bookings) and falls back to the order item's own quantity where it
  doesn't (Package and Tour Package bookings, where one unit
  purchased has always equalled one room -- the exact convention
  insert_package_booking_rows() itself already uses). Updated the
  colspans on the "Includes" and fee sub-rows to match the two new
  columns. Full lint sweep + tag-balance check clean afterward.

5.15.1 (Tour Package Delete + Pricing Type Save Bug)
- Fixed: pricing_type (Flat vs Per-Head), price_per_adult and
  price_per_child were never included in the $data array built by
  the save_tour_package POST handler in class-hb-portal.php, even
  though HB_Tour_Package::create()/update() already had full field-
  map support for all three. A pure handler bug -- whatever the
  admin picked in the form, the saved package always fell back to
  Flat with zero per-head prices, on both new packages and edits.
  Fixed by adding the three missing keys to the $data array.
  Verified via a direct before/after PHP simulation of the handler
  logic plus php -l.
- Added: a Delete option for Tour Packages. Previously the portal's
  Tour Packages list only offered Edit/Duplicate -- there was no way
  to remove a package entirely, only its individual itinerary steps
  one at a time. Added a Delete link next to Duplicate (with a
  confirm() warning that itinerary steps and pricing tiers go too,
  but past orders/bookings are unaffected) and a new
  delete_tour_package POST handler, following the same
  can_manage_group() + can_access_homestay() access check used by
  every other Tour Package action.
- While wiring the delete handler, found and fixed a related latent
  gap: HB_Tour_Package::delete() cascaded to itinerary components
  but never cleaned up hb_tour_package_component_tiers rows --
  that table didn't exist yet when delete() was first written
  (added in 5.15.0). Left as-is, deleting a package would have
  orphaned any pricing-tier rows attached to its components. Fixed
  by looping each component via HB_Tour_Package_Component::
  get_for_package() and calling HB_Tour_Package_Component_Tier::
  delete_for_component() before the components themselves are
  removed.
- Full lint sweep (php -l on every file), duplicate-method-name
  check, and form/table/div/tr/td tag-balance check on
  class-hb-portal.php all clean after these three changes.

5.15.0 (Edit Itinerary Steps + Headcount Pricing Bands)
- Added the ability to edit an existing itinerary step in place --
  its slot, resource, dates/duration, and default price -- instead
  of deleting and recreating it every time, as requested.
- Added per-component pricing tiers: a step (most commonly a car,
  but any type) can now define guest-count BANDS, each with its own
  resource and price -- e.g. a Hatchback for 1-2 guests, a Sedan for
  3-4, an SUV for 5-6. Confirmed the exact model with him: 3 fixed
  bands starting at 2/4/6, a single guest pays the 2-person band's
  price (no smaller tier exists), and this is FLAT pricing with
  tiered components -- not per-head pricing, which uses a separate
  linear formula that wouldn't apply bands at all. A component with
  no tiers defined keeps behaving exactly as before; this is purely
  additive. New table, hb_tour_package_component_tiers.
- New portal screens: an Edit Step form, and a Pricing Tiers manager
  beneath it (add a band with its own resource/price, remove one).
  Reuses the same "Homestay -- Category" / base-pool / "Other"
  picker the itinerary builder already has, so a tier's resource
  follows the identical pattern.
- Wired guest-count-aware resolution through every path that touches
  an itinerary: the live availability/price check, the checkout
  blocking logic, and the admin manual-booking flow -- all three
  now correctly pick the tier matching the actual guest count,
  automatically, with no separate per-path tier logic to keep in
  sync. Verified the exact scenario he described directly: a lone
  guest pays the 2-person band's price, and each band resolves to
  a genuinely different car and price.
- Caught a real bug of my own while verifying this, before it
  shipped: a tier left with "Same as Default" for its resource (only
  meant to change price for a band, not the car/homestay) would have
  had its override logic blindly zero out the component's actual
  resource instead of leaving it alone. Fixed to only override the
  resource fields when a tier genuinely specifies one, while always
  overriding price. Verified both cases directly.

5.14.0 (Homestay Base Pool, "Other" Homestay, and the Real Orders-Page Bug)
- Tested live via the Chrome extension against the actual site rather
  than relying on code review alone this time -- found that v5.13.2
  hadn't even deployed yet (WordPress had it "scheduled" 11 hours
  out despite the version string already reading 5.13.2); forced the
  update through immediately and confirmed View Itinerary and the
  Tour Package Bookings list both work correctly on live data before
  investigating anything further.
- Root cause of "homestay dropdown shows blank" (itinerary builder
  and finalization form both): Celeste Tamang Homestay has no Room
  Categories at all -- confirmed live ("No room categories yet") --
  and both dropdowns only ever offered Room Categories as options.
  A homestay with no categories therefore had zero options, not a
  loading glitch. Added each homestay's own base room pool as a
  selectable option (new ref_kind 'homestay', alongside the existing
  'category'), and his other request -- an "Other -- homestay not
  listed here" free-text option (new ref_kind 'manual', with a new
  manual_label column) for allocating a stay at a property outside
  this platform entirely. New schema: ref_kind + manual_label on
  both hb_tour_package_components and hb_tour_package_bookings.
  Rewired every layer that touches a homestay-stay component to
  understand all three kinds consistently: the shared option-builder
  and decoder (used by both dropdowns and both handlers, so they
  can't drift apart), HB_Tour_Package_Availability::check() and
  ::finalize_step(), and HB_Order::insert_tour_package_booking_rows().
  Caught a real correctness bug of my own while doing this: a manual
  entry has ref_id=0 by design (nothing to resolve), which is the
  EXACT same signal the code already used for "still pending
  finalization" -- without a fix, a deliberately-typed external
  homestay would have shown as "Needs finalization" forever. Fixed
  by checking ref_kind alongside ref_id everywhere that distinction
  matters.
- Found the REAL root cause of Tour Package orders missing from the
  Orders page -- a different, deeper bug than v5.13.2's fix
  addressed. HB_Orders_Data only ever queries hb_room_bookings to
  discover which orders exist for a homestay, but a finalize_later
  package's still-pending homestay step writes nothing there at all,
  and even a resolved package's ONLY blocked resource might be a car
  or activity, living in a table this query never looked at.
  Confirmed directly: orders #1744/#1743 had zero hb_room_bookings
  rows between them. Fixed by also checking
  hb_tour_package_bookings (which always has a row, resolved or
  not) as a second source of order IDs, merged and re-sorted, in
  both get_orders() and get_orders_for_homestays().
- Caught the same orphaned-code mistake pattern once more while
  adding a new method to HB_Tour_Package_Bookings_Data -- an edit
  consumed the return statement and closing brace of the method
  above it. Caught immediately by lint as a parse error, fixed, and
  verified the whole file's structure before moving on.
- Verified directly rather than assumed: the order-id merge/sort/
  limit logic, and the ref_choice decoder across all four cases
  (category, homestay, manual/other, and empty/unspecified).

5.13.2 (Tour Package Orders Missing from the Orders List)
- Reported: Tour Package bookings didn't show up on the portal's
  Orders page at all, and by extension had no working Invoice link
  there either. Root cause: HB_Homestay::get_all_product_ids() -- the
  ONE shared helper both HB_Orders_Data and HB_Bookings_Data use to
  find "every product ID this homestay's orders/bookings could be
  under" -- already walked a homestay's own product, its Room
  Categories, and its guest-wise Packages, but was never extended to
  also walk its Tour Packages when that feature was built. The exact
  same root-cause SHAPE as v5.13.1's fix (a shared resolution helper
  never updated for the newer booking type) and the same shape as two
  earlier bugs before that (v4.7.2, v5.2.2) -- this is now the fourth
  time this plugin has hit "new booking type added, a shared
  resolution/scope helper elsewhere never updated for it." Given how
  often this exact pattern keeps recurring specifically around Tour
  Packages, any FUTURE product type this plugin adds should get a
  sweep of every place a homestay's own product/category/package ids
  get resolved, not just the obvious one.
- One-line fix, in the one place both the Orders and Bookings lists
  already share: added the same Tour Package resolution the other
  two product types already had. The Orders list itself needed no
  changes at all -- it already renders every order generically
  (including its Invoice link), so once the order becomes
  discoverable, both the row and its working invoice link appear
  together, no separate fix required. Verified the fix directly:
  simulated the resolution before and after, confirming a Tour
  Package's product id is now correctly found.

5.13.1 (Critical Scope-Check Fix + Always-On Headcount)
- Reported: "View Itinerary" gave "Booking not found, or you do not
  have permission to view it" even for a booking the viewer clearly
  owned. Root cause: product_is_in_allowed_scope() -- the ONE shared
  function gating invoice viewing, check-in/out, order-status
  changes, Edit Booking, Extend Booking, guest-request approval, AND
  View Itinerary -- resolves a product back to its owning homestay by
  checking Homestay, then Room Category, then guest-wise Package, but
  was NEVER updated to also check Tour Package when that feature was
  built starting several versions ago. Every Tour Package order has
  therefore been silently failing EVERY one of those checks this
  whole time, not just View Itinerary -- this is the exact
  "package branch added, category branch forgotten" pattern this
  plugin has hit and fixed before (v4.7.2, v5.2.2), recurring here
  because Tour Packages arrived later and this shared function was
  never revisited for it. One-line fix in the one place all of those
  features already funnel through; verified the fix resolves the
  exact reported scenario directly before shipping.
- Added headcount (Adults/Children) as an ALWAYS-available field on
  Tour Package bookings, admin-side and customer-side -- previously
  only shown for per-head-priced packages, so a flat-priced package
  (his own reported case: "Custom 4 nights package") had no way to
  record group size at all. Flat-priced packages still don't charge
  by headcount (a note now says so explicitly on both forms), but the
  count is recorded either way, since knowing how many people are
  traveling is useful regardless of how the package is priced.
- Made the exact same kind of editing mistake once more while
  reworking the admin booking form's conditional headcount fields
  into unconditional ones -- an orphaned endif this time (caught
  immediately by lint) and, once fixed, a SEPARATE stray extra
  closing div that lint couldn't catch (PHP doesn't parse HTML), only
  found by checking the file's div-open/div-close balance explicitly
  and tracing the actual mismatch rather than assuming the first fix
  was complete.

5.13.0 (Tour Package Bookings Management -- The Missing List)
- Added the dedicated "Tour Package Bookings" list, closing the gap
  confirmed across the last two passes: a package's room-stay
  component only ever showed in the ordinary Bookings list as an
  unlabeled category booking, with no link back to the package, and
  its car/activity components didn't appear in any admin list at
  all. New toggle on the Bookings section (Room-Wise / Tour Package),
  same pattern as the "+ New Booking" toggle added last pass -- one
  row per actual booking (every itinerary step rolled up, not shown
  per-component in the list itself), scoped to homestays the viewer
  can access via each package's OWNING homestay.
- New data class, HB_Tour_Package_Bookings_Data, groups
  hb_tour_package_bookings rows back up to one row per order item.
  Caught a subtle bug in my own first draft of this query before it
  shipped: I'd used MySQL's MAX() on the status column to get a
  "rolled-up status" for a multi-component booking, but MAX() on a
  string compares alphabetically, not by any meaningful priority --
  it could easily have hidden a component still needing finalization
  behind whatever status happened to sort higher. Replaced with an
  explicit needs_finalization flag instead, and pull the order's real
  status from wc_get_order() directly for display, the authoritative
  source every other list here already uses.
- Added a "View Itinerary" detail screen for one booking: every
  resolved component with its dates and status, built by reusing the
  same itinerary-resolution logic the invoice's day-wise page already
  uses (HB_Invoice_Template::get_tour_package_lines()) rather than a
  third implementation of the same thing.
- Built the actual missing piece from the last two passes: a
  finalization form for any finalize_later step still pending. An
  admin picks the real homestay category / car / activity (the same
  flat "Homestay -- Category" manual-entry dropdown the package
  builder already uses for cross-vendor stays) and a date, and it's
  checked for availability and blocked exactly the way a real
  checkout would block it -- new HB_Tour_Package_Availability::finalize_step(),
  which writes into whichever of hb_room_bookings / hb_car_bookings /
  hb_activity_bookings that component type actually uses. Verified
  the date-math (a 3-night homestay assignment producing the correct
  end date) and the activity-vs-span distinction (an activity
  correctly stores no end date, since it's a single day, not a
  range) directly before shipping. Verified every new database
  write's field/format-specifier count carefully, including catching
  a false alarm from my own verification script's regex (it had
  bled into a neighboring array and reported a mismatch that wasn't
  real) rather than accepting the tool's output uncritically.

5.12.0 (Per-Head Pricing + Admin Tour Package Booking)
- Added per-head pricing as a real alternative to flat pricing:
  packages can now be priced per adult / per child instead of one
  flat total. Schema: pricing_type, price_per_adult, price_per_child
  on hb_tour_packages, using the same safe fallback-resolution
  pattern learned from the earlier "Column 'status' cannot be null"
  bug for the new pricing_type field too. Wired through completely:
  portal create/edit forms (a toggle switches between flat and
  per-head fields), the frontend product page (Adults/Children
  selectors appear only for per-head packages), the live AJAX price
  check, and cart validation/pricing -- verified the actual
  calculation directly across mixed adults+children, adults-only,
  and flat-mode-ignores-per-head-fields cases before wiring it in
  anywhere.
- Admin/manager can now book an existing Tour Package on a guest's
  behalf from "+ New Booking" (a new Room-Wise / Tour Package
  toggle) -- walk-ins and phone bookings work the same way for
  packages as they already did for room-wise stays: package picker,
  start date, adults/children for per-head packages, itinerary
  choices for customer_picks packages, custom price override,
  advance payment, order status.
- Deliberately reused rather than duplicated: the new manual-booking
  path calls the EXACT SAME meta-saving and blocking-row insertion
  HB_Order already uses for real checkout (made two of its methods
  public specifically for this), rather than writing a second
  implementation of the room/car/activity blocking logic that could
  drift out of sync with the first over time. Availability is
  re-validated through the same HB_Tour_Package_Availability engine
  everything else in this feature already shares.
- Access model: booking an existing package for a guest is gated the
  same way room-wise manual booking already is (available to any
  portal role with "+ New Booking" access, checked against the
  package's owning homestay) -- deliberately NOT restricted to Group
  Admin the way managing/creating packages now is, since booking a
  walk-in is a front-desk operational task, not a management one.
- STILL NOT DONE: the dedicated Tour Package Bookings management
  list. Confirmed the real gap behind this request: a package's
  room-stay component currently shows in the ordinary Bookings list
  as an unlabeled category booking, with no indication it's part of
  a larger package, and its car/activity components don't appear in
  any admin list at all. Remains for the next pass.

5.11.0 (Group-Admin-Only Packages, Duplication, Invoice Print Fix)
- Confirmed plan change: Tour Package creation and management is now
  Group Admin only, not individual Homestay Admins -- moved the
  entire "Tour Packages" portal section (and every one of its
  handlers: package save/delete, itinerary steps, price sync, car/
  activity save/delete) from can_manage_settings() to
  can_manage_group() gating. Verified the targeted replacement
  landed in exactly the right 8 places and left an unrelated nearby
  comment about a different feature (photo uploads) untouched.
- Moved Cars & Activities management out of Homestay Settings
  entirely (no longer reachable there) into a new "Manage Cars &
  Activities" view inside Tour Packages, with a homestay selector
  since Group Admin now manages these across every homestay rather
  than just their own -- reuses the existing management UI
  underneath unchanged, just relocated and re-gated.
- Added package duplication: a "Duplicate" action clones a package's
  settings AND its full itinerary (every component, in order) into a
  new package, named "(Copy)" and forced to Inactive status so a
  duplicate is a starting point to review/adjust rather than
  something that goes live under the original's terms before anyone
  has looked at it. Verified directly: cloning preserves every
  component's slot/type/reference/pricing correctly.
- Fixed the invoice logo not appearing when saved/printed as a PDF,
  despite showing correctly on-screen -- a classic browser print
  behaviour: background colors and images are stripped from printed/
  PDF output by default unless explicitly forced, which silently
  dropped the dark background box behind the logo. Since the actual
  Celeste logo file is a white-on-transparent variant, losing that
  dark backing made it invisible against the page's white background
  in the PDF specifically, while looking fine on-screen where the
  background survives. Fixed with print-color-adjust: exact (both
  inline on the logo's own background box and as a global rule on
  both invoice page wrappers) to force it through.
- Made the same orphaned-signature mistake once more while wiring in
  the relocated Cars & Activities view -- caught immediately by lint
  as a parse error, fixed, and reverified the surrounding structure
  before moving on.

5.10.1 (Root Cause of the Package Creation Error, Found and Fixed)
- Root cause confirmed via the improved error message added in
  5.10.0: "Column 'status' cannot be null". Found a real logic bug in
  HB_Tour_Package::create() -- the ternary that resolves a missing
  'status' to a default re-referenced $data['status'] directly in its
  TRUE branch instead of the already-resolved fallback variable. That
  is only wrong when the caller never sets 'status' at all, which is
  exactly what happens when creating a brand-new package -- the
  Create form has no status field (only Edit does) -- so the create
  handler never populates it, the ternary's fallback of 'active' gets
  used for the validity CHECK, passes (since 'active' is valid), then
  the ternary's RESULT re-reads the still-undefined $data['status']
  and silently inserts NULL into a NOT NULL column.
- Swept for the same pattern and found it in two more places with the
  identical bug for the identical reason (their own Add forms have no
  status field either): HB_Car::create() and HB_Activity::create() --
  both would have failed the same way the first time anyone tried to
  add a car or activity resource, not yet reported but confirmed via
  code review once the first instance was found. Fixed all three the
  same way: resolve the fallback into its own variable once, then
  reuse THAT variable on both sides of the validity check, rather
  than re-reading the original (possibly still-undefined) data.
- Found one more structurally identical but NOT actually buggy
  instance (HB_Tour_Package_Component::create()'s component_type) --
  its own fallback value isn't itself a member of the valid-values
  list, so it always fell through to a hardcoded default rather than
  re-referencing undefined data. Cleaned it up to the same pattern
  anyway, purely so it doesn't look like the same bug to a future
  read and cost time re-investigating something that was never wrong.
- Verified the fix directly across all three cases the ternary needs
  to handle correctly: status entirely unset (now correctly resolves
  to 'active', not NULL), an invalid value submitted (correctly
  rejected back to 'active'), and a valid explicit value (correctly
  passed through unchanged).

5.10.0 (Package Creation Fix + Rich Package Invoices)
- Investigated "Could not create the package -- please try again"
  reported after building the portal itinerary UI. Found the create
  path already had reasonable error-surfacing and a table self-heal
  from earlier work; strengthened both further: the self-heal now
  unconditionally re-runs table creation (safe/idempotent) instead of
  only when the table is fully missing, which also catches a table
  that EXISTS but is missing a column from an interrupted earlier
  update -- a bare existence check would never catch that. Applied
  the same strengthening to itinerary-step creation, the very next
  action after creating a package. Could not fully confirm the root
  cause without live database access; if this recurs, the error
  message now surfaces the actual underlying database error instead
  of the generic text, which will pin down the real cause.
- Package invoices now show a "what's included" summary line under
  the package (car/activity names, not just the homestay stay), and
  a full day-wise itinerary as a second printed page -- built from
  the ACTUAL resolved hb_tour_package_bookings rows a purchase
  produced, not the package's raw template, so a customer_picks
  package correctly shows the alternative that was actually chosen,
  and any still-unassigned finalize_later step shows as "to be
  confirmed" rather than guessing. Verified the day-by-day date
  mapping directly, including a genuinely tricky case: a gap day
  with nothing scheduled correctly shows as free rather than being
  silently dropped from the itinerary.
- Strengthened the invoice's logo lookup with a fallback chain
  (Customizer Custom Logo, then Site Icon, then a last-resort
  fallback) and wired his known Celeste logo as that fallback --
  suspected his site's actual header logo is set through the theme's
  page-builder template rather than WordPress's native Customizer
  setting, which the original lookup only checked. Also wrapped the
  logo in a dark background swatch so it stays visible regardless of
  the logo's own color, since the known Celeste logo is a white-on-
  transparent variant that would be invisible directly on the
  invoice's white background.
- Found and fixed a pre-existing instance of the orphaned-docblock
  mistake pattern from earlier passes: render()'s own docblock had
  ended up misplaced above a different method entirely. Restored it
  to the right place while making these other changes, not left as
  found.

5.9.0 (Tour Packages -- Bookable End to End)
- Confirmed with him: customers only ever BOOK an already-built
  package, never create one -- package creation stays admin-only, as
  it always was; this pass is entirely about the customer-facing
  purchase path.
- Tour Packages are now genuinely bookable for the first time:
  frontend product page (a start-date picker, the itinerary shown
  appropriately per mode -- read-only for fixed, radio-button
  alternatives for customer_picks, a general description for
  finalize_later), a live AJAX price/availability check (a new,
  separate AJAX action from the existing one, since a package takes
  a single start date rather than a check-in/check-out pair and
  doesn't fit that dispatcher's shape), full cart integration
  (validates the whole itinerary before allowing add-to-cart, stores
  the customer's per-slot choices, re-validates and re-prices at
  checkout rather than trusting what was cached at add-to-cart time),
  and -- the actual point of the exercise -- automatic blocking.
- Automatic blocking reuses the EXISTING tables and availability
  engine every other booking type in this plugin already uses,
  rather than inventing a parallel system: a homestay-stay component
  writes into hb_room_bookings via the category's own product_id,
  exactly like a direct category booking would, so it's invisible to
  nothing else that already reads availability from that table. Car
  and activity components write into their own new per-date ledgers
  the same way. A finalize_later component with no homestay/car/
  activity assigned yet blocks nothing (nothing to block) but is
  still recorded, ready for an admin to assign afterward. Verified
  the full pipeline directly: a 3-component itinerary (a 2-night
  homestay stay starting day 1, a car and an activity both on day 3)
  produced exactly the right dates in exactly the right tables.
- HB_Tour_Package_Availability is the one place that resolves an
  itinerary and checks/prices it -- used identically by the frontend
  AJAX preview, cart validation, and the final checkout price, so
  all three always agree with each other rather than three separate
  implementations that could drift.
- Caught FIVE instances of the same mistake this pass (three in the
  prior session, two more here) -- a str_replace edit consuming an
  existing function's signature or docblock and leaving it orphaned.
  Two were caught immediately by lint as parse errors; the docblock
  ones don't break PHP syntax, so those were only caught by directly
  checking the surrounding structure survived, not by lint alone.
  Naming this pattern explicitly since it recurred enough to be worth
  watching for specifically on future edits to this codebase, not
  just trusting lint as the sole safety net.
- Verified every new $wpdb->insert() call's field count against its
  format-specifier count programmatically rather than by hand, after
  an earlier session's manual count came out wrong on a similar
  change -- all four new inserts matched exactly.
- STILL NOT DONE: package invoices/bookings-list display (a package
  booking should show its full itinerary, not just look like a
  single generic line item), and the finalize_later admin screen for
  actually assigning a homestay/date to a pending-finalization
  component after purchase. Both remain for upcoming passes.

5.8.0 (Tour Packages -- Portal Builder)
- Followed up a design question with a concrete answer from him: the
  customer never picks or browses other homestays' rooms directly --
  only an admin does, when building the package. Built the portal
  UI accordingly: a new "Tour Packages" section (admin-level roles,
  only visible once a homestay has tour_packages_enabled) to create
  and list packages, an itinerary builder for adding/removing steps,
  and a "Set Base Price to Match Components" shortcut.
- The "Add Itinerary Step" form's homestay-stay option is a single
  flat dropdown of every "Homestay -- Category" pair the package may
  legally use: the owning homestay's own categories, plus every
  OTHER homestay's categories where that homestay currently has
  tour_packages_enabled -- exactly the "manual entry line" he asked
  for, not a live cross-vendor search or availability browser.
  Car and activity components stay scoped to the owning homestay's
  own resources, per his note that the cross-vendor case was
  specifically about homestay stays.
- Added a minimal Cars & Activities manager to Homestay Settings
  (only shown once Tour Packages is enabled) -- this didn't exist
  yet, and the itinerary builder was about to reference it as if it
  did; built it now rather than ship a dead link to a screen that
  isn't there.
- Extended the IDOR-prevention pattern from the earlier security
  pass to every new handler here: a Tour Package, its components, and
  every car/activity resource all verify they genuinely belong to
  the homestay_id being claimed, not just that the user can access
  SOME homestay. Verified directly that a forged cross-homestay
  request is blocked while a legitimate same-homestay action isn't.
- Caught two real mistakes while building this and fixed both before
  shipping, not after: a str_replace edit that silently consumed the
  render_section_settings() function's own opening line, orphaning
  its body outside any function (lint caught it immediately -- a
  parse error, not a silent bug); and a later edit that dropped two
  lines of an existing comment it was inserting next to, leaving an
  orphaned sentence fragment (caught by directly checking the
  comment was still intact, not just by lint, since a missing
  comment doesn't break PHP syntax).
- STILL NOT USABLE END TO END: a package can now be fully built with
  a real itinerary, but there is still no frontend for a customer to
  actually book one, and no automatic room/car/activity blocking
  wired into cart/checkout yet -- both remain for upcoming passes.

5.7.0 (Tour Packages -- Foundation, In Progress)
- First installment of Tour Packages: a package that can bundle
  homestay stays (at any homestay on the platform, not just the
  creator's own), car hire, and activities into one purchasable
  product. Scoped with him across three itinerary modes: fixed (the
  creator sets the exact itinerary, customer only picks a start
  date), customer_picks (creator offers alternatives per slot,
  customer chooses), and finalize_later (customer books now with
  general dates, exact assignment happens afterward). Cars and
  activities are real bookable resources with their own per-date
  availability, not just static add-on line items, per his answer.
- NOT YET USABLE END TO END: this pass is the data foundation only
  -- seven new tables, four new CRUD classes (HB_Car, HB_Activity,
  HB_Tour_Package, HB_Tour_Package_Component), and the master admin's
  per-homestay enable/disable toggle (off by default for every
  homestay -- deliberately NOT tied to general Premium status, since
  a package can reach into another homestay's room pool, so turning
  it on is meant to be a deliberate per-homestay decision). There is
  no portal UI yet for actually BUILDING a package, no frontend
  booking flow, and no automatic room/car/activity blocking wired in
  -- those are still to come across the next several passes. The
  toggle itself is fully functional today: enabling/disabling a
  homestay immediately re-syncs every package it owns, same pattern
  the Premium override already uses.
- Judgment call made and flagged to him rather than silently decided:
  the master-admin toggle controls BOTH directions with one switch
  (can create packages, AND can be pulled into another homestay's
  package) -- otherwise a homestay explicitly locked out of packages
  could still have its rooms sold through someone else's package.
- Verified the newer, more novel logic directly rather than trusting
  it on read: slot-grouping correctly separates real alternatives
  (customer_picks) from single-option steps, the default-price sum
  correctly totals only the first alternative per slot, and the
  component type-resolution dispatch (homestay category / car /
  activity / finalize_later placeholder) handles all four cases.

5.6.0 (Professional Tally-Style Invoice)
- Rebuilt the invoice as a professional, Tally-style layout: a
  bordered letterhead with the homestay's own name, address, phone,
  email and GSTIN (all new settings fields from v5.5.0's
  foundation), a Bill To block with the guest's details, an itemized
  table (Particulars/Check-in/Check-out/Nights/Room No./Amount),
  Grand Total, Advance Paid / Balance Due when an advance was
  recorded, an "Amount in Words" line using proper Indian numbering
  (lakh/crore, not the Western thousand/million grouping), the
  homestay's own Terms & Conditions, and a signature block. Verified
  the Indian-numbering word conversion across eleven cases (simple
  amounts, exact lakh/crore boundaries, paise, zero) and confirmed
  end-to-end rendering against a realistic mocked order including
  the advance/balance math.
- This plugin had THREE separate, slowly-diverging invoice
  implementations (the customer's My Account page, the portal's
  manager-facing page, and the emailed copy) -- consolidated all of
  them into one shared class, HB_Invoice_Template, so there is now
  exactly one definition of what an invoice actually contains. Each
  caller keeps only its own page chrome around it (print/email
  buttons on the two on-screen views, a greeting line on the email).
  The table-based, inline-styled markup this required for Tally-
  style formatting turned out to be a good fit for the email
  version too, which needed exactly that kind of markup anyway for
  email-client compatibility -- one welcome side effect of doing
  this as a genuine consolidation rather than three parallel edits.
- The email version was still using the old narrow "is this a
  room-wise homestay item" check that predates package/category
  support -- fixed as part of the consolidation, so emailed invoices
  now correctly include package and room-category bookings too.
- Caught and fixed a mistake made mid-edit: removing the customer-
  facing invoice's old per-class resolver method left its opening
  doc-comment marker behind, orphaned above the next method and
  swallowing it into an unterminated comment. Found via lint
  immediately, not left in.

5.5.0 (Admin Backdating, Advance Payment, Booking Edit/Delete)
- Admin/manager bookings (via the portal's "+ New Booking") can now be
  backdated -- the past-date restriction only ever applied there, so
  it was safe to remove without touching customer checkout at all.
- Added an admin-entered Advance Payment field on manual bookings --
  purely a record-keeping figure (e.g. cash taken at check-in), never
  a real payment-gateway charge, capped so it can never exceed the
  order total, and never exposed to customer checkout anywhere.
- Added homestay business details (address, phone, email, GSTIN) to
  the data model and portal settings, as the foundation for the
  Tally-style invoice (still to come) -- not yet used anywhere else.
- Added HB_Booking_Edit: lets an admin/manager correct dates, guest
  counts, status, and advance payment on an EXISTING booking, or
  delete it outright (removes just the item if the order has others,
  otherwise trashes the whole order -- recoverable, not a permanent
  wipe). Deliberately does NOT auto-recalculate price when dates/
  guests change -- four different pricing paths exist in this plugin
  (room-wise, category, package, package-in-category), and getting an
  automatic recalculation subtly wrong on a feature whose whole point
  is correcting mistakes would be worse than not attempting it, so
  the admin sees the current total and can type a new one directly.
  Availability IS re-validated automatically on a date change,
  correctly excluding the booking's own existing rows from the
  conflict count (verified this exclusion logic directly: a booking
  keeping its room count on the same date passes, expanding into an
  already-full pool on that date is correctly rejected).
- New portal "Edit" link on the Bookings list (Admin-level roles
  only), leading to a form for all of the above plus a Delete action
  with confirmation.
- Made a genuine mistake while building the Bookings list's new
  Actions column and caught it before it shipped: an overcomplicated
  first version of the per-row access check would have incorrectly
  hidden the Edit link for package/category bookings. Realized the
  row list is already scoped to homestays the viewer can access, so
  a second per-row check wasn't needed at all -- simplified rather
  than patched.

5.4.0
- Added automatic WordPress-native update delivery via the sitss.in
  License Manager Server (the same shared mechanism used by his other
  plugins -- School Management Suite, Laundry Management Suite). This
  plugin now shows a normal "Update available" row on the Plugins
  page with a "View version details" popup and one-click update,
  exactly like a WordPress.org-hosted plugin -- no more manually
  uploading a new zip to update.
- New file: includes/class-lms-plugin-updater.php (LMS_Plugin_Updater),
  reconstructed and verified from the source conversation where it
  was originally built and functionally tested against a mocked
  server response, rather than rewritten from scratch here -- pulled
  the exact same class, then independently re-verified it against
  four scenarios before wiring it in: an available update is detected
  and populates the right package/version, a matching current version
  correctly shows no update, the plugin-information popup returns the
  right details for this plugin's slug, and a request for a different
  plugin's slug is correctly ignored.
- Registered under slug homestay-booking-premium, matching the
  product already listed on sitss.in. Update checks are NOT
  license-gated -- every install (Free or Premium) gets the same
  update, since Premium features are gated at runtime by
  HB_License, not by which build a site is running.
- current_version is passed as the HB_BOOKING_VERSION constant
  directly (not a hardcoded string), so it can never drift out of
  sync with the plugin header version -- every future release only
  needs the one version bump already done at the top of every
  release in this changelog, nothing updater-specific to remember.
- Hooked in admin-only (is_admin() guard), since every hook this
  class uses (update transient, plugins_api popup, upgrader
  complete, plugin row meta, admin_init) is wp-admin specific --
  no reason to load it on frontend requests.

5.3.0 (Security Hardening + WC Compatibility Verification)
- Verified WooCommerce compatibility: "WC tested up to: 10.9" was
  already accurate (10.9.4 is the latest stable 10.x release).
  Deliberately NOT bumped to WooCommerce 11.0 (released Aug 4, 2026,
  per his explicit "version 10" request) -- also worth noting, a
  WordPress.org reviewer reported serious site-wide slowdowns within
  a day of updating to 11.0, so staying on 10.x looks like the right
  call independent of the request. Separately flagged for him:
  WordPress core 7.0.3 (Aug 6, 2026) is a security release fixing a
  critical pre-auth XSS with potential PHP code execution, among
  others -- a site-level update, not something this plugin controls.
- FIXED (significant, IDOR/broken access control): the portal's
  handlers for editing/deleting Room Categories, Rooms, Packages, and
  package-category pricing overrides all checked "does this user have
  access to THIS homestay" using a client-submitted homestay_id, but
  then acted on a SEPARATELY submitted resource ID (category_id,
  room_id, package_id) without ever confirming that ID actually
  belonged to the claimed homestay. A Homestay Manager could submit a
  request using their own homestay's ID (passing the access check)
  while pointing the actual edit at a different vendor's category,
  room, or package -- and it would go through. Added shared ownership-
  verification helpers (category_belongs_to_homestay(),
  room_belongs_to_homestay(), package_belongs_to_homestay(),
  product_belongs_to_homestay()) and applied them to every affected
  handler: photo upload/removal, save_package, save_room_category,
  delete_room_category, save_room, delete_room, copy_room_occupancy,
  save_package_category_price, and block_dates (which had a related
  gap on its category_id field specifically). Verified directly: a
  forged cross-homestay request is now blocked, while a legitimate
  same-homestay edit still goes through correctly.
- Checked wp-admin's equivalent package handlers for the same
  pattern -- these gate on manage_options (full Administrator trust,
  not per-homestay scoping), so the same cross-vendor tampering
  concern doesn't apply there; a user who reaches those handlers
  already has unrestricted access to the whole site by design. No
  change needed on that side.
- Re-ran the full security checklist from the WordPress.org
  submission pass (v4.7.0) against everything added since: ABSPATH
  guards on all 29 files, nonce checks on every new portal handler,
  capability checks on every new portal handler, and a fresh scan for
  unprepared SQL queries across every file touched this project --
  three flagged, all confirmed false positives (internal table-name
  constants only, no user input reaches them).
- Caught and immediately corrected a mistake made while applying one
  of these fixes: two fields (name, min_adults) were briefly dropped
  from save_package's data array during editing -- restored before
  moving on, flagging it here for the record rather than omitting it.

5.2.2
- FIXED: printing an invoice for a room-wise Room Category booking
  (e.g. a "Mountain View" room booked directly, not through a
  package) showed "Invoice not found, or you do not have permission
  to view it" in the portal, even for an admin who genuinely had
  access. Root cause: the portal's scope-check helper
  (product_is_in_allowed_scope(), which also gates check-in/out,
  order-status changes, and customer request approval) had been
  correctly updated to resolve a homestay from a PACKAGE product back
  in v4.7.2, but was never revisited when Room Categories were built
  afterward in v5.0.0 -- it simply didn't know category products
  existed, so it always failed for one. This should also correct
  check-in/out and order-status changes for direct category bookings
  as a side effect, same as the earlier package fix did.
- Swept the codebase for the same "package branch added, category
  branch forgotten" pattern given this was clearly a real recurring
  gap. Found one more, minor and non-functional: the admin notice
  shown on a package's WooCommerce product edit screen (pointing back
  to where pricing should actually be changed) didn't have a category
  equivalent either -- added it. Confirmed every other place that
  resolves a package back to its homestay for permission/display
  purposes already had matching category coverage from when it was
  originally built, so this was the last of it.

5.2.1
- FIXED (significant): a homestay whose bookings go entirely through
  Room Categories (no bookings on the homestay's own base product at
  all) had NONE of its orders showing in the portal's Orders page, or
  in the Bookings list when filtered to that homestay specifically.
  Root cause: HB_Orders_Data and HB_Bookings_Data both resolved "this
  homestay's bookings" by querying hb_room_bookings for JUST the
  homestay's own product_id -- completely missing that each of its
  room categories is its own separate product/pool with its own
  product_id (by design, since v5.0.0). WooCommerce orders themselves
  were entirely unaffected (they don't know or care about this
  plugin's internal linkage) -- this was purely about our own
  reporting views going blind for a category-only homestay.
  Confirmed via screenshots: Celeste Tamang Homestay (room-wise)
  showed all its orders correctly; a second homestay using only
  categories showed none, even though the same orders were plainly
  visible in WooCommerce's own Orders screen.
- Centralized the fix into one shared method,
  HB_Homestay::get_all_product_ids() (homestay's own product + every
  category's + every package's), used by both HB_Orders_Data and
  HB_Bookings_Data instead of each reimplementing the same lookup --
  verified directly against the exact order IDs from the screenshots
  (1719, 1720, 1723, 1724) to confirm they're now found correctly.
- Also swept the rest of the codebase for the same pattern one more
  time. Everywhere else that uses $homestay->product_id alone is
  correctly scoped to ONE SPECIFIC pool being booked/synced (an
  individual homestay, category, or package's own product) rather
  than trying to represent "this homestay and everything under it" --
  those are intentionally single-pool, not the same bug.
- NOTED, not yet fixed: HB_Manual_Booking (the portal's "+ New
  Booking" feature) only ever books a homestay's own base product --
  a manager currently can't create a manual/walk-in booking for a
  specific room category through it. Flagging this now since it's
  the same underlying gap area, but it's a feature limitation rather
  than a bug (manual bookings for category-only homestays currently
  need to go through the actual product page instead).

5.2.0
- Fixed: category (and package) bookings were being silently excluded
  from the "My Bookings" tab / order page, same root cause as the
  earlier invoice bug -- only room-wise bookings were ever recognized
  there. Fixed, and the display label now shows "Package (Room Type)"
  together for a category-selected package booking.
- Calendar closing is now category- and room-aware. This needed a
  real schema change: the blackout table was entirely homestay-level
  with no concept of categories at all. Added category_id/room_id
  columns, with a safe migration that explicitly drops the old unique
  key on existing installs (dbDelta can't reliably rewrite an
  existing index itself -- getting this wrong would have silently
  blocked legitimate category-level closures on any date that already
  had a homestay-level block). Rewrote HB_Blackout to accept either a
  plain ID (unchanged legacy behaviour) or a homestay/category object,
  auto-detecting which is which -- verified this resolution directly.
  Both wp-admin and portal "Close Dates" forms now have a Room
  Category dropdown and a specific-room dropdown; closing one specific
  room automatically closes exactly 1 room from that room's category
  and records which physical room was taken out of service.
- Fixed: "Return to Shop" on the empty cart now reads "Return to
  Select" -- this is a booking site, not a general product catalog.
- Fixed: category products showed WooCommerce's default "Add to
  Cart" instead of "Book Now" -- the button-text filter simply never
  checked for category products.
- Added a per-homestay Terms & Conditions field, synced into that
  homestay's product description automatically whenever it's edited.
  Previously the product description was only ever set ONCE, at
  product creation -- editing it afterward (T&C or otherwise) never
  actually updated the live product. Now both create and update paths
  share one description builder, so this stays in sync going forward.
- Split the increasingly long Homestay Settings page into collapsible
  sections (Basic Info open by default, Pricing & Age Bands,
  Occupancy Rules, Cancellation/Reschedule/Invoice, Terms &
  Conditions each collapsed) using native HTML <details>/<summary> --
  zero JavaScript, works everywhere, addresses "too long to manage"
  without touching any of the underlying save logic. Verified the
  section tags are correctly balanced (5 opens, 5 closes) before
  shipping.

5.1.0 (Category-Aware Package Pricing)
- Replaced the "links to a separate category product page" approach
  from v5.0.0 with what he actually asked for: an inline Room Type
  dropdown right inside a guest-wise package's own booking form.
  Selecting a category re-runs the live price/availability check
  scoped to that category directly, with the price updating in place.
- Package pricing can now be set PER ROOM CATEGORY from the
  management console -- base price, offer price, extra-adult fee,
  and extra-child fee are all independently overridable per category
  (confirmed: extra adult/child pricing does vary per category, not
  just the base rate). Any field left blank inherits the package's
  own value -- a set category value is the FINAL price for that
  field, per his explicit "final on top of base package" framing.
  Verified the per-field resolution directly (full override, partial
  override with the rest correctly inherited, and no-override cases
  all resolved correctly).
- A package booked with a category selected now correctly draws from
  THAT category's own room pool (not the shared homestay pool), same
  automatic non-conflicting room-assignment behaviour categories
  already have, and the assigned room number honours the existing
  invoice toggle -- verified directly that a category-selected
  package booking only reduces that category's own availability and
  leaves the homestay's shared pool untouched.
- Invoices (both customer-facing and portal) now show the package
  name together with the selected room type, e.g. "Solo Traveler
  (Mountain View)", when a category was chosen.
- Caught and fixed a real bug mid-build: an earlier edit accidentally
  left orphaned PHP code sitting inside an unclosed doc comment --
  php -l stayed clean throughout (comments can contain anything), but
  it silently ate the docblock for a nearby method and left dead code
  in the file. Found it via direct code review, not a syntax error,
  and cleaned it up before this shipped.
- No booking made with NO category selected is affected -- falls back
  to the exact same shared-homestay-pool behaviour as before.

5.0.0 (Room Categories)
- Added ROOM CATEGORIES -- a homestay can now optionally define
  multiple room types (e.g. Mountain View, Valley View), each with
  its own room count, its own pricing (base + offer + extra-adult +
  extra-child, same mechanism as packages), its own occupancy rule,
  and its own real WooCommerce product, same product-per-unit pattern
  packages already use. Entirely opt-in and fully backward-compatible
  -- a homestay with zero categories keeps working exactly as before.
- Individual, numbered ROOMS within a category -- unique room number
  per homestay, add one at a time, with a "Copy Category Occupancy to
  All Rooms" bulk action and the ability to override a specific
  room's occupancy afterward (a room with no override inherits the
  category's rule field-by-field).
- Both a homestay's own base pool AND its categories can coexist --
  each independently configurable/visible, exactly as asked.
- Guest-wise packages now show, before the guest commits to booking,
  which of the homestay's room categories can actually fit that
  package's occupancy, each with that category's own live pricing --
  so a "Family" package at a homestay with several room types shows
  exactly which rooms qualify and what they cost.
- FIXED (found while building this): a fixed-occupancy package (no
  adjustable guest-count field, e.g. Solo Traveler/Couple) never ran
  its live availability check unless a date field happened to fire a
  change event -- a guest could reach Book Now with no idea a date
  was unavailable until the server rejected it. The check now also
  runs once automatically on page load.
- FIXED (found while building this): package (and now category)
  bookings were being silently excluded from invoices entirely, both
  the customer-facing and portal invoice views -- only room-wise
  bookings ever showed up in the invoice table. Both fixed.
- Optional room-number-on-invoice: a per-homestay toggle: when a
  category has individual rooms configured, checkout now
  automatically assigns a specific, non-conflicting room per booking
  for the whole stay (verified directly: two rooms needed in one
  booking get two distinct rooms, a later booking gets whatever's
  left, and a category with no individually-tracked rooms left simply
  falls back to category-level tracking rather than failing). With
  the toggle on, the assigned room number(s) now show on invoices.
- Architecture note: the existing multi-room splitting, availability,
  and pricing engine turned out to already be fully generic (keyed
  only on product_id/total_rooms/max_adults/etc, never assuming
  specifically a homestay) -- categories reuse it unchanged, which
  kept this from being a much larger rewrite than it already was.
- KNOWN GAP, consistent with the same gap packages have had since
  v4.5.0: Extend Booking and the customer self-service request
  workflow don't yet recognize category bookings either (same root
  cause -- both resolve a homestay via the order line item's own
  product, which for a category item is the category's product).
  Both fail safely; the portal now hides the Extend action for
  category bookings, same as it already does for packages.

4.9.0
- Added frontend photo management for both homestay and package
  WooCommerce products -- 1 featured photo + up to 5 gallery photos,
  JPEG/PNG/WebP only, 1MB max per photo, all enforced server-side
  (never just trusting the browser). Uses WooCommerce's own native
  featured image + product gallery fields, so photos show up
  everywhere WooCommerce already displays them automatically.
- New shared class HB_Product_Photos handles the actual upload
  (through wp_handle_upload() into the real Media Library, not just a
  raw file drop -- thumbnails generate correctly) and validation
  (real MIME-type sniffing via wp_check_filetype_and_ext(), never
  trusting the filename extension or Content-Type alone). Verified
  the size/count validation logic directly (999KB passes, exactly
  1MB passes, 1.5MB fails; a 5-photo-full gallery correctly blocks a
  6th add).
- Replacing the featured photo deletes the old attachment outright
  (not just detaches it) to actually free up Media Library space, per
  his explicit request -- same for removing a gallery photo.
- Permissions reuse the existing HB_User_Access::can_access_homestay()
  check -- Group Admin can edit photos for any homestay/package,
  Homestay Manager/Admin only for ones they're assigned to. Available
  on both the Homestay Settings page (for the homestay's own product)
  and the package edit form (for that package's product).

4.8.0
- Added brute-force / bot protection to the portal login, which had
  none before (just a nonce + wp_signon(), no rate limiting of any
  kind) -- worth doing now that /hb-dashboard/ becomes a predictable,
  publicly-known URL pattern once this is a WordPress.org plugin.
  - Honeypot field: an invisible input real visitors never see or
    fill; a bot that blindly fills every field trips it and gets
    silently rejected before wp_signon() or the rate limiter even run.
  - Rate limiting: failed attempts are tracked by BOTH username and IP
    (WP transients, no new DB table) -- 5 failures within 15 minutes
    locks that username AND that IP out for 15 minutes. Tracking only
    one axis would leave a gap (username-only lets an attacker rotate
    IPs forever; IP-only lets a botnet spread attempts thin across
    many machines) -- verified both trigger correctly via direct
    simulation. The IP counter deliberately does NOT clear on a
    successful login (a valid login on one account doesn't mean every
    other attempt from that IP was legitimate).
  - Admin email alert the moment a lockout actually triggers (not on
    every single failure, which would flood the inbox during a real
    attack), itself rate-limited to once per hour per username+IP
    combination.
- NOT added in this pass: CAPTCHA and two-factor authentication --
  both need either a third-party service/API key or substantially
  more code, so left as a manual future addition rather than picking
  a provider unasked. The above three layers (honeypot + rate limit +
  alerting) cover the most common automated attack pattern without
  any new dependency or configuration required.

4.7.3
- Fixed: the wp-admin License page (and the top-level "Homestay
  Booking" menu, so License can actually be reached from it) were
  gated to manage_options -- Administrator only. The hb_group_admin
  role this plugin creates for portal Group Admins never had
  manage_options at all, so a genuine Group Admin account had no way
  to reach License from wp-admin, matching how the portal already
  correctly allows Group Admin there. Changed the menu registrations
  to the permissive 'read' capability and moved the real access check
  into each page's own render method via HB_User_Access::can_manage_group()
  (manage_woocommerce OR the hb_manage_group capability) -- the same
  rule the portal already uses, so License access is now consistent
  wherever it's managed from. Deliberately left every OTHER wp-admin
  page (Dashboard, Homestays, Bookings, Calendar, Portal Branding)
  untouched, since only License was reported as needing this.
- He also reported the License menu link showing a malformed URL
  (/wp-admin/hb-license instead of /wp-admin/admin.php?page=hb-license)
  and being denied even after manually correcting the URL. Searched
  the entire codebase for anywhere that could build a URL like that --
  found nothing; every reference to the License page correctly uses
  admin_url('admin.php?page=hb-license'). This, combined with the
  fact no other page shows the same symptom, points at the
  previously-suspected Admin and Site Enhancements (ASE) plugin
  conflict from v4.1.1 still being unresolved specifically for this
  page -- not something fixable from within this plugin's own code.
  Recommended he check ASE's menu-restriction whitelist for the
  License page specifically, or test with ASE temporarily disabled.

4.7.2
- FIXED: portal invoice viewing showed "Invoice not found, or you do
  not have permission to view it" for any order made up entirely of
  guest-wise (package) bookings, even though the invoice existed and
  the order genuinely belonged to a homestay the user could access.
  Room-wise bookings were unaffected. Root cause: the portal's shared
  scope-check helper (product_is_in_allowed_scope(), used to gate
  invoice viewing, check-in/out, order-status changes, and customer
  request approval/rejection) only ever resolved a homestay directly
  from the order line item's own product -- for a package line item
  that's the package's product, not the homestay's, so it always
  returned false. Fixed by resolving package products back to their
  parent homestay too, same as every other place in the codebase that
  already does this. Since this ONE helper gates several portal
  actions, this fix should also correct check-in/out and order-status
  changes for package bookings, not just invoices -- verified the
  resolution logic directly (package product correctly resolves to
  its homestay; unrelated products still correctly denied).
- NOT covered by this fix: the underlying extend-booking and
  self-service-request logic (HB_Booking_Extend, HB_Booking_Requests)
  still independently look up a homestay via the line item's own
  product when actually PERFORMING an extension or processing a
  request -- so while the portal will now correctly let an admin
  attempt these actions on a package booking, the action itself may
  still fail with its own "not a bookable homestay product" error.
  This remains the same known gap documented since v4.5.0, just
  narrower than before.

4.7.1
- Found the cause of a live-site mismatch he reported: a package's
  product page showed one price (the WooCommerce product's own
  regular_price) while the booking calculation charged a different,
  correct one (the package's configured Base Price). Root cause: the
  plugin only re-syncs a package's WooCommerce product price when its
  settings are actually SAVED (create/update) -- if the product's
  price was ever changed directly on the WooCommerce product edit
  screen instead, that edit silently persists until the next time the
  package settings are re-saved, which re-syncs and overwrites it.
  Immediate fix for his live site: re-save the package from Manage
  Packages (even with no changes) to force the re-sync. Also added a
  preventive fix: a clear notice now shows directly on the product
  edit screen for any homestay or package product, pointing back to
  the correct settings page and explaining that edits made there will
  be overwritten on the next save -- so this can't recur unnoticed.

4.7.0 (WordPress.org submission readiness pass)
- Added a proper Dashboard page as the true top-level landing page for
  the "Homestay Booking" wp-admin menu (homestay counts, today's
  arrivals, currently in-house, license status, quick links). The
  Homestays list moved from the shared top-level slug to its own
  dedicated submenu (hb-homestays-list) so the Dashboard and the list
  are unambiguous, separate pages -- fixed every internal link across
  wp-admin (about a dozen) that pointed at the old combined URL.
- Fixed the Premium purchase link: get_purchase_url() fell back to a
  dead "#" link whenever the URL wasn't manually configured in
  settings. Now defaults to https://sitss.in/product/homestay-room-booking-premium/
  -- also set as the plugin's Plugin URI header (the "Visit plugin
  site" link WordPress shows in the Plugins list) and corrected the
  placeholder text in both settings forms.
- Added WordPress.org-required plugin headers: Requires at least,
  Tested up to, Requires PHP, License, License URI, Author URI --
  verified against WordPress 7.0.2 and WooCommerce 10.9.4, the latest
  stable releases as of this update.
- Declared HPOS (High-Performance Order Storage) compatibility via
  before_woocommerce_init -- confirmed the plugin only ever reads/
  writes order data through WC_Order/WC_Order_Item's own methods,
  never raw postmeta, so this was a true positive declaration, not
  just suppressing a warning.
- Common-error scan: confirmed all files have direct-access (ABSPATH)
  guards, no debug leftovers (var_dump/print_r), no dangerous
  functions (eval/extract), and reviewed every wpdb query flagged as
  "not using prepare()" -- all three were false positives (either a
  hardcoded internal table name with no user input, or a conditional
  prepare() call the static scan couldn't see). Nonce/capability
  coverage spot-checked across all admin_post and AJAX handlers.
- Added a proper WordPress.org-format readme.txt (required exact
  filename/format for the plugin directory listing) -- header block,
  Description, Installation, FAQ, Changelog, Upgrade Notice. Renamed
  the previous detailed internal file to DEVELOPER-NOTES.txt to avoid
  a filename collision with the new lowercase readme.txt on
  case-insensitive filesystems.
- KNOWN GAPS, not addressed in this pass: readme.txt's Contributors
  field is a placeholder (needs a real WordPress.org username) and
  its Screenshots section references images that don't exist yet;
  user-facing strings across the plugin are not wrapped for
  translation (__()/_e()), which is a large separate undertaking.

4.6.0
- Guest-wise packages now support an optional Offer Price: set lower
  than the base price, guests see both (base struck through, offer
  price shown) and are charged the offer price for the base rate --
  extra adult/child fees are unaffected. Leave at 0 for no offer.
  Previously there was no way to run a promotional rate on a package
  at all; the underlying WooCommerce product's price could be edited
  directly but pricing was always recalculated from the package's own
  base price at checkout, silently ignoring any change made that way.
- Packages now support a Min Adults setting alongside Max Adults --
  when they're equal, the booking form shows a fixed guest count
  instead of a dropdown (e.g. a "Couple" package: min 2, max 2, no
  children or infants allowed at all).
- Starter packages updated: Solo Traveler no longer includes an
  infant option (1 adult only), and the third starter is now "Couple"
  (fixed at 2 adults, no children/infants) instead of "Group". This
  only affects NEWLY created starter packages -- existing packages on
  an already-running site are unchanged and should be edited directly
  if the same adjustments are wanted there.
- Add to Cart button now reads "Book Now" on homestay and package
  product pages (and in catalog/archive listings), matching typical
  hotel-booking site conventions. Every other product on the site
  keeps WooCommerce's normal "Add to cart" text.
- Group Admin (and unscoped users) can now delete a homestay directly
  from the portal's Homestay Settings page, not just from WP Admin --
  same underlying behaviour either way: the WooCommerce product is
  set to Draft rather than permanently deleted, so past orders and
  booking history are unaffected.

4.5.1
- FIXED: a new homestay always created as Room-wise, even when Guest-
  wise or Both was intended -- HB_Homestay::create() built its INSERT
  statement without a booking_mode column at all, silently relying on
  the database column's own default. Since a new homestay's Guest-wise
  packages are only auto-seeded once booking_mode is actually saved as
  guest_wise/both, this meant a brand-new homestay could never end up
  with any package products no matter what was intended for it.
- Added a Booking Mode selector directly to WP Admin's "Add New
  Homestay" form (Premium-gated), so it can be set correctly at
  creation time -- choosing Guest-wise or Both there now seeds the 3
  starter packages immediately, same as switching an existing
  homestay's mode after the fact.
- FIXED: the portal's "Add New Homestay" page called the Booking Mode
  & Packages block with no homestay to attach it to (that block
  belongs on the Homestay Settings page, reached right after creating
  a new homestay) -- this would fatally error the page for anyone with
  an available homestay slot. The call is now correctly placed on
  Homestay Settings only, where the homestay it refers to actually
  exists.

4.5.0
- Added GUEST-WISE BOOKING (Premium): a second, selectable booking mode
  per homestay, alongside the existing Room-wise mode. Group Admin
  chooses per homestay -- Room-wise only (default), Guest-wise only, or
  Both -- from WP Admin or the portal's Homestay Settings.
- Guest-wise lets a homestay define its own packages (e.g. Solo
  Traveler, Family, Group -- 3 starters auto-created the first time
  Guest-wise is turned on), each a real separate WooCommerce product
  with its own occupancy caps and its own independent pricing profile
  (base + extra-adult + extra-child fee, same mechanism as room-wise
  pricing but configured separately per package). Booking one unit of
  a package always reserves exactly one room -- no auto-splitting
  across multiple rooms like room-wise does; a larger group either
  fits within one package's caps or books more than one unit via
  quantity (e.g. 2x Family Room).
- Room-wise and Guest-wise bookings draw from the exact same shared
  room pool and can never double-book each other -- verified directly
  by simulating a package booking and confirming a subsequent
  room-wise availability check on the same date correctly sees the
  reduced room count.
- Guest-wise requires Premium, checked the same way as every other
  Premium feature in this plugin (site-wide license or that specific
  homestay's manual override).
- Bookings lists (WP Admin and portal) show a Type column identifying
  which package (if any) each booking used.
- KNOWN GAP: the customer self-service request workflow (date-change/
  extension/cancellation) and Extend Booking do not yet recognize
  guest-wise bookings, since they're built around looking up a
  homestay via the order line item's own product -- for a package
  item that's the package's product, not the homestay's. Both fail
  safely (a clear error message, no data corruption) rather than
  doing anything incorrect; a guest with a package booking needing a
  change should be handled manually for now. The portal's Bookings
  list hides the Extend action for package rows accordingly.

4.4.0
- Added "Per-Homestay Premium Override" to the portal's License page
  (Group Admin only): a manual Force On / Force Off / Reset switch for
  each homestay, independent of the site-wide license. This is NOT a
  separate license -- no key, no server call, takes effect immediately.
  Lets a Group Admin cut off Premium features for one non-renewing
  property without affecting any other homestay on the site (or the
  reverse -- force a specific homestay to keep Premium even without an
  active site-wide license). Sits on top of the single site-wide
  license from v4.3.2 as an exception list; most homestays are expected
  to stay on "Default" (follow the site license) indefinitely.

4.3.2
- REMOVED per-homestay licensing (added in v4.2.0-v4.3.1) -- consolidated
  back to a SINGLE license that covers every homestay on the site.
  Individual homestays no longer have their own separate license key.
  This was a deliberate simplification: duration and grace period are
  fully governed by the license term set on sitss.in when a key is
  issued, so there's nothing to configure locally in the plugin -- one
  Activate/Deactivate on the portal's License page (now Group Admin
  only, not Homestay Admin) controls Premium for the whole site. If the
  license isn't renewed, every homestay automatically reverts to Free
  once the server-side term and grace period both lapse, with zero
  manual action needed. The now-unused per-homestay database columns on
  hb_homestays are left in place (harmless, nothing reads/writes them
  anymore) rather than risking a destructive migration.
- Partial room closing and the customer self-service request workflow
  are gated by this single license again (previously per-homestay).

4.3.1
- Added a "Site-Wide License" block to the portal's License page (Group
  Admin / unscoped users), so the SITE-WIDE license (the one that covers
  every homestay, previously only manageable via WP Admin) can now be
  activated, re-checked, and deactivated from the portal too -- same as
  the per-homestay licenses added in v4.2.0. Uses the same underlying
  activation logic as WP Admin, so status stays consistent wherever it's
  managed from.

4.3.0
- Added "+ Add New Homestay" to the portal's Homestay Settings section
  (Group Admin / unscoped users), so new properties can be created
  entirely from the portal, no WP Admin needed -- including the
  first-time-setup case where zero homestays exist yet. A minimal form
  (name, rooms, base pricing) creates the homestay and its WooCommerce
  product immediately, the same way WP Admin's "Add New Homestay"
  always has; everything else (occupancy rules, cancellation policy,
  etc.) can be filled in via Homestay Settings right after. Respects
  the same Free-tier homestay limit as WP Admin.

4.2.1
- FIXED: activation could fail with "No licensing server URL is
  configured yet" even when nothing was wrong -- the Licensing Server
  URL now defaults to https://sitss.in automatically if it was never
  explicitly set (or was saved blank), so activation works out of the
  box. You can still override it if your licensing server ever moves.
- Added a "Licensing Server Settings" block to the portal's License
  page (Group Admin / unscoped users), so the Licensing Server URL and
  Upgrade/Purchase URL can now be configured entirely from the portal
  -- no WP Admin access needed for this either, closing the loop for
  sites where WP Admin access to this plugin's own pages is unreliable
  (see the 4.1.1 entry below for background on that).

4.2.0
- The Manager Portal (/hb-dashboard/) is no longer Premium-only -- it's
  now available on the Free version too. Free still keeps its existing
  limits (1 homestay, full-property closing only, no customer
  self-service request workflow); the portal itself, and everything
  else in it (Bookings, Calendar, Check-in/Check-out, Orders, Guest
  Requests, New Booking, Team management, all roles), is now open
  regardless of license status.
- Added PER-HOMESTAY LICENSING: each homestay can now have its own
  independent license, activated from that homestay's own Manager
  Portal > License page -- no WP Admin access required, which matters
  since Homestay Manager/Admin accounts have none by design. A homestay
  is Premium if EITHER the site-wide license (WP Admin) OR its own
  license is active. The homestay-count limit itself stays site-wide
  only (an individual homestay's license can't add more homestay
  slots), but partial room closing and the customer self-service
  request workflow can now be unlocked per property.
  See "PER-HOMESTAY LICENSING" above.
- Customer "My Bookings": viewing bookings and printing/emailing
  invoices is no longer Premium-gated at all -- it's a basic feature
  available to every guest regardless of license. Only the interactive
  request workflow (date-change/extension/cancellation) is
  Premium-gated, now per-homestay rather than site-wide, so a guest
  booked at a Premium-licensed homestay sees the full request options
  even if a guest booked at a different, unlicensed homestay on the
  same site doesn't.

4.1.1
- FIXED: WP Admin > Homestay Booking > License (and every other admin
  page this plugin registers -- the main Homestays list, Calendar,
  Bookings list, and Portal Branding) could show "Sorry, you are not
  allowed to access this page" for a genuine Administrator account.
  These pages were gated behind the "manage_woocommerce" capability,
  which WooCommerce normally grants every Administrator automatically
  -- but that grant can go missing (a role/capability plugin, a
  WooCommerce reactivation, or similar) even for a real admin account.
  Switched all of this plugin's own wp-admin pages to "manage_options"
  instead, WordPress's own built-in capability that every Administrator
  always has regardless of WooCommerce's state. (The multi-vendor
  Homestay Manager/Admin/Group Admin portal-scoping logic, which is a
  different, intentional use of "manage_woocommerce" to mean "unscoped
  shop-manager-level user," is unaffected by this change.)

4.1.0
- Rewired licensing to talk to the actual License Manager Server at
  sitss.in (REST API at /wp-json/license-manager/v1/), replacing the
  earlier generic client with one matching its exact request/response
  format. Product slug corrected to "homestay-booking-premium" (must
  match what's registered on the server). Any key from sitss.in works
  the same way here whether it came from a WooCommerce purchase or was
  issued manually from the server's admin screen -- paste it into
  WP Admin > Homestay Booking > License and activate.
- License status now includes the server's own "grace" period (both
  "active" and "grace" count as Premium-active; grace shows a renewal
  notice on the License page) in addition to active/expired/revoked/
  invalid/limit_reached.
- Background validation now runs via a real WP-Cron job (once daily)
  rather than piggybacking on admin page loads, with a 5-day local
  buffer if a single day's check can't reach the server -- and that
  buffer never overrides a confirmed bad answer from the server (only
  covers "couldn't reach it to check," never "it said no").
- See "HOW LICENSING WORKS" above for the full integration details.

4.0.1
- FIXED: a critical login bug in the Manager Portal. Passwords
  containing an apostrophe, quote, or backslash (e.g. "It's-2026!")
  were being corrupted before reaching WordPress's authentication check,
  causing fully correct credentials to be rejected with a generic
  "Incorrect username or password" message. This was caused by a missing
  wp_unslash() call on the password field (WordPress automatically adds
  backslashes to all submitted form data as a legacy compatibility
  measure; every other field correctly removed them, but the password
  field did not). If you were unable to log into the portal with a
  password containing special characters, this fixes it.
- Added debug logging (only active when WP_DEBUG is enabled) that
  records WordPress's actual authentication error code to your server's
  PHP error log on a failed login attempt -- never the password itself
  -- so any future login issue can be diagnosed precisely instead of
  guessing from the generic on-screen message.

4.0.0
- MAJOR: introduced a Free/Premium split. Free stays fully functional
  for a single homestay with core booking + WP Admin management;
  Premium (license-gated) unlocks unlimited homestays, the entire
  Manager Portal, partial room closing, and the customer self-service +
  cancellation-policy workflow added in 3.5.0. See "FREE VS PREMIUM"
  above for the full breakdown and licensing server setup.
- Added WP Admin > Homestay Booking > License: enter/activate/deactivate
  a license key, see status and homestay usage, configure the licensing
  API URL and the Upgrade/Purchase URL.
- Added an offline grace period (14 days) so a previously-active license
  keeps working if the licensing server is briefly unreachable, with a
  background re-check every 3 days.
- Visiting the Manager Portal URL without an active license now shows an
  "Upgrade to Premium" page instead of the login form.

3.5.1
- Point release for sites still on the 3.5.x branch (pre-Premium split):
  the same login bug fix and debug logging described under 4.0.1 above,
  without any of the Free/Premium licensing changes bundled in.

3.5.0
- Added a customer-facing "My Bookings" tab in WooCommerce My Account.
  Every logged-in customer can see all their bookings with full details,
  print/email their own invoice, and submit requests to:
    - CHANGE DATES (choose a new check-in/check-out)
    - EXTEND their stay (choose a later check-out)
    - CANCEL their booking (shows the exact charge/refund they'd get,
      calculated live from the homestay's cancellation policy, before
      they submit)
  No request changes anything by itself -- every request goes to the
  manager/admin portal's new "Guest Requests" queue for review.
- Added a configurable, tiered CANCELLATION POLICY per homestay (e.g.
  free 7+ days out, 50% charge 3-6 days out, 100% charge inside 3 days)
  and a configurable flat RESCHEDULE FEE (% of the booking total, applied
  when a date-change request is approved). Both are editable from
  WP Admin > Homestay Booking > Edit Homestay AND from the portal's
  Homestay Settings section (Homestay Admin / Group Admin) -- the same
  policy is used everywhere it's shown, to the customer and the manager.
- Added "Guest Requests" to the manager portal: a queue of pending
  date-change/extension/cancellation requests (scoped to the reviewer's
  assigned homestays), each showing the customer, the requested change,
  and -- for cancellations -- the exact charge/refund amount. Approve or
  Reject with one click:
    - Approving a DATE CHANGE actually moves the booking (checks
      availability for the new dates first, automatically rolling back
      if unavailable) and recalculates the price for the new night count,
      applying the reschedule fee if one is configured.
    - Approving an EXTENSION creates a new order for the extra nights,
      billed to the same guest (reusing the existing Extend Booking logic).
    - Approving a CANCELLATION cancels the order and records the exact
      charge/refund as an order note -- actual refund processing still
      goes through WooCommerce/your payment gateway, same as other
      money-affecting actions in this plugin.
  A red badge on the "Guest Requests" nav item shows how many requests
  are waiting.

3.4.0
- Check-in/Check-out lists (Arrivals, Departures, In-House, Recently
  Checked Out) now show the guest's name alongside the Order ID.
- Added portal login branding: WP Admin > Homestay Booking > Portal
  Branding lets you set a custom logo, heading, subheading, and footer
  text for the portal's login screen (upload your logo under Media,
  paste its URL). Falls back to the default look if left blank.
- Added Email and WhatsApp options for invoices, alongside Print:
    - EMAIL sends a self-contained HTML invoice to the order's billing
      email via WordPress's normal mail sending (wp_mail). Note: many
      hosts' default PHP mail() is unreliable for deliverability -- for
      consistent delivery, install and configure an SMTP plugin (e.g.
      WP Mail SMTP) with a real mail provider (Gmail, SendGrid, etc.).
    - WHATSAPP opens a pre-filled WhatsApp message (via the free wa.me
      link format) containing an invoice summary and total, addressed
      to the order's billing phone number. This does NOT attach a PDF
      or use the paid WhatsApp Business API -- it's a zero-setup,
      no-account-required deep link that opens WhatsApp Web/App with
      the message ready to send. If you later want automatic sending
      with a PDF attachment, that requires a genuine WhatsApp Business
      API integration (a Meta Business account, phone number
      verification, and approved message templates) -- let your
      developer know if you want that built out.
    - Both buttons automatically grey out/disable if the order has no
      email or phone number on file.
- All invoice actions (Back, Print, Email, WhatsApp) are now small
  icon-only buttons instead of full-width text buttons, with tooltips
  for clarity. The Orders list's "Print Invoice" link is now a matching
  small icon button too.

3.3.1
- Added "Undo Check-in" (on the Departures and Currently In-House lists)
  and "Undo Check-out" (on a new "Recently Checked Out (today)" list) in
  the portal's Check-in/Check-out section, so a mistaken tap can be
  reversed without editing the database directly.

3.3.0
- Bookings lists (WP Admin and portal) now show the customer's name
  alongside the Order ID, not just the order number.
- Added "New Booking": Homestay Manager/Admin/Group Admin can create a
  booking directly for a walk-in or phone guest, right from the portal.
  Creates a real WooCommerce order (so it behaves identically to a
  normal booking everywhere else -- calendar, check-in/out, invoices).
  Choose either the homestay's regular calculated price, or enter a
  custom total.
- Calendar day cells (WP Admin and portal) are now colour-coded by
  availability status, with a legend: green = fully free, amber = some
  rooms booked/closed but some still free, red = fully booked or fully
  closed. Colours are light tints matching the plugin's existing
  success/warning/danger palette.

3.2.0
- Portal URL changed from ?hb_portal=1 to the pretty URL /hb-dashboard/
  (the old ?hb_portal=1 link still works too, for anyone with an old
  bookmark).
- FIXED: 2 adults + 2 infants was incorrectly forced into 2 rooms.
  Infants no longer count toward a room's total occupancy cap (only
  toward their own separate max-infants-per-room limit) -- they still
  respect that limit, but no longer compete with adults/children for
  shared room space in the capacity calculation.
- Added a mobile hamburger menu: on narrow screens, the sidebar becomes
  an off-canvas drawer with a menu icon, and Logout is now always
  visible on mobile (previously it could disappear). Tables now scroll
  horizontally on mobile instead of squeezing unreadably, and forms/
  stat cards reflow to a mobile-friendly single/double column layout.
  Desktop view is unchanged.
- Extending a booking now creates a NEW WooCommerce order for the
  additional nights, billed to the SAME guest (customer/billing details
  copied over), rather than adding a fee line to the original order.
  The original order/invoice is untouched; the extension gets its own
  order and its own invoice, cross-referenced via order notes on both.
- Added "Print Invoice" on the Orders section -- opens a standalone,
  print-optimized invoice page (booking details, guest info, line
  items, totals) for any order, with a Print button.
- Added the "Group Admin" role: sees and manages EVERY homestay
  (unscoped, like an Administrator, but portal-only), plus a new "Team"
  section in the portal to create Homestay Manager/Homestay Admin
  accounts and assign them to homestay(s) -- entire multi-vendor group
  operations can now be run from the portal without touching WP Admin.

3.1.0
- FIXED: past dates were selectable in the portal's "Close Dates" form
  (no min-date guard) -- now disallowed.
- Added multi-vendor homestay assignment: WP Admin > Homestay Booking >
  Edit Homestay now has an "Assign Users" checklist. Homestay Manager
  and Homestay Admin accounts only ever see homestays explicitly
  assigned to them, enforced server-side on every portal action (not
  just menu visibility) -- true vendor isolation.
- Added the "Homestay Admin" role: everything a Homestay Manager can do,
  plus a "Homestay Settings" portal section to edit that homestay's own
  pricing/rooms/occupancy directly from the portal (scoped to their
  assigned homestay(s) only).
- Added a "Check-in / Check-out" portal section: Arrivals, Departures,
  and Currently In-House lists with one-click status buttons.
- Added "Extend Booking": extend an existing booking's check-out date
  from the Bookings section, with availability validation for the
  additional nights and automatic extra-charge recording (order note +
  fee line) without silently attempting to re-charge a payment method.

3.0.0
- Added partial room closing: choose how many rooms to close instead of
  only being able to close the whole property.
- Added the "Homestay Manager" WordPress role and a dedicated, modern,
  theme-independent frontend portal (Dashboard, Bookings, Calendar,
  Orders) with its own WordPress-integrated login.
- Refactored booking-list and daily-stats queries into shared classes
  (HB_Bookings_Data, HB_Availability::get_daily_stats) used by both WP
  Admin and the new portal, so they can never show different numbers and
  future features naturally extend to both.
- Plugin author changed to Srijani.

2.2.0
- Split "children" into a free infant age band and a paid child age band.

2.1.0
- Added a backend booking calendar and the ability to manually close
  dates (whole-property only, at this version).

2.0.0
- Rebuilt for multi-homestay support.

1.2.0
- Added "Max total occupancy per room" setting.
- Fixed a critical fatal error on the product page.

1.1.0
- Added automatic multi-room booking.

1.0.0
- Initial release.

FILES INCLUDED
--------------
homestay-booking.php                    Main plugin bootstrap
includes/class-hb-install.php           DB tables, role registration, legacy migration
includes/class-hb-license.php           Free/Premium license validation + feature gates
includes/class-hb-license-admin.php     WP Admin License settings page
includes/class-hb-homestay.php          Homestay CRUD + WooCommerce product sync
includes/class-hb-package.php           Guest-wise package CRUD + WooCommerce product sync
includes/class-hb-product-photos.php    Featured image + gallery upload/removal for homestay and package products
includes/class-lms-plugin-updater.php   Automatic WordPress-native update delivery via the sitss.in License Manager Server
includes/class-hb-booking-edit.php      Admin edit/delete of an existing booking (dates, guests, status, advance payment)
includes/class-hb-invoice-template.php  Shared Tally-style invoice body, used by the customer page, portal page, and email
includes/class-hb-car.php               Tour Packages: car-hire resource CRUD + per-date availability
includes/class-hb-activity.php          Tour Packages: activity resource CRUD + per-date availability
includes/class-hb-tour-package.php      Tour Packages: package CRUD + WooCommerce product sync
includes/class-hb-tour-package-component.php  Tour Packages: itinerary step CRUD, slot grouping, pricing
includes/class-hb-tour-package-component-tier.php  Tour Packages: headcount pricing bands per itinerary step
includes/class-hb-tour-package-availability.php  Tour Packages: itinerary resolution, availability check, pricing
includes/class-hb-tour-package-bookings-data.php  Tour Packages: bookings-list queries + order-id lookup used by the Orders page fix
includes/class-hb-user-access.php       Multi-vendor scoping: who can see which homestay
includes/class-hb-homestays-admin.php   Admin UI: list/add/edit/delete homestays + user assignment
includes/class-hb-portal-branding-admin.php  Portal login screen branding settings
includes/class-hb-blackout.php          Full/partial date-closing logic
includes/class-hb-pricing.php           Price calculation logic (per-homestay)
includes/class-hb-availability.php      Availability + shared daily-stats aggregation
includes/class-hb-frontend.php          Booking form + AJAX price/availability check
includes/class-hb-cart.php              Cart validation, custom pricing, cart display
includes/class-hb-order.php             Saves booking to orders + DB table, syncs status
includes/class-hb-bookings-data.php     Shared bookings-list query (WP Admin + portal)
includes/class-hb-orders-data.php       Shared orders query + status updates (portal)
includes/class-hb-guest-status.php      Check-in/check-out tracking per booking
includes/class-hb-booking-extend.php    Extends a booking by creating a new order (same guest)
includes/class-hb-manual-booking.php    Manager-created walk-in bookings (regular or custom price)
includes/class-hb-booking-requests.php  Customer request workflow + cancellation charge calculation
includes/class-hb-invoice-mailer.php    Shared invoice-email builder (portal + customer views)
includes/class-hb-customer-account.php  My Account "My Bookings" tab, requests, customer invoice
includes/class-hb-admin-bookings.php    WP Admin bookings list
includes/class-hb-admin-calendar.php    WP Admin calendar (full/partial closing)
includes/class-hb-portal.php            Portal: login, mobile menu, Dashboard/Bookings/Calendar/
                                         Check-in-Out/Orders/Invoice/Settings/Team/Requests sections
assets/js/hb-frontend.js                Live price/availability JS (guest-facing)
assets/css/hb-frontend.css              Booking form styling (guest-facing)
