Skip to main content

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:

ColumnPurpose
business_typeOne of clinic, pharmacy, retail, breeder, groomer, trainer.
is_publicMaster visibility flag. Defaults to false.
latitude, longitudeGeo coordinates for proximity search.
service_radius_kmOptional service area.
headline, bioMarketing copy shown on the profile.
servicesJSON array — short list of bullet points (e.g. ["Dentistry", "Boarding"]).
phone, addressContact 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:

ParamTypeNotes
business_typestringOne of the supported types. Returns 422 on unknown values.
qstringSubstring match across name, headline, bio. SQL LIKE with % and _ escaped.
near[lat], near[lng]floatLatitude/longitude for proximity. Both required to activate the filter.
near[radius_km]floatDefaults 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:

  1. 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 uses whereBetween on the two coordinate columns — pure arithmetic.
  2. 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 LengthAwarePaginator so 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 index nor show is rate-limited today. Audit P1.
  • Lat/lng range validation — accepted values aren't bounded to [-90,90] / [-180,180]. Audit P1.
  • No service_radius_km enforcement 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).