# Livestock Marketplace – Listing Service Spec This document covers: 1. All DB tables required for animal listings 2. API endpoints – purpose, request, and response formats --- ## 1. Database Tables > Convention: Every table has `created_at` and `updated_at` (`TIMESTAMP`). ### 1.1 `users` (reference) Minimal definition (assuming you already have auth elsewhere). | Column | Type | Constraints | Description | | ---------- | --------- | ---------------- | ---------------------- | | id | UUID | PK | User ID (seller/buyer) | | name | VARCHAR | NOT NULL | Display name | | phone | VARCHAR | UNIQUE, NOT NULL | Phone number | | created_at | TIMESTAMP | NOT NULL | Row created time | | updated_at | TIMESTAMP | NOT NULL | Row last updated time | **Relationships** - 1 `user` → N `listings` - 1 `user` → N `locations` (saved addresses only) --- ### 1.2 `species` | Column | Type | Constraints | Description | | ---------- | --------- | ----------- | -------------------------- | | id | INT | PK | Species ID | | name | VARCHAR | UNIQUE | e.g. Cattle, Buffalo, Goat | | created_at | TIMESTAMP | NOT NULL | | | updated_at | TIMESTAMP | NOT NULL | | **Relationships** - 1 `species` → N `breeds` - 1 `species` → N `animals` --- ### 1.3 `breeds` | Column | Type | Constraints | Description | | ----------- | --------- | --------------- | ---------------- | | id | INT | PK | Breed ID | | species_id | INT | FK → species.id | Parent species | | name | VARCHAR | NOT NULL | e.g. Gir, Murrah | | description | TEXT | NULL | Optional notes | | created_at | TIMESTAMP | NOT NULL | | | updated_at | TIMESTAMP | NOT NULL | | **Relationships** - 1 `species` → N `breeds` - 1 `breed` → N `animals` --- ### 1.4 `locations` Used both for: - **Captured locations** (no user, not saved; `user_id = NULL`, `is_saved_address = false`) - **Saved addresses** for a user (e.g. farm, home; `user_id` set, `is_saved_address = true`) | Column | Type | Constraints | Description | | ----------------- | --------- | --------------------------- | --------------------------------------------------------------------- | | id | UUID | PK | Location ID | | user_id | UUID | FK → users.id, NULLABLE | Owner if this is a saved address; NULL if just captured for a listing | | is_saved_address | BOOLEAN | NOT NULL, default false | True if user chose to save it as an address | | location_type | VARCHAR | NULL (enum suggestion) | e.g. `farm`, `home`, `office`, `other` | | country | VARCHAR | NULL | Country | | state | VARCHAR | NULL | State | | district | VARCHAR | NULL | District | | city_village | VARCHAR | NULL | City / village | | pincode | VARCHAR | NULL | Postal code | | lat | DECIMAL | NULL | Latitude | | lng | DECIMAL | NULL | Longitude | | source_type | VARCHAR | NOT NULL, default `unknown` | `device_gps`, `manual`, `unknown` | | source_confidence | VARCHAR | NOT NULL, default `medium` | `high`, `medium`, `low` | | created_at | TIMESTAMP | NOT NULL | | | updated_at | TIMESTAMP | NOT NULL | | **Interpretation** - **Captured only**: `user_id = NULL`, `is_saved_address = false` - **Captured + saved as user’s farm/home**: set `user_id`, `is_saved_address = true`, `location_type = 'farm'` (or similar). **Relationships** - 1 `user` → N `locations` (saved addresses) - 1 `location` → N `animals` (many animals can share same farm address) --- ### 1.5 `animals` One animal per listing (enforced via unique constraint at `listings.animal_id`). | Column | Type | Constraints | Description | | -------------------------- | --------- | ------------------------- | ----------------------------------------------- | | id | UUID | PK | Animal ID | | species_id | INT | FK → species.id, NOT NULL | Species | | breed_id | INT | FK → breeds.id, NULL | Breed (optional) | | sex | VARCHAR | NOT NULL | `M`, `F`, `Neutered` | | age_months | INT | NULL | Age in months | | weight_kg | DECIMAL | NULL | Approx weight | | color_markings | VARCHAR | NULL | Color / markings | | quantity | INT | NOT NULL, default 1 | Number of animals in this listing | | purpose | VARCHAR | NOT NULL | `dairy`, `meat`, `breeding`, `pet`, `work`, etc | | health_status | VARCHAR | NOT NULL | `healthy`, `minor_issues`, `serious_issues` | | vaccinated | BOOLEAN | NOT NULL, default false | | | dewormed | BOOLEAN | NOT NULL, default false | | | previous_pregnancies_count | INT | NULL | For females, number of previous pregnancies | | pregnancy_status | VARCHAR | NULL | `not_pregnant`, `pregnant`, `recently_calved` | | milk_yield_litre_per_day | DECIMAL | NULL | Avg daily milk yield | | ear_tag_no | VARCHAR | NULL | Tag / registration ID | | description | TEXT | NULL | Detailed description | | suggested_care | TEXT | NULL | Suggested food & accessories (free-text) | | location_id | UUID | FK → locations.id, NULL | Location of animal (farm etc.). NULL if unknown | | created_at | TIMESTAMP | NOT NULL | | | updated_at | TIMESTAMP | NOT NULL | | **Relationships** - 1 `species` → N `animals` - 1 `breed` → N `animals` - 1 `location` → N `animals` - 1 `animal` ↔ 1 `listing` (via `listings.animal_id` UNIQUE) --- ### 1.6 `listings` One listing per animal (1–1). | Column | Type | Constraints | Description | | ------------------------ | --------- | --------------------------------- | ------------------------------------- | | id | UUID | PK | Listing ID | | seller_id | UUID | FK → users.id, NOT NULL | Seller | | animal_id | UUID | FK → animals.id, UNIQUE, NOT NULL | The animal this listing is for | | title | VARCHAR | NOT NULL | Listing title | | price | DECIMAL | NOT NULL | Asking price | | currency | VARCHAR | NOT NULL, e.g. `INR` | Currency code | | is_negotiable | BOOLEAN | NOT NULL, default true | Price negotiable | | listing_type | VARCHAR | NOT NULL | `sale`, `stud_service`, `adoption` | | status | VARCHAR | NOT NULL, default `active` | `active`, `sold`, `expired`, `hidden` | | views_count | INT | NOT NULL, default 0 | Total views | | bookmarks_count | INT | NOT NULL, default 0 | Times bookmarked | | enquiries_call_count | INT | NOT NULL, default 0 | Phone enquiries | | enquiries_whatsapp_count | INT | NOT NULL, default 0 | WhatsApp enquiries | | clicks_count | INT | NOT NULL, default 0 | Other CTA clicks (e.g. “View number”) | | created_at | TIMESTAMP | NOT NULL | | | updated_at | TIMESTAMP | NOT NULL | | **Relationships** - 1 `user` → N `listings` - 1 `animal` ↔ 1 `listing` - 1 `listing` → N `listing_media` --- ### 1.7 `listing_media` (images / videos) | Column | Type | Constraints | Description | | ---------- | --------- | ----------------------- | -------------------------------- | | id | UUID | PK | Media ID | | listing_id | UUID | FK → listings.id | Parent listing | | media_url | VARCHAR | NOT NULL | URL to image/video | | media_type | VARCHAR | NOT NULL | `image`, `video` | | is_primary | BOOLEAN | NOT NULL, default false | True if main display image/video | | sort_order | INT | NOT NULL, default 0 | For ordering media in gallery | | created_at | TIMESTAMP | NOT NULL | | | updated_at | TIMESTAMP | NOT NULL | | **Relationships** - 1 `listing` → N `listing_media` --- ### 1.8 Relationship Summary - **1–N** - `users` → `listings` - `users` → `locations` (saved addresses) - `species` → `breeds` - `species` → `animals` - `breeds` → `animals` - `locations` → `animals` - `listings` → `listing_media` - **1–1** - `animals` ↔ `listings` (enforced via `listings.animal_id` UNIQUE) - **N–M** - None currently; all many-to-many are avoided in this MVP schema. --- ## 2. API Endpoints ### 2.1 Create Listing (with Animal + optional Location) #### `POST /listings` **Purpose** Create a new listing and its animal. Optionally: - Use an existing `location_id` or - Create a new captured/saved location in the same call. **Request (JSON)** ```json { "seller_id": "UUID-of-seller", "title": "High-yield Gir cow for sale", "price": 55000, "currency": "INR", "is_negotiable": true, "listing_type": "sale", "animal": { "species_id": 1, "breed_id": 10, "sex": "F", "age_months": 36, "weight_kg": 450, "color_markings": "Brown with white patches", "quantity": 1, "purpose": "dairy", "health_status": "healthy", "vaccinated": true, "dewormed": true, "previous_pregnancies_count": 1, "pregnancy_status": "pregnant", "milk_yield_litre_per_day": 15, "ear_tag_no": "TAG-12345", "description": "Calm nature, easy to handle.", "suggested_care": "Green fodder, mineral mix, clean shed.", "location_id": "existing-location-uuid", "new_location": { "country": "India", "state": "Maharashtra", "district": "Pune", "city_village": "Baramati", "pincode": "413102", "lat": 18.15, "lng": 74.5833, "source_type": "device_gps", "source_confidence": "high", "save_as_address": true, "location_type": "farm" } }, "media": [ { "media_url": "https://cdn.app.com/listings/abc1.jpg", "media_type": "image", "is_primary": true, "sort_order": 1 } ] } ``` Notes: - Client can either: - Provide `location_id`, **or** - Provide `new_location` object. If `save_as_address = true`, backend should create a `locations` row with `user_id = seller_id`, `is_saved_address = true`. - Media is optional in this first call; can also be added later via media APIs. **Response (201 Created)** ```json { "listing": { "id": "listing-uuid", "seller_id": "UUID-of-seller", "animal_id": "animal-uuid", "title": "High-yield Gir cow for sale", "price": 55000, "currency": "INR", "is_negotiable": true, "listing_type": "sale", "status": "active", "views_count": 0, "bookmarks_count": 0, "enquiries_call_count": 0, "enquiries_whatsapp_count": 0, "clicks_count": 0, "created_at": "2025-11-22T10:00:00Z", "updated_at": "2025-11-22T10:00:00Z", "animal": { "id": "animal-uuid", "species_id": 1, "breed_id": 10, "sex": "F", "age_months": 36, "weight_kg": 450, "color_markings": "Brown with white patches", "quantity": 1, "purpose": "dairy", "health_status": "healthy", "vaccinated": true, "dewormed": true, "previous_pregnancies_count": 1, "pregnancy_status": "pregnant", "milk_yield_litre_per_day": 15, "ear_tag_no": "TAG-12345", "description": "Calm nature, easy to handle.", "suggested_care": "Green fodder, mineral mix, clean shed.", "location": { "id": "location-uuid", "user_id": "UUID-of-seller", "is_saved_address": true, "location_type": "farm", "country": "India", "state": "Maharashtra", "district": "Pune", "city_village": "Baramati", "pincode": "413102", "lat": 18.15, "lng": 74.5833, "source_type": "device_gps", "source_confidence": "high" } }, "media": [ { "id": "media-uuid", "media_url": "https://cdn.app.com/listings/abc1.jpg", "media_type": "image", "is_primary": true, "sort_order": 1 } ] } } ``` --- ### 2.2 List / Search Listings #### `GET /listings` **Purpose** List active listings with optional filters (species, location, price, etc.). **Query parameters (examples)** - `species_id` (int, optional) - `breed_id` (int, optional) - `state` (string, optional) - `district` (string, optional) - `min_price`, `max_price` (optional) - `listing_type` (string, optional) - `page`, `page_size` (for pagination) **Request** ```http GET /listings?species_id=1&state=Maharashtra&page=1&page_size=20 ``` **Response (200 OK)** ```json { "items": [ { "id": "listing-uuid", "title": "High-yield Gir cow for sale", "price": 55000, "currency": "INR", "is_negotiable": true, "listing_type": "sale", "status": "active", "species_id": 1, "breed_id": 10, "animal_id": "animal-uuid", "thumbnail_url": "https://cdn.app.com/listings/abc1.jpg", "location_summary": { "state": "Maharashtra", "district": "Pune", "city_village": "Baramati" }, "created_at": "2025-11-22T10:00:00Z" } ], "page": 1, "page_size": 20, "total": 1 } ``` --- ### 2.3 Get Listing Detail #### `GET /listings/{listing_id}` **Purpose** Get full details of a single listing (including animal, location, media). **Response (200 OK)** ```json { "id": "listing-uuid", "seller_id": "UUID-of-seller", "animal_id": "animal-uuid", "title": "High-yield Gir cow for sale", "price": 55000, "currency": "INR", "is_negotiable": true, "listing_type": "sale", "status": "active", "views_count": 120, "bookmarks_count": 10, "enquiries_call_count": 5, "enquiries_whatsapp_count": 8, "clicks_count": 14, "created_at": "2025-11-22T10:00:00Z", "updated_at": "2025-11-22T11:00:00Z", "animal": { "...": "full animal object as above" }, "media": [ { "id": "media-uuid", "media_url": "https://cdn.app.com/listings/abc1.jpg", "media_type": "image", "is_primary": true, "sort_order": 1 } ] } ``` --- ### 2.4 Update Listing (and Animal) #### `PUT /listings/{listing_id}` **Purpose** Edit listing fields and animal details (e.g. price, status, description, suggested care). **Request (JSON)** Only fields to update need to be sent (PATCH style with PUT semantics). ```json { "title": "Gir cow – price reduced", "price": 52000, "status": "active", "animal": { "description": "Price reduced, urgent sale.", "suggested_care": "Green fodder, clean water, regular deworming." } } ``` **Response (200 OK)** Returns updated listing object (same shape as `GET /listings/{id}`). --- ### 2.5 Create / Capture Location #### `POST /locations` **Purpose** Create a new location. Used for: - Saved address for a user (farm/home) - Captured location for an animal/listing (not necessarily saved) **Request (JSON)** ```json { "user_id": "UUID-of-user-or-null", "is_saved_address": true, "location_type": "farm", "country": "India", "state": "Maharashtra", "district": "Pune", "city_village": "Baramati", "pincode": "413102", "lat": 18.15, "lng": 74.5833, "source_type": "device_gps", "source_confidence": "high" } ``` - For **captured-only** (not saved): set `user_id = null`, `is_saved_address = false`. **Response (201 Created)** ```json { "id": "location-uuid", "user_id": "UUID-of-user-or-null", "is_saved_address": true, "location_type": "farm", "country": "India", "state": "Maharashtra", "district": "Pune", "city_village": "Baramati", "pincode": "413102", "lat": 18.15, "lng": 74.5833, "source_type": "device_gps", "source_confidence": "high", "created_at": "2025-11-22T10:05:00Z", "updated_at": "2025-11-22T10:05:00Z" } ``` --- ### 2.6 Update Location (PUT for Locations) #### `PUT /locations/{location_id}` **Purpose** Update location details OR convert a captured location into a saved address for a user (e.g. mark as farm). **Request (JSON)** ```json { "user_id": "UUID-of-user", "is_saved_address": true, "location_type": "farm", "city_village": "New Village Name", "pincode": "413103" } ``` **Response (200 OK)** ```json { "id": "location-uuid", "user_id": "UUID-of-user", "is_saved_address": true, "location_type": "farm", "country": "India", "state": "Maharashtra", "district": "Pune", "city_village": "New Village Name", "pincode": "413103", "lat": 18.15, "lng": 74.5833, "source_type": "device_gps", "source_confidence": "high", "created_at": "2025-11-22T10:05:00Z", "updated_at": "2025-11-22T11:00:00Z" } ``` --- ### 2.7 Get Saved Locations for a User #### `GET /users/{user_id}/locations` **Purpose** Fetch all saved addresses for a given user (farm, home, etc.). **Response (200 OK)** ```json { "items": [ { "id": "location-uuid-1", "user_id": "UUID-of-user", "is_saved_address": true, "location_type": "farm", "country": "India", "state": "Maharashtra", "district": "Pune", "city_village": "Baramati", "pincode": "413102", "lat": 18.15, "lng": 74.5833, "source_type": "device_gps", "source_confidence": "high", "created_at": "2025-11-22T10:00:00Z", "updated_at": "2025-11-22T10:00:00Z" } ] } ``` --- ### 2.8 Add Media to Listing #### `POST /listings/{listing_id}/media` **Purpose** Attach new images/videos to a listing after creation. **Request (JSON)** ```json { "items": [ { "media_url": "https://cdn.app.com/listings/abc2.jpg", "media_type": "image", "is_primary": false, "sort_order": 2 }, { "media_url": "https://cdn.app.com/listings/abc3.mp4", "media_type": "video", "is_primary": false, "sort_order": 3 } ] } ``` **Response (201 Created)** ```json { "media": [ { "id": "media-uuid-2", "listing_id": "listing-uuid", "media_url": "https://cdn.app.com/listings/abc2.jpg", "media_type": "image", "is_primary": false, "sort_order": 2 }, { "id": "media-uuid-3", "listing_id": "listing-uuid", "media_url": "https://cdn.app.com/listings/abc3.mp4", "media_type": "video", "is_primary": false, "sort_order": 3 } ] } ``` --- ### 2.9 Update Media (PUT for Images/Media) #### `PUT /listing-media/{media_id}` **Purpose** Update a single media item (e.g. mark as primary, change sort order, fix URL). **Request (JSON)** ```json { "is_primary": true, "sort_order": 1 } ``` **Response (200 OK)** ```json { "id": "media-uuid-2", "listing_id": "listing-uuid", "media_url": "https://cdn.app.com/listings/abc2.jpg", "media_type": "image", "is_primary": true, "sort_order": 1, "created_at": "2025-11-22T10:10:00Z", "updated_at": "2025-11-22T10:20:00Z" } ``` --- ### 2.10 Create a Custom Requirement #### `POST /requirements` **Purpose** Custom Requirements allow buyers to express needs such as "Looking for a cow giving more than 10 litres of milk per day." These relate to an animal_id and are visible to sellers while listing animals. **Request Body** ```json { "buyer_id": 12, "animal_id": 201, "title": "Cow giving 10+ litres milk", "description": "Healthy cow with high milk output", "min_price": 30000, "max_price": 60000, "location": "Pune" } ``` **Response** ```json { "requirement_id": 501, "message": "Custom requirement created successfully" } ``` --- ### 2.11 Update an Existing Requirement #### `PUT /requirements/{requirement_id}` **Request Body** ```json { "title": "Cow giving 12+ litres", "max_price": 65000 } ``` --- ### 2.12 Delete a Requirement #### `DELETE /requirements/{requirement_id}` --- ### 2.13 Get All Requirements for a Buyer #### `GET /requirements/buyer/{buyer_id}` **Purpose** Retrieves all active and past requirements of a buyer. --- ### 2.14 Get Matching Requirements for an Animal (For Sellers) #### `GET /requirements/matching?animal_id={animal_id}` **Purpose** Used when a seller lists an animal so the system can show matching requirements.