Locations (Spaces) — QA Plan
Pre-requisites
- Test org on
liteplatform tier (maxSpaces=1, lets you hit the tier ceiling quickly), and one onpro(maxSpaces=2). - A second org for isolation tests.
- Owner, admin, coach, member personas.
- An org session with
location_idset to a location WITH coordinates, and another session withlocation_idset to a location WITHOUT coordinates. - For the spaces section: an org with the
spacesflag on, a program with a class type, anexclusivespace “Main Floor” (capacity 12) and asharedspace “Open Gym”.
Golden paths
G1 — Owner adds a location with autocomplete
| Step | Action | Expected |
|---|---|---|
| 1 | Owner navigates to settings → Spaces. | List loads, ordered by sort order then name. |
| 2 | Click add. Type address into autocomplete. | Suggestions appear from Google Places. |
| 3 | Pick a result. | Fields populate: address, city, country, lat, lng. |
| 4 | Set name + capacity. Submit. | POST succeeds; row appears with non-null lat/lng. |
G2 — Owner adds a location without coords (manual)
| Step | Action | Expected |
|---|---|---|
| 1 | Submit form with name only, no autocomplete. | Row inserted; lat/lng null. |
| 2 | Try GPS check-in on a session linked to this location. | 400 ‘Session location coordinates are not configured’. |
G3 — Deactivate
| Step | Action | Expected |
|---|---|---|
| 1 | Delete location. | is_active=false. List omits it. |
| 2 | A future session still linked: opens fine; location name still resolvable for display. |
Edge cases
E1 — Tier limit
| Step | Action | Expected |
|---|---|---|
| 1 | Org at lite already has 1 active space (maxSpaces=1). | |
| 2 | POST a new location. | 403 ‘Space limit reached (1/1). Upgrade your plan to add more.‘ |
| 3 | Upgrade to pro. | Now POST succeeds. |
E1a — Pro can create up to 2 spaces
| Step | Action | Expected |
|---|---|---|
| 1 | pro org with 1 active space. POST a second. | 201. |
| 2 | POST a third. | 403 ‘Space limit reached (2/2). Upgrade your plan to add more.‘ |
| 3 | Deactivate one; POST again. | 201 (only active spaces count). |
| 4 | Same org on elite. | No limit. |
E2 — Tier downgrade with over-limit
| Step | Action | Expected |
|---|---|---|
| 1 | Org at pro with locations > lite cap. Downgrade to lite. | Locations remain active. (Tier check is creation-time only — verify whether a separate enforcement exists in platform-billing TODO.) |
E3 — Cross-org access
| Step | Action | Expected |
|---|---|---|
| 1 | Owner of org A: GET /organizations/A/locations/{idFromB}. | 404. |
| 2 | Same for PATCH and DELETE. | 404 each. |
E4 — Non-staff mutation
| Step | Action | Expected |
|---|---|---|
| 1 | Coach POST/PATCH/DELETE. | 403. |
| 2 | Member same. | 403. |
E5 — Partial coords
| Step | Action | Expected |
|---|---|---|
| 1 | POST with latitude=32.07 and no longitude. | Row inserted (both nullable). |
| 2 | GPS check-in against a session linked here. | 400 ‘Session location coordinates are not configured’. |
E6 — GPS Haversine boundary
| Step | Action | Expected |
|---|---|---|
| 1 | Location at known coords. Self-checkin from just inside GPS_MAX_METERS. | Succeeds. |
| 2 | From just outside GPS_MAX_METERS. | 400 ‘GPS_TOO_FAR’. |
E7 — Concurrency and order fields
| Step | Action | Expected |
|---|---|---|
| 1 | POST without concurrency/sortOrder. | Row has concurrency='exclusive', sortOrder=0. |
| 2 | PATCH concurrency:'shared', sortOrder:2. | Response reflects both. |
| 3 | PATCH concurrency:'private' or sortOrder:-1. | 400. |
| 4 | Three spaces with sortOrder 1, 0, 0 (names “B”, “C”, “A”). GET list. | Order: A, C, B. |
Spaces flag (spaces)
Single session create/edit, flag on unless stated. Times are org-local on one date. Bulk writers are covered in scheduling-bookings QA.
| # | Scenario | Expected |
|---|---|---|
| S1 | Session 07:00–08:00 in Main Floor (exclusive). Coach creates 07:30–08:30 in Main Floor. | 409 { code:'space_conflict', locationId, conflictingSessionIds:[<first id>] }. No row inserted. |
| S2 | Same as S1 in Open Gym (shared). | 201. |
| S3 | Session 07:00–08:00 in Main Floor. Create 08:00–09:00 in Main Floor. | 201 (adjacent is not overlap). |
| S4 | S1, but the existing session is cancelled, or soft-deleted. | 201 (ignored). |
| S5 | S1 retried by owner with overrideSpaceConflict:{ reason:'Two coaches split the floor' }. | 201. One audit_events row (AuditService), action class_session.space_override, category operations, target the new session, metadata { rule: 'conflict', reason, locationId, locationName, conflictingSessionIds }. |
| S6 | S1 retried by a coach with overrideSpaceConflict. | 403. No row, no audit. |
| S7 | Create in Main Floor (capacity 12) with capacity:15. | 409 { code:'space_capacity_exceeded', locationId, spaceCapacity:12, capacity:15 }. |
| S8 | S7 retried by admin with overrideSpaceCapacity:{ reason }. | 201; audited as in S5. Same with capacity:12 and no override: 201. |
| S9 | Overlap and capacity both broken. Owner sends only overrideSpaceConflict. | 409 space_capacity_exceeded. Retry with both overrides: 201. |
| S10 | Edit an existing session: PATCH only title. Its time already overlaps another in an exclusive space. | 200 (no check: no location/time/capacity field in the request). |
| S11 | PATCH startsAt/endsAt into an overlap in Main Floor. | 409 space_conflict. |
| S12 | Override sent when nothing clashes. | 201; override ignored, no audit row. |
| S13 | Flag off: repeat S1, S7 and S6. | All 201; no 409, no 403, no audit. Behavior identical to before TKN-444. |
| S14 | Web, flag on, org with ≥2 active spaces: open Create session. | Space is required. On a 409, owner/admin see an override dialog asking for a reason; coaches see the error only. |
Cross-persona
- Members can list and get-by-id; cannot mutate.
- Coaches read but cannot mutate.
i18n
| Lang | Strings to verify |
|---|---|
| en | schedule.locationsTab.empty + emptyHint show the empty state and talk about spaces. |
| he | Same key, Hebrew. |
| ru | Same key, Russian. |
| All | ”Space limit reached” copy localized; “GPS_TOO_FAR” is a stable machine string the client translates via schedule.checkinPage.tooFar. |
| All | Settings, space pickers and the override dialog read “Space(s)”, not “Location(s)”. |
Expected vs actual
- After deactivation:
is_active=false,deleted_atstill NULL. - After autocomplete fill:
latitudeandlongitudeboth non-null and within plausible bounds (-90 to 90, -180 to 180). - Tier check: count of
is_active=truelocations in the org against the tier’s cap.