Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/IvanchoDev89/maleku-system/llms.txt

Use this file to discover all available pages before exploring further.

Maleku System’s booking engine is built around three design goals: guarantee that no two customers can book the same room or tour slot simultaneously, calculate accurate dynamic pricing at booking time rather than at display time, and immediately notify both the guest and the vendor with a confirmation email containing a unique reference code. The engine handles two fundamentally different booking types — property room stays and tour experiences — each with its own pricing logic and availability model.
All booking endpoints require a verified email address. Requests from users whose email has not been confirmed will be rejected by the require_verified_email dependency before the booking logic is reached.

Booking Types

Property Bookings

Created via POST /api/v1/bookings/property. Requires a property_id and a room_id (the specific room within the property). Pricing is calculated per night using the room’s price_per_night and weekend_price fields. Check-in and check-out dates drive the night count. Minimum stay is 1 night; check-in cannot be in the past.

Tour Bookings

Created via POST /api/v1/bookings/tour. Requires a tour_id, a booking_date, and the number of participants. Pricing is flat per person: tour.price × participants. The booking_date must be a day the tour runs (validated against tour.schedule_days) and cannot exceed tour.max_group_size including existing bookings for that date.

Availability Checking

The booking engine uses a two-phase availability check to eliminate race conditions without requiring application-level mutexes. Phase 1 — Advisory Lock Acquisition Before querying availability, the engine computes a deterministic 31-bit integer key from the resource ID and date range:
def _generate_room_lock_key(room_id, check_in, check_out) -> int:
    key_string = f"{room_id}:{check_in.date().isoformat()}:{check_out.date().isoformat()}"
    return (
        int(hashlib.md5(key_string.encode(), usedforsecurity=False).hexdigest()[:8], 16)
        & 0x7FFFFFFF
    )
This key is passed to PostgreSQL’s session-level advisory lock:
SELECT pg_advisory_xact_lock(:lock_key)
The lock is held for the duration of the transaction. Any concurrent request that hashes to the same key will block at this step until the first transaction commits or rolls back. The advisory lock is skipped automatically when the database URL points to SQLite (used in test environments). Phase 2 — Re-verified Availability With the lock held, check_room_availability() (from availability_service.py) executes two queries:
  1. It looks for any existing booking in PENDING, CONFIRMED, or COMPLETED status whose date range overlaps the requested window.
  2. It checks the RoomAvailability table for any vendor-blocked dates that fall within the range.
If either check finds a conflict, the endpoint returns 409 Conflict with a detail message: "Room is not available for the selected dates". The same pattern applies to tour bookings via check_tour_availability(), which additionally validates against the tour’s scheduled days of the week and counts booked participants for the requested date.

Pricing Calculation

Pricing is computed at booking time by pricing_service.py and is never inferred from the listing’s display price alone.

Property / Room Pricing

# From pricing_service.py
def calculate_room_price(room, check_in, check_out, guests) -> dict:
    nights = (check_out - check_in).days
    weekend_nights = count_weekend_nights(check_in, check_out)
    weekday_nights = nights - weekend_nights

    weekday_price = float(room.price_per_night or 0)
    weekend_price = float(room.weekend_price or weekday_price)

    weekday_total = weekday_nights * weekday_price
    weekend_total = weekend_nights * weekend_price
    base_subtotal = weekday_total + weekend_total

    extra_guests = max(0, guests - room.max_guests)
    extra_guests_total = extra_guests * float(room.extra_guest_price or 0) * nights

    subtotal = base_subtotal + extra_guests_total
    ...
Pricing ComponentRule
Weekday rateroom.price_per_night for Mon–Fri nights
Weekend rateroom.weekend_price for Sat–Sun nights; falls back to weekday rate if not set
Extra guest feeroom.extra_guest_price × (guests - room.max_guests) × nights; applied only when the guest count exceeds the room’s max occupancy
Weekly discountIf stay ≥ 7 nights and property.weekly_discount > 0, the percentage is subtracted from the subtotal after the nightly calculation

Tour Pricing

Tour pricing is a flat per-person rate multiplied by the number of participants. The tour’s duration_hours is stored on the booking record for reference but does not affect the price.
# From pricing_service.py
def calculate_tour_price(tour, participants) -> dict:
    price_per_person = float(tour.price or 0)
    subtotal = price_per_person * participants
    return {"participants": participants, "price_per_person": price_per_person, "subtotal": subtotal, ...}

Price Preview

Guests can preview the full price breakdown before confirming a booking by calling POST /api/v1/bookings/preview. The response includes a detailed breakdown of weekday nights, weekend nights, extra-guest charges, any weekly discount, and the commission amount — all without creating a booking record.

Confirmation Codes

Every booking is assigned a unique confirmation code at creation time using Python’s secrets module:
def generate_confirmation_code():
    return f"CRT-{secrets.token_hex(4).upper()}"
This produces codes in the format CRT-{8 hex chars uppercase}, for example:
CRT-3A8F12E0
CRT-B92C04FD
CRT-1E7A5C28
The code is stored in the confirmation_code column (unique, indexed) and is included in all confirmation emails to both the guest and the vendor.

Booking Statuses

The BookingStatus enum (defined in app/models/base.py) governs the full booking lifecycle:
StatusDescription
pendingBooking created; awaiting payment. Default state at creation.
confirmedPayment succeeded (set by Stripe webhook) or manually confirmed by vendor.
cancelledCancelled by the guest (clients may only cancel their own bookings) or by the vendor.
completedStay or tour has taken place; used for revenue reporting and review eligibility.
refundedPayment was refunded via Stripe; set automatically by the charge.refunded webhook handler.
Role-based status transitions are enforced at the API level:
  • Vendors can only move bookings to confirmed or cancelled.
  • Clients can only move their own bookings to cancelled.
  • Admins / Super Admins can set any status.

Email Notifications

The EmailService sends confirmation emails asynchronously after a booking is committed to the database, so a mail delivery failure never blocks the booking response.
Sent to booking.guest_email (or the authenticated user’s email) immediately after POST /api/v1/bookings/property or POST /api/v1/bookings/tour returns successfully. Includes the confirmation code, property/tour name, dates, participant count, total amount, and currency.
In local development, all outgoing email is captured by MailHog running on port 8025. No real emails are delivered during development. Access the MailHog UI at http://localhost:8025.

Example: Create a Property Booking

POST /api/v1/bookings/property
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "property_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "room_id": "f0e1d2c3-b4a5-6789-bcde-f01234567890",
  "check_in": "2025-09-15T15:00:00Z",
  "check_out": "2025-09-20T11:00:00Z",
  "guests": 2,
  "guest_name": "Jane Doe",
  "guest_email": "jane@example.com",
  "guest_phone": "+1-555-0123",
  "guest_notes": "Late check-in requested after 9 PM"
}
A successful response returns BookingResponse with the generated confirmation_code, calculated subtotal, commission_amount, total_amount, and status: "pending".

Build docs developers (and LLMs) love