Provider Marketplace
The marketplace is the public-facing directory of clinics, pharmacies, breeders, groomers, and trainers a pet owner can browse from inside the vetty app (or the public web). Every tenant has to explicitly opt in before appearing — tenants.is_public = true is the master switch.
Tenants as providers
Tenants carry a small set of marketplace columns layered on top of their core record:
| Column | Purpose |
|---|---|
business_type | One of clinic, pharmacy, retail, breeder, groomer, trainer. |
is_public | Master visibility flag. Defaults to false. |
latitude, longitude | Geo coordinates for proximity search. |
service_radius_km | Optional service area. |
headline, bio | Marketing copy shown on the profile. |
services | JSON array — short list of bullet points (e.g. ["Dentistry", "Boarding"]). |
phone, address | Contact details that surface in the listing. |
Migrations: 2026_04_20_090200_add_provider_fields_to_tenants_table.php, 2026_04_18_090000_add_business_type_to_tenants_table.php.
Search API
GET /api/v1/providers (public, unauthenticated). Query parameters:
| Param | Type | Notes |
|---|---|---|
business_type | string | One of the supported types. Returns 422 on unknown values. |
q | string | Substring match across name, headline, bio. SQL LIKE with % and _ escaped. |
near[lat], near[lng] | float | Latitude/longitude for proximity. Both required to activate the filter. |
near[radius_km] | float | Defaults to 25. Clamped to [1, 500]. |
When near is set, the controller returns results sorted by computed distance ascending and annotates each row with distance_km (rounded to two decimals).
How geo filtering works
A two-phase approach lets the same code work on MySQL, PostgreSQL, and SQLite (:memory: under phpunit) without depending on per-vendor math functions:
- SQL bounding-box pre-filter. The controller computes a latitude/longitude rectangle that circumscribes the radius circle (1° latitude ≈ 111 km; longitude width scales with
cos(lat)). The DB query useswhereBetweenon the two coordinate columns — pure arithmetic. - PHP-side haversine. The (small) candidate set is sorted in PHP using a hand-rolled haversine, filtered to the inscribed radius circle, and paginated into a
LengthAwarePaginatorso the resource collection shape matches the non-geo path.
This replaces an earlier SQL-only approach that required SIN/COS/ASIN/SQRT/POWER/RADIANS UDFs in SQLite.
Single-provider lookup
GET /api/v1/providers/{tenant} returns the full marketplace view of one tenant. Returns 404 when is_public=false.
Audit + roadmap
- Public-PII rate limiting — neither
indexnorshowis rate-limited today. Audit P1. - Lat/lng range validation — accepted values aren't bounded to
[-90,90]/[-180,180]. Audit P1. - No
service_radius_kmenforcement on the seller side — a tenant claiming a 5 km radius still appears for searches at 50 km away. (Roadmap item.) - Future enhancements — verified-provider badge, photo gallery, working-hours field with same-app booking.
How owners use it
The vetty home tab carries a Find a clinic / pharmacy / groomer card. Tapping into it opens the marketplace screen, which:
- Defaults to the device's coordinates (with permission) and a 25 km radius.
- Lets the user filter by
business_type. - Renders a results list with name, distance, services, and a tap target into the provider detail screen.
- Provider detail offers Book appointment (clinic only — currently routes to phone) and Place order (pharmacy / retail — opens the order-request flow; see Commerce).