Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ This repository is a small monorepo. Run each app from its own folder:
- `apps/mobile` - Expo React Native app
- `apps/payment` - optional Stripe-backed payment service

## Documentation Map

- [New developer onboarding](docs/getting-started.md)
- [Central environment variable reference](docs/environment.md)
- [Architecture overview](docs/architecture/overview.md)
- [Mobile architecture](docs/architecture/mobile.md)
- [API architecture](docs/architecture/api.md)
- [Payment architecture](docs/architecture/payment.md)
- [Deployment architecture](docs/architecture/deployment.md)
- [Staging runbook](docs/staging.md)
- [Troubleshooting](docs/troubleshooting.md)
- [OpenSpec workflow](docs/openspec.md)

The local workflow below is Windows PowerShell first because that is the current development environment. macOS/Linux equivalents are mostly the same, except virtual environment activation paths and shell environment-variable syntax.

## Prerequisites
Expand Down
2 changes: 1 addition & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ API_PORT=8000
JWT_SECRET=changeme
ALLOWED_ORIGINS=*
# Local/demo default: keep mock mode unless you intentionally start apps/payment.
PAYMENT_MODE=service
PAYMENT_MODE=mock
PAYMENT_SERVICE_BASE_URL=
# Stripe should redirect to the mobile/frontend experience, not the API host.
# Development/dev client example:
Expand Down
219 changes: 143 additions & 76 deletions apps/api/README.md
Original file line number Diff line number Diff line change
@@ -1,120 +1,187 @@
# ShoeInn API

## Quickstart (Windows PowerShell)
FastAPI backend for authentication, companies, premium care services, booking holds, appointments, provider/company operations, live updates, notifications, payment reconciliation, and demo seeding.

## Requirements

- Python 3.11+
- Docker Desktop with Compose v2
- PostgreSQL 15 locally through `docker compose`
- Optional: Stripe/payment service only when `PAYMENT_MODE=service`

## Quick Start

From the repository root, the easiest Windows workflow is:

```powershell
.\scripts\start-api.ps1
```
Copy-Item .env.example .env

That script starts Postgres, creates `apps/api/.venv`, copies and normalizes `.env`, installs dependencies, runs Alembic migrations, starts Uvicorn on `http://localhost:8000`, and seeds demo data unless `-NoSeed` is passed.

Manual setup:

```powershell
cd .\apps\api
docker compose up -d
py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r ..\..\requirements.txt
pip install -e .
alembic upgrade head
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
Copy-Item .env.example .env
```

> **Postgres credentials**
>
> The local database created by `make up` uses the default Postgres credentials `postgres` / `postgres`. Update your `.env`
> file only if you have customised the database user or password, and make sure the value matches the connection string in
> `docker-compose.yml`. A mismatch (for example `DATABASE_URL=postgresql+psycopg://shoeinn:shoeinn@localhost:5432/shoeinn`
> while docker-compose still provisions `postgres` / `postgres`) will result in `password authentication failed for user` errors
> when running Alembic migrations.
For a host-run API with Docker Postgres, set:

For Windows host + Docker Postgres, use `localhost` in `DATABASE_URL`. The checked-in `.env.example` uses `db`, which only works from inside the Docker network.
```env
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/shoeinn
PAYMENT_MODE=mock
```

If you've previously started the database with a different password, Postgres will keep that credential inside the persisted
volume. You can reset the local database (and remove all data) with:
Run migrations and start:

```powershell
python -m alembic upgrade head
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```

Health checks:

```powershell
Invoke-RestMethod http://localhost:8000/health
Invoke-RestMethod http://localhost:8000/ready
```

`/health` checks process liveness. `/ready` checks database connectivity, migration head, required notification table, payment mode, and single-instance live-event mode.

## Database

`docker-compose.yml` runs only Postgres:

- Host port: `5432`
- Database: `shoeinn`
- User/password: `postgres` / `postgres`

If you previously used different credentials, reset the local volume:

```powershell
docker compose down -v
docker compose up -d
python -m alembic upgrade head
```

The `Makefile` in this folder is useful as a reference, but do not assume `make` exists on Windows PowerShell.
Use `localhost` in `DATABASE_URL` when the API runs on the host. Use `db` only from a container on the Compose network.

## Demo Seed

Default Shelby County market:

Seed demo data:
```powershell
Invoke-RestMethod -Method Post "http://localhost:8000/dev/seed?reset=true"
```
Invoke-RestMethod -Method Post http://localhost:8000/dev/seed

Mt. Juliet market:

```powershell
Invoke-RestMethod -Method Post "http://localhost:8000/dev/seed?reset=true&demo_market=mt_juliet"
```

### Availability projection
`reset=true` clears known demo-market records before recreating the selected market.

Confirmed bookings update the `available_slots` read model so clients can query `/slots` without hitting transactional tables. See [docs/cqrs.md](docs/cqrs.md) for an overview of the hold lifecycle, optimistic concurrency checks, and background cleanup that keeps inventory fresh.
Demo credentials are documented in [docs/getting-started.md](../../docs/getting-started.md).

## Curl examples
Register & login:
```
curl -X POST http://localhost:8000/auth/register -H 'Content-Type: application/json' \
-d '{"email":"a@a.com","password":"Password1!","role":"customer"}'
## Key Endpoints

curl -X POST http://localhost:8000/auth/login -H 'Content-Type: application/json' \
-d '{"email":"a@a.com","password":"Password1!"}'
```
Browse companies:
```
curl http://localhost:8000/companies
```
- `GET /health`
- `GET /ready`
- `POST /auth/login`
- `GET /companies`
- `GET /services`
- `POST /appointments`
- `GET /slots`
- `POST /company/appointments/{appointment_id}/claim`
- `WS /live/ws?token=...`
- `POST /push/tokens`
- `POST /webhooks/payments`
- `GET /payments/return/success`
- `GET /payments/return/cancel`

Discover services across companies:
```
curl "http://localhost:8000/services?city=Austin&query=clean"
```
The endpoint aggregates active services and returns normalized pricing data:

```json
[
{
"id": "SERVICE_ID",
"name": "Basic Clean",
"description": "Quick refresh",
"duration_min": 30,
"price_cents": 1000,
"price": 10.0,
"company": {
"id": "COMPANY_ID",
"name": "Clean Kicks",
"city": "Austin",
"state": "TX",
"postal_code": "73301"
}
}
]
Example login:

```powershell
Invoke-RestMethod -Method Post "http://localhost:8000/auth/login" `
-ContentType "application/json" `
-Body '{"email":"customer@shoeinn.com","password":"Password1!"}'
```
Optional query parameters:

* `query` – fuzzy match against service or company name
* `city`/`state` – filter by company location
* `company_id` – scope to a specific provider
## Payment Modes

Book appointment:
```
curl -X POST http://localhost:8000/appointments -H 'Authorization: Bearer TOKEN' \
-H 'Content-Type: application/json' -d '{"company_id":"ID","type":"pickup",\
"address":{"line1":"1 Main","city":"Austin","state":"TX","postal_code":"73301"},\
"start_time_iso":"2025-08-18T15:30:00-05:00"}'
Local development defaults to mock payments:

```env
PAYMENT_MODE=mock
PAYMENT_SERVICE_BASE_URL=
PAYMENT_MOBILE_REDIRECT_BASE=
```
Claim appointment (company user):

Use service mode only when `apps/payment` is running:

```env
PAYMENT_MODE=service
PAYMENT_SERVICE_BASE_URL=http://localhost:8001
PAYMENT_MOBILE_REDIRECT_BASE=shoeinn://app
```
curl -X POST http://localhost:8000/company/appointments/APP_ID/claim -H 'Authorization: Bearer TOKEN'

For Expo Go return-flow testing:

```env
PAYMENT_MOBILE_REDIRECT_BASE=exp://<YOUR-LAN-IP>:8081/--
```

## Tests
The API starts the payment sync worker only when service mode and `PAYMENT_SERVICE_BASE_URL` are configured and `ENABLE_PAYMENT_SYNC_WORKER` is enabled.

## Workers

Payment sync:

Focused provider appointment claiming and assignment tests:
- Started from `app.main` on API startup.
- Active only in service payment mode with a configured payment service URL.

Notification worker:

```powershell
.\venv\Scripts\python.exe -m pytest tests\test_assignment_claiming.py -q
cd .\apps\api
.\.venv\Scripts\Activate.ps1
python -m app.workers.notification_worker
```

These tests use in-memory SQLite through `tests/conftest.py`, so no external Postgres, migrations, or seed data are required.
The API process can also run the in-process notification dispatcher when `ENABLE_NOTIFICATION_DISPATCHER=true`. In staging Compose, the API has that disabled and `notification-worker` runs as a separate service.

## Workers
Expired booking holds are cleared by explicit utility/test paths; `app.main` does not currently start a dedicated hold cleanup worker.

## Tests

- `app.main` starts the payment sync worker only when `PAYMENT_SERVICE_BASE_URL` is configured.
- The notification worker is manual:
Run all backend tests:

```powershell
python -m app.workers.notification_worker
cd .\apps\api
.\.venv\Scripts\Activate.ps1
python -m pytest tests -q
```

Focused tests:

```powershell
python -m pytest tests\test_assignment_claiming.py -q
python -m pytest tests\test_dev_seed.py -q
python -m pytest tests\test_payment_gateway.py -q
```

- The current docs in `docs/cqrs.md` mention hold cleanup starting automatically, but `app.main` does not currently start that worker.
The backend test suite uses in-memory SQLite through `tests/conftest.py`, so local Postgres, migrations, and seed data are not required for unit/integration tests.

## More Documentation

- [Central environment variable reference](../../docs/environment.md)
- [API architecture](../../docs/architecture/api.md)
- [Deployment architecture](../../docs/architecture/deployment.md)
- [Troubleshooting](../../docs/troubleshooting.md)
12 changes: 6 additions & 6 deletions apps/api/docs/cqrs.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# CQRS and Availability Projection

The booking flow now follows a simple Command/Query Responsibility Segregation pattern:
The booking flow uses a simple Command/Query Responsibility Segregation pattern.

* **Command side** `/appointments` receives booking requests. The handler places (or reuses) an `appointment_holds` row scoped to `company_id`, `service_id`, and `start_time_utc`. It keeps the write inside a single transaction, verifies that an appointment for the same slot does not already exist, and promotes the hold into a confirmed appointment. A unique index on `appointments(company_id, start_time_utc)` protects against double-booking at the database level.
* **Read side** confirmed appointments are projected into the `available_slots` read store. Each write sets `is_available` to `false` and stamps `last_booked_at`. Mobile clients can query `/slots` to retrieve available times quickly without replaying command-side logic.
* **Consistency** – any transient hold failures or uniqueness violations bubble up as `409` responses. Tests simulate dueling customers to ensure optimistic concurrency works and that expired holds are cleared before a follow-up booking succeeds.
- **Command side** - `/appointments` receives booking requests. The handler places or reuses an `appointment_holds` row scoped to `company_id`, `service_id`, and `start_time_utc`. It keeps the write inside one transaction, verifies that an appointment for the same slot does not already exist, and promotes the hold into an appointment. A unique index on `appointments(company_id, start_time_utc)` protects against double-booking at the database level.
- **Read side** - confirmed appointments are projected into the `available_slots` read store. Each write sets `is_available=false` and stamps `last_booked_at`. Mobile clients query `/slots` for available times without replaying command-side logic.
- **Consistency** - transient hold failures or uniqueness violations bubble up as `409` responses. Tests simulate dueling customers to verify optimistic concurrency and expired-hold behavior.

Expired holds are deleted by a background thread started in `app/main.py`. The job polls `appointment_holds` on a configurable cadence (`HOLD_CLEANUP_INTERVAL_SECONDS`) so inventory is released automatically if a client abandons checkout. For deterministic validation you can call `app.utils.holds.clear_expired_holds()` directly in scripts or tests.
Expired holds are cleared by explicit utility paths and tests, not by an automatically started worker in `app/main.py`. For deterministic validation or future worker wiring, call `app.utils.holds.clear_expired_holds()` directly. `HOLD_CLEANUP_INTERVAL_SECONDS` is reserved configuration for a future scheduled cleanup loop.

When introducing new consumers, project their booking events into `available_slots` rather than hitting transactional tables directly. This keeps read latency low while letting the command side evolve independently.
When introducing new read-side consumers, project booking events into `available_slots` rather than hitting transactional tables directly. This keeps read latency low while allowing the command side to evolve independently.
5 changes: 5 additions & 0 deletions apps/mobile/.env.staging.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
#
# Use the staging API URL, not localhost.
EXPO_PUBLIC_API_URL=https://api-staging.example.com
EXPO_PUBLIC_API_BASE_URL=https://api-staging.example.com
EXPO_PUBLIC_APP_ENV=staging
EXPO_PUBLIC_ENABLE_DEMO_LOGINS=true
EXPO_PUBLIC_DEMO_MARKET=shelby
EXPO_PUBLIC_MOBILE_REDIRECT_BASE=shoeinn://payment-return

# Optional for travel-route rendering in staging.
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=replace-me-for-staging
Loading
Loading