Files
booking_backend/api.md
2026-04-10 20:51:43 -05:00

13 KiB

WaterTrek API (Frontend Handoff)

Base URL: /api/v1/
Auth: JWT Bearer token in Authorization: Bearer <access_token>

Auth + Accounts

Register Vendor

  • POST /api/v1/accounts/register/vendor/
  • Public endpoint

Request:

{
  "email": "vendor@example.com",
  "password": "StrongPassword123",
  "first_name": "Nora",
  "last_name": "Smith",
  "phone_number": "+1-555-1234",
  "business_name": "Nora Rentals",
  "description": "Jet ski and boat rentals",
  "contact_phone": "+1-555-1234",
  "contact_email": "hello@norarentals.com",
  "address_line1": "1 Harbor Way",
  "city": "Miami",
  "state": "FL",
  "postal_code": "33101",
  "country": "USA"
}

Response 201:

{
  "id": 12,
  "email": "vendor@example.com",
  "first_name": "Nora",
  "last_name": "Smith",
  "phone_number": "+1-555-1234",
  "is_vendor": true,
  "is_customer": false,
  "vendor_profile": {
    "business_name": "Nora Rentals",
    "slug": "nora-rentals",
    "description": "Jet ski and boat rentals",
    "contact_phone": "+1-555-1234",
    "contact_email": "hello@norarentals.com",
    "address_line1": "1 Harbor Way",
    "address_line2": "",
    "city": "Miami",
    "state": "FL",
    "postal_code": "33101",
    "country": "USA",
    "created_at": "2026-04-08T17:00:00Z",
    "updated_at": "2026-04-08T17:00:00Z"
  }
}

Login (JWT)

  • POST /api/v1/accounts/token/

Request:

{
  "email": "vendor@example.com",
  "password": "StrongPassword123"
}

Response 200:

{
  "refresh": "<refresh_token>",
  "access": "<access_token>"
}

Refresh JWT

  • POST /api/v1/accounts/token/refresh/

Request:

{
  "refresh": "<refresh_token>"
}

Current User

  • GET /api/v1/accounts/me/
  • Requires JWT

Vendor Profile Setup / Update

  • GET /api/v1/accounts/vendor-profile/me/
  • PATCH /api/v1/accounts/vendor-profile/me/
  • Requires vendor JWT

Patch example:

{
  "business_name": "Nora Rentals LLC",
  "description": "Premium water equipment rentals",
  "city": "Fort Lauderdale"
}

Equipment APIs

Public Equipment List

  • GET /api/v1/equipment/items/
  • Public endpoint
  • Query params:
    • category (category id or slug)
    • location (icontains match)
    • vendor_slug
    • min_price
    • max_price
    • available_from (ISO datetime)
    • available_to (ISO datetime)

Example: GET /api/v1/equipment/items/?location=miami&min_price=100&max_price=300

Public Equipment Detail

  • GET /api/v1/equipment/items/{public_id}/
  • Public endpoint
  • Marketing / click-through: Each successful detail response appends:
    • marketing_click_id (integer): pass this on POST /api/v1/booking/bookings/request/ as marketing_click_id so bookings can be attributed to the visit (organic or campaign).
    • click_traffic_type: "organic" or "marketing" (derived from query params below).
  • Optional query params (standard UTM + common ad IDs) are read and stored on the click row for vendor reporting:
    • utm_source, utm_medium, utm_campaign, utm_term, utm_content
    • gclid, fbclid
  • If any of those params is non-empty, click_traffic_type is marketing; otherwise organic.

Example: GET /api/v1/equipment/items/jetski-001/?utm_source=google&utm_medium=cpc&utm_campaign=spring_sale

Vendor Inventory CRUD

  • GET /api/v1/equipment/vendor/items/
  • POST /api/v1/equipment/vendor/items/
  • GET /api/v1/equipment/vendor/items/{id}/
  • PUT /api/v1/equipment/vendor/items/{id}/
  • PATCH /api/v1/equipment/vendor/items/{id}/
  • DELETE /api/v1/equipment/vendor/items/{id}/
  • Requires vendor JWT

Create payload:

{
  "public_id": "jetski-001",
  "title": "Yamaha Jet Ski",
  "description": "Fast and stable for 2 riders",
  "details": {
    "engine_cc": 998,
    "seats": 2
  },
  "location": "Miami Beach",
  "price_per_day": "180.00",
  "is_active": true,
  "category_id": 1
}

Vendor Storefront by Slug

  • GET /api/v1/equipment/storefront/{slug}/
  • Public endpoint
  • Returns vendor profile + active inventory list

Adventrues APIs

Public Adventure List

  • GET /api/v1/adventrues/offerings/
  • Public endpoint
  • Query params:
    • category (category id or slug)
    • location (matches meeting_point)
    • vendor_slug
    • min_price
    • max_price
    • available_from (ISO datetime)
    • available_to (ISO datetime)

Public Adventure Detail

  • GET /api/v1/adventrues/offerings/{public_id}/
  • Public endpoint
  • Same marketing / click-through behavior as equipment detail: response includes marketing_click_id and click_traffic_type; UTM / gclid / fbclid query params are captured when present.

Vendor Adventure CRUD

  • GET /api/v1/adventrues/vendor/offerings/
  • POST /api/v1/adventrues/vendor/offerings/
  • GET /api/v1/adventrues/vendor/offerings/{id}/
  • PUT /api/v1/adventrues/vendor/offerings/{id}/
  • PATCH /api/v1/adventrues/vendor/offerings/{id}/
  • DELETE /api/v1/adventrues/vendor/offerings/{id}/
  • Requires vendor JWT

Create payload:

{
  "public_id": "sunset-kayak-001",
  "title": "Sunset Kayak Tour",
  "description": "Guided paddle with sunset views",
  "meeting_point": "Biscayne Bay Marina",
  "duration_minutes": 120,
  "capacity": 10,
  "price_per_person": "75.00",
  "is_active": true,
  "category_id": 1
}

Vendor Adventure Storefront by Slug

  • GET /api/v1/adventrues/storefront/{slug}/
  • Public endpoint
  • Returns vendor profile + active adventure offerings

Booking APIs

Availability Check

  • GET /api/v1/booking/availability/
  • Public endpoint
  • Required query params:
    • exactly one of equipment_item_id or adventure_offering_id
    • starts_at (ISO datetime)
    • ends_at (ISO datetime)
  • Rule notes:
    • If availability slots exist for the target, booking requires at least one is_available=true slot fully covering the requested range.
    • Any overlapping is_available=false slot blocks the request.
    • If no slots exist yet, target is treated as bookable by default (subject to overlap checks).

Example: GET /api/v1/booking/availability/?equipment_item_id=5&starts_at=2026-04-12T10:00:00Z&ends_at=2026-04-13T10:00:00Z

Response:

{
  "equipment_item_id": 5,
  "adventure_offering_id": null,
  "starts_at": "2026-04-12T10:00:00Z",
  "ends_at": "2026-04-13T10:00:00Z",
  "is_available": true,
  "conflicts": 0
}

Create Booking Request (Customer)

  • POST /api/v1/booking/bookings/request/
  • Requires customer JWT

Request:

{
  "equipment_item_id": 5,
  "starts_at": "2026-04-12T10:00:00Z",
  "ends_at": "2026-04-14T10:00:00Z",
  "customer_notes": "Need two life jackets",
  "marketing_click_id": 9042
}
  • Optional marketing_click_id: integer from the latest GET detail response (marketing_click_id) for the same equipment item or adventure offering. Must be within 90 days of the click. Omit for unattributed bookings (still allowed).

Response includes booking with status requested and listing_click (nullable FK id) when attribution was applied.

Adventure booking request example:

{
  "adventure_offering_id": 9,
  "starts_at": "2026-05-10T15:00:00Z",
  "ends_at": "2026-05-10T17:00:00Z",
  "participants_count": 3,
  "customer_notes": "First-time kayakers"
}

List My Bookings

  • GET /api/v1/booking/bookings/
  • Requires JWT
  • If caller is vendor: returns bookings for their vendor profile
  • Else: returns bookings where caller is customer

Booking Detail

  • GET /api/v1/booking/bookings/{id}/
  • Requires JWT with ownership (customer or vendor on that booking)

Vendor Approve Booking

  • POST /api/v1/booking/bookings/{id}/approve/
  • Requires vendor JWT (own booking only)

Payload (optional):

{
  "vendor_notes": "Approved, pickup after 9AM",
  "note": "Ready for pickup window"
}

Vendor Decline Booking

  • POST /api/v1/booking/bookings/{id}/decline/
  • Requires vendor JWT (own booking only)

Customer Cancel Booking

  • POST /api/v1/booking/bookings/{id}/cancel/
  • Requires customer JWT (own booking only)

Payload (optional):

{
  "note": "Trip postponed to next month"
}

Payment APIs (Mock Stripe)

Create Payment Intent (Mock)

  • POST /api/v1/payment/intents/
  • Requires customer JWT (must own booking)
  • Booking must be in approved state

Request:

{
  "booking_id": 42,
  "currency": "usd"
}

Response 201:

{
  "payment": {
    "id": 10,
    "booking_id": 42,
    "stripe_payment_intent_id": "pi_mock_123abc...",
    "stripe_charge_id": "",
    "amount": "360.00",
    "currency": "usd",
    "status": "requires_payment",
    "created_at": "2026-04-08T18:00:00Z",
    "updated_at": "2026-04-08T18:00:00Z"
  },
  "client_secret": "pi_secret_mock_123abc...",
  "mocked": true
}

Payment Status

  • GET /api/v1/payment/{payment_id}/status/
  • Requires JWT (booking customer or booking vendor owner)

Stripe Webhook (Mocked)

  • POST /api/v1/payment/webhooks/stripe/
  • Public endpoint for local/mock integration
  • Idempotent by stripe_event_id

Request:

{
  "stripe_event_id": "evt_mock_001",
  "event_type": "payment_intent.succeeded",
  "stripe_payment_intent_id": "pi_mock_123abc...",
  "payload": {
    "source": "frontend-test"
  }
}

Supported event_type values:

  • payment_intent.processing
  • payment_intent.succeeded
  • payment_intent.payment_failed
  • charge.refunded

Behavior:

  • Updates PaymentRecord.status
  • On payment_intent.succeeded, booking auto-transitions approved -> confirmed
  • Stores webhook in WebhookEvent and enforces idempotency

Marketing APIs (UTM, click-through, vendor analytics)

Base path: /api/v1/marketing/

Track listing click (explicit)

  • POST /api/v1/marketing/track/click/
  • Public (optional JWT if the user is logged in; stored on the click when present)
  • Use when the SPA opens a listing without calling the public detail endpoint (e.g. client-side route from cache). Otherwise prefer relying on GET equipment/adventure detail, which records the click automatically.

Request:

{
  "listing_type": "equipment",
  "public_id": "jetski-001",
  "utm_source": "newsletter",
  "utm_medium": "email",
  "utm_campaign": "april_promo",
  "utm_term": "",
  "utm_content": "hero",
  "gclid": "",
  "fbclid": "",
  "referrer": "https://mail.example.com/"
}
  • listing_type: "equipment" or "adventure" (must match public_id).

Response 201:

{
  "marketing_click_id": 9042,
  "traffic_type": "marketing"
}

Vendor: list listing clicks

  • GET /api/v1/marketing/vendor/clicks/
  • Requires vendor JWT
  • Required query params: from, to — ISO-8601 datetimes (UTC), half-open range [from, to).
  • Optional filters: traffic_type (organic | marketing), listing_type (equipment | adventure), utm_campaign (exact match).

Vendor: marketing summary (conversions)

  • GET /api/v1/marketing/vendor/summary/
  • Requires vendor JWT
  • Required query params: from, to — same as above.

Returns (shape):

  • clicks.organic / clicks.marketing / clicks.total — listing detail or track-click events in range.
  • bookings_attributed.organic — bookings in range whose listing_click.traffic_type is organic.
  • bookings_attributed.marketing — bookings in range whose listing_click.traffic_type is marketing.
  • bookings_attributed.unattributed — bookings in range with no listing_click.
  • conversion_rate_click_to_booking.organicorganic bookings ÷ organic clicks (omitted / null if clicks = 0).
  • conversion_rate_click_to_booking.marketing — same for marketing.
  • campaigns — up to 50 rows with non-empty utm_campaign on clicks: utm_source, utm_medium, utm_campaign, clicks, bookings (bookings that referenced a click with that triple), conversion_rate.

Interpreting organic vs marketing: Traffic is classified per click from UTM / gclid / fbclid. To compare conversion rates fairly, the client should send marketing_click_id on checkout for both organic and paid visits when possible (so bookings are not left in unattributed).

Notes for React Integration

  • All datetimes are ISO-8601 and UTC.
  • Booking overlap protection is implemented for requested, approved, and confirmed windows.
  • requested works as a soft hold and blocks overlapping requests until resolved.
  • total_price is computed server-side:
    • equipment: rounded-up day count x price_per_day
    • adventure: participants_count x price_per_person
  • Payment APIs are currently mocked (Stripe-style IDs and webhook event flow) to unblock frontend integration before live Stripe keys are wired.
  • Use public_id for equipment-facing detail pages and numeric id for vendor CRUD.
  • Booking domain services are implemented server-side for rules and transitions:
    • is_bookable(...)
    • quote_booking(...)
    • create_booking_request(...)
    • transition_booking_status(...)
  • Persist marketing_click_id from listing detail (or POST .../marketing/track/click/) and send it with POST .../booking/bookings/request/ so vendors can tie bookings to campaigns and compare organic vs marketing conversion in GET .../marketing/vendor/summary/.