inital commit
This commit is contained in:
451
api.md
Normal file
451
api.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# 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:
|
||||
```json
|
||||
{
|
||||
"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`:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"email": "vendor@example.com",
|
||||
"password": "StrongPassword123"
|
||||
}
|
||||
```
|
||||
|
||||
Response `200`:
|
||||
```json
|
||||
{
|
||||
"refresh": "<refresh_token>",
|
||||
"access": "<access_token>"
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh JWT
|
||||
- `POST /api/v1/accounts/token/refresh/`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
```json
|
||||
{
|
||||
"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):
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"booking_id": 42,
|
||||
"currency": "usd"
|
||||
}
|
||||
```
|
||||
|
||||
Response `201`:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"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`:
|
||||
```json
|
||||
{
|
||||
"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.organic` — `organic` 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/`.
|
||||
Reference in New Issue
Block a user