diff --git a/PART_2.md b/PART_2.md new file mode 100644 index 0000000..34e726c --- /dev/null +++ b/PART_2.md @@ -0,0 +1,497 @@ +# Part 2: Real-Time Location Updates Backend - Architecture Walkthrough + +This is a system design question: design the backend for real-time location updates for Cymelle delivery trips and driver/rider activity. The system must support 500+ concurrent active trips or deliveries while keeping customer tracking, driver apps, and admin/dispatcher views responsive. + +The current Cymelle backend already has orders and rides. I would keep the existing REST APIs for lifecycle actions such as creating an order, requesting a ride, accepting a ride, completing a ride, and cancelling a ride. I would add a dedicated real-time location subsystem beside that API because location data has a different shape: high-frequency writes, low-latency reads, fan-out to multiple viewers, and short-lived live state. + +--- + +## 1. Requirements and Scale Assumptions + +For 500 active trips: + +- Each driver/rider sends a GPS update every 2-5 seconds. +- At 1 update every 3 seconds, ingestion is roughly `500 / 3 = 167 location updates per second`. +- Each trip may have 1 customer viewer, 1 driver app, and several admin/dispatcher viewers. +- The harder problem is not raw write volume; it is managing many persistent connections and fanning out updates quickly. + +Core requirements: + +- Drivers/riders push latitude, longitude, speed, bearing, accuracy, and timestamp. +- Customers see the live location for their own delivery or ride. +- Admins/dispatchers can monitor multiple active trips. +- The system detects stale drivers and bad GPS data. +- Recent live location is available quickly. +- Historical trails can be stored for audits, dispute resolution, and route playback. + +--- + +## 2. WebSocket vs Polling + +Polling is simpler, but it is a poor fit for real-time trip tracking. + +With polling, customers and admins repeatedly ask "where is this driver?" every few seconds even when the location has not changed. At 500 active trips, if each customer polls every 5 seconds, that is already around 100 requests per second just for customer tracking. Admin dashboards make this worse because one dispatcher may watch many trips. Polling also adds artificial latency because the customer only sees an update on the next poll interval. + +WebSocket is a better fit because it keeps a persistent connection open: + +- Driver/rider app pushes location updates to the server as they happen. +- Customer and admin clients subscribe to a trip and receive updates immediately. +- The server sends only changed data rather than forcing clients to keep asking. +- Connection overhead is lower than repeated HTTP request/response cycles. + +I would use WebSocket for customer/admin delivery of updates. For driver ingestion, WebSocket works well too, but MQTT over WebSocket is also worth considering for mobile apps because it handles unreliable networks nicely and has small packet overhead. For this system, a pragmatic first version is: + +- Driver app to backend: WebSocket location ingestion. +- Backend to customers/admins: WebSocket subscriptions. +- Optional later upgrade: MQTT for driver ingestion if mobile network reliability becomes a major issue. + +REST still remains useful as a fallback: + +- `GET /api/trips/{id}/location/current` for current location fallback. +- `GET /api/trips/{id}/location/history` for route playback. + +--- + +## 3. Proposed Architecture + +```text ++-------------------+ WebSocket/MQTT +----------------------+ +| Driver/Rider App | ---------------------------> | Location Service | +| GPS every 2-5 sec | | Spring WebFlux | ++-------------------+ +----------+-----------+ + | + | + +------------------------------------+------------------+ + | | | + v v v + +--------------------+ +--------------------+ +------------------+ + | Redis | | Kafka / RabbitMQ | | PostgreSQL | + | live location | | location-events | | trip metadata | + | pub/sub fan-out | | durable stream | | orders/rides | + +---------+----------+ +---------+----------+ +------------------+ + | | + | v + | +--------------------+ + | | TimescaleDB / | + | | partitioned table | + | | location history | + | +--------------------+ + | + v + +----------------+----------------+ + | WebSocket subscribers | + | Customers / Admins / Dispatch | + +---------------------------------+ +``` + +In Cymelle terms: + +- PostgreSQL remains the source of truth for `Order`, `Ride`, `AppUser`, and assignment state. +- Redis stores the latest live location for each active ride/delivery. +- Redis Pub/Sub or Redis Streams fans updates across multiple backend nodes. +- Kafka or RabbitMQ stores a durable event stream for history writers and analytics. +- TimescaleDB, or a partitioned PostgreSQL table, stores historical breadcrumbs. + +--- + +## 4. Storage Choices for High-Frequency Location Data + +The system should not write every GPS point directly into the main relational tables used for orders and rides. That would mix high-frequency telemetry with transactional business data and make the primary database harder to tune. + +I would split storage into hot, warm, and cold paths. + +### Hot Path: Redis + +Redis stores the current location for each active trip: + +```text +trip:{tripId}:location = { + driverId, + lat, + lng, + speed, + bearing, + accuracyMeters, + updatedAt, + sequence +} +``` + +Why Redis: + +- Very fast reads for "where is my driver now?" +- TTL can expire stale live state automatically. +- Redis Pub/Sub can broadcast updates between app nodes. +- Redis Geo can support proximity queries like "drivers within 2km". + +I would set a TTL, for example 60-120 seconds, so a driver who disconnects does not look live forever. + +### Warm Path: Event Stream + +Every valid location update is also published as an event: + +```text +topic: location-events +key: tripId or driverId +value: location payload +``` + +Kafka is ideal if the team wants replayable history and high throughput. RabbitMQ is acceptable for a smaller system if the goal is reliable async processing rather than long replay windows. For this prompt, I would pick Kafka for the durable location log because location events are naturally append-only and may be replayed to rebuild route history. + +Partition by `tripId` or `driverId` so updates for the same trip stay ordered. + +### Historical Path: TimescaleDB or Partitioned PostgreSQL + +For route playback and audit trails, I would store downsampled or batched location points in a time-series table: + +```text +location_points + id + trip_id + driver_id + lat + lng + speed + bearing + accuracy_meters + recorded_at +``` + +TimescaleDB is a strong choice because it is PostgreSQL-compatible and optimized for time-series data. If we want fewer moving parts, we can start with PostgreSQL partitioned by date and add TimescaleDB later. + +### Cold Path: Existing PostgreSQL Domain Tables + +The existing `rides` and `orders` tables should store business state: + +- assigned driver +- pickup/dropoff +- requested/accepted/completed/cancelled timestamps +- final route summary +- final distance/duration if needed + +They should not store every GPS point. + +--- + +## 5. Spring Boot Service Layer Structure + +I would add a dedicated `LocationService` module or service inside the backend first, then split it into its own deployable service if scaling pressure justifies it. + +For WebSocket-heavy traffic, I would use Spring WebFlux rather than a traditional blocking Spring MVC controller path. WebFlux is better for many long-lived connections because it uses non-blocking I/O. + +Suggested package structure: + +```text +location/ + config/ + WebSocketConfig + LocationSecurityConfig + + websocket/ + DriverLocationWebSocketHandler + TripSubscriptionWebSocketHandler + WebSocketSessionRegistry + + dto/ + LocationUpdateMessage + LocationSnapshotResponse + SubscribeTripMessage + + service/ + LocationIngestionService + LocationValidationService + LocationAuthorizationService + LocationFanoutService + LocationQueryService + StaleTripDetectionService + + repository/ + RedisLocationRepository + LocationHistoryRepository + + messaging/ + LocationEventProducer + LocationEventConsumer +``` + +### Ingestion Flow + +```text +Driver WebSocket message + | + v +Authenticate session user + | + v +Authorize driver for trip + | + v +Validate location update + | + +--> reject impossible / stale / out-of-bounds data + | + v +Write latest location to Redis + | + v +Publish location event to Kafka + | + v +Fan out to subscribed customer/admin sessions +``` + +Validation should include: + +- required trip ID and driver ID +- driver is assigned to the trip or allowed to service the delivery +- coordinates are within the operating region +- timestamp is not too old or too far in the future +- accuracy is acceptable +- speed jump is physically plausible +- sequence number is newer than the last accepted update + +### Subscription Flow + +```text +Customer/Admin opens tracking screen + | + v +Client opens WebSocket subscription + | + v +Backend authenticates user + | + v +Backend authorizes access to trip + | + v +Backend sends latest Redis snapshot + | + v +Backend streams future updates +``` + +Authorization matters: + +- Customers can subscribe only to their own delivery/ride. +- Drivers can publish only for trips assigned to them. +- Admins and dispatchers can subscribe to all active trips. + +--- + +## 6. Scaling Considerations + +At 500 active trips, message volume is manageable. The bigger scaling concern is WebSocket connection count and fan-out across multiple backend instances. + +### Stateless Location Nodes + +Run multiple Location Service instances behind a load balancer: + +```text + +------------------+ + | Load Balancer | + | WebSocket aware | + +---+----------+---+ + | | + v v + +---------+ +---------+ + | Loc Svc | | Loc Svc | + | Node A | | Node B | + +----+----+ +----+----+ + | | + +-----+------+ + | + v + +-----------+ + | Redis | + | Pub/Sub | + +-----------+ +``` + +WebSocket connections are stateful, but application state should not live only inside one node. Redis holds the latest location, and Pub/Sub broadcasts updates so customers connected to Node B can receive updates from a driver connected to Node A. + +### Load Balancing + +The load balancer must support WebSocket upgrades. Sticky sessions can reduce cross-node chatter, but the system should still work without perfect stickiness because Redis Pub/Sub or Streams carries updates between nodes. + +For driver ingestion, consistent routing by `tripId` is useful but not required if Redis and Kafka are the coordination points. + +### Fan-Out Strategy + +For each update: + +1. Write current location to Redis. +2. Publish to `trip:{tripId}:updates`. +3. Every Location Service node subscribed to that channel checks local WebSocket sessions. +4. Nodes push the update only to clients subscribed to that trip. + +This avoids every client polling the API. + +### Backpressure and Rate Limits + +Drivers should not be allowed to flood updates: + +- Minimum interval, for example one update per second. +- Drop updates that are older than the last accepted sequence. +- Optionally downsample history writes while still updating live Redis state. +- Queue or drop low-priority admin fan-out if a client is slow. + +### Capacity Estimate + +For 500 active trips: + +- 500 driver connections. +- 500 customer tracking connections. +- 10 admins watching many trips. +- Roughly 1,000-1,500 WebSocket connections is realistic. +- Around 100-250 location updates per second depending on update interval. + +This is well within what a small cluster of non-blocking Spring WebFlux nodes, Redis, and Kafka can handle. + +--- + +## 7. Failure Modes to Plan For + +### Driver Loses Network + +Mobile networks are unreliable. Store `lastSeenAt` in Redis for each trip. If no update arrives for 30 seconds, mark the live location as stale and send a stale status to subscribers. + +```text +last update older than 30s -> STALE +last update older than 2m -> OFFLINE +``` + +The frontend should show "last updated 43 seconds ago" rather than leaving a frozen marker with no context. + +### Location Service Node Crashes + +Connected clients lose their WebSocket and reconnect through the load balancer. Because latest location is in Redis, the new node can immediately send the current snapshot after reconnect. + +### Redis Fails + +Redis is on the hot path. Run Redis with high availability using Sentinel or a managed Redis service with failover. If Redis is unavailable: + +- Continue accepting updates if Kafka is available. +- Temporarily degrade live tracking. +- Serve the last persisted history point from the database as a fallback. + +The fallback may be slower and less fresh, but the system should not fail silently. + +### Kafka or Event Broker Lags + +If Kafka lags, live tracking can still work through Redis. The consequence is delayed route history, not broken live customer tracking. Once the consumer catches up, it writes historical points. + +### Database Is Slow + +Do not query PostgreSQL on every location update. Authorization can be cached briefly, for example `trip:{tripId}:assignment`, with invalidation when a ride is completed or cancelled. The database is used for trip metadata and historical queries, not every live coordinate. + +### Bad GPS or Spoofed Location + +Reject or flag suspicious updates: + +- impossible speed +- coordinates outside service area +- timestamp replay +- sequence number rollback +- poor accuracy +- driver not assigned to the trip + +Rejected events can be logged to a dead-letter topic for audit. + +### Slow Subscribers + +If a customer or admin client cannot keep up, do not let that client block ingestion. Keep only the most recent location update per subscribed trip for that connection, and drop intermediate points if needed. + +### Duplicate or Out-of-Order Updates + +Use a monotonically increasing sequence number or client timestamp plus server-received timestamp. Ignore updates older than the latest accepted update for that trip/driver. + +--- + +## 8. Security and Privacy + +Location is sensitive data, so access control has to be strict. + +- Drivers can publish only for assigned active trips. +- Customers can subscribe only to their own active delivery/ride. +- Admin access should be role-gated and audited. +- WebSocket authentication should reuse the same auth/session model as the rest of Cymelle. +- Historical location data should have retention limits. +- Completed trips should stop accepting live updates. + +I would also avoid broadcasting exact driver location after a trip is complete. + +--- + +## 9. API Sketch + +REST endpoints: + +```text +GET /api/trips/{tripId}/location/current +GET /api/trips/{tripId}/location/history?from=...&to=... +GET /api/admin/locations/active +``` + +WebSocket endpoints: + +```text +/ws/location/driver + driver -> server location updates + +/ws/location/trips/{tripId} + server -> customer/admin trip updates + +/ws/location/admin + server -> admin stream for active trips +``` + +Example driver update: + +```json +{ + "tripId": "uuid", + "lat": -1.2921, + "lng": 36.8219, + "speedKph": 34.2, + "bearing": 91.5, + "accuracyMeters": 8.0, + "recordedAt": "2026-06-15T10:30:00Z", + "sequence": 184 +} +``` + +--- + +## Architecture Summary + +```text +Driver/Rider App + | + | WebSocket or MQTT, GPS every 2-5 seconds + v +Spring Boot Location Service + | + +--> validate + authorize update + | + +--> Redis + | latest live location + | stale detection TTL + | pub/sub fan-out + | + +--> Kafka / RabbitMQ + | durable location event stream + | + +--> TimescaleDB or partitioned PostgreSQL + | historical route breadcrumbs + | + +--> Existing PostgreSQL + ride/order/driver assignment metadata + +Redis Pub/Sub + | + v +Customer/Admin WebSocket Subscribers +``` + +My proposed design keeps the existing Cymelle order and ride system as the source of business truth, then adds a real-time location layer optimized for high-frequency GPS data. WebSockets avoid polling overhead, Redis handles live state and fan-out, Kafka gives a durable event stream, TimescaleDB or partitioned PostgreSQL stores route history, and Spring WebFlux keeps the location service efficient under thousands of concurrent WebSocket connections. diff --git a/README.md b/README.md index d97a87f..d72c3d8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,354 @@ # Cymelle -## Local Docker Compose +Cymelle is a commerce, delivery, and ride operations application. Customers can browse products, place orders, request delivery rides, and pay through Paystack. Admins manage products, inventory, orders, users, and rides. Drivers accept and complete delivery rides. -Run the full local development stack with Postgres 18, Spring Boot API, and TanStack/Vite web app: +## Architecture + +The current system is a three-tier application: a React frontend, a Spring Boot API, and PostgreSQL. Paystack is the external payment provider. + +```text ++-----------------------------+ +| Browser | +| Customer / Admin / Driver | ++-------------+---------------+ + | + | HTTP + | http://localhost:3000 + v ++-----------------------------+ +| orders-frontend | +| React + TypeScript | +| TanStack Router + Query | +| Vite dev server | ++-------------+---------------+ + | + | REST API + | VITE_API_BASE_URL + | http://localhost:8080/api + v ++-----------------------------+ +| orders-backend | +| Spring Boot REST API | +| Security + Services + JPA | +| Swagger/OpenAPI | ++--------+--------------------+ + | | + | JDBC | HTTPS + v v ++----------------+ +------------------+ +| PostgreSQL | | Paystack API | +| app data | | payments | ++----------------+ +------------------+ +``` + +Local Docker Compose runs the same shape with three services: + +```text ++------------------------------------------------------------+ +| Docker Compose project: cymelle | +| | +| web: node:24 api: temurin 17 | +| pnpm dev --host 0.0.0.0 ./gradlew bootRun | +| localhost:3000 -------------> localhost:8080 | +| | | | +| v v | +| orders-frontend/ db: postgres:18 | +| bind mount localhost:5432 | +| | | +| v | +| postgres-data volume | ++------------------------------------------------------------+ +``` + +## Project Layout + +- `orders-frontend/`: React web app, route files, API clients, hooks, shared UI, dashboard and storefront screens. +- `orders-backend/`: Spring Boot API, controllers, services, repositories, DTOs, entities, security, OpenAPI, and seed data. +- `compose.yml`: Local development stack for Postgres, API, and web. +- `.env.example`: Local environment variables consumed by Docker Compose. +- `render.yaml`: Render Blueprint for Docker-backed deployment. +- `DEPLOYMENT.md`: Render and Neon deployment notes. +- `PART_2.md`: Real-time location tracking backend design walkthrough. + +## Tech Stack + +### Frontend + +- React 19 +- TypeScript +- Vite +- TanStack Router for file-based routing +- TanStack Query for server state +- TanStack Table and TanStack Form +- Tailwind CSS 4 +- Radix UI, local UI components, Lucide React, and Tabler Icons +- pnpm +- Biome for linting and formatting +- Vitest for tests +- Nitro output for production server builds + +### Backend + +- Java 17 +- Spring Boot 3.5 +- Spring Web +- Spring Security +- Spring Data JPA +- Bean Validation +- PostgreSQL driver +- H2 test dependency +- Springdoc OpenAPI / Swagger UI +- Gradle Kotlin DSL + +### Infrastructure + +- Docker Compose for local development +- PostgreSQL 18 Alpine locally +- Render Docker web services for deployment +- Neon PostgreSQL for deployed database +- Paystack payment integration + +## Domain Model + +```text +AppUser + id, username, displayName, passwordHash, role, enabled + role: ADMIN | CUSTOMER | DRIVER + | + +--> CUSTOMER owns Orders and requested Rides + +--> DRIVER can accept and complete Rides + +--> ADMIN manages products, users, orders, inventory, and rides + +Product + id, name, sku, price, currency, active + | + +-- one-to-one --> InventoryItem + | + +-- many-to-one from --> OrderItem + +InventoryItem + id, product, availableQuantity, reservedQuantity, reorderLevel + status: IN_STOCK | LOW_STOCK | OUT_OF_STOCK + +Order + id, customerName, customer, status, totalAmount, currency, createdAt + status: PENDING | SHIPPED | DELIVERED | CANCELLED + | + +-- one-to-many --> OrderItem + | + +-- optional delivery association --> Ride + | + +-- one-to-one --> Payment + +OrderItem + id, order, product, productName, quantity, unitPrice, lineTotal + stores product name and price snapshot at checkout time + +Ride + id, customer, driver, order, pickupLocation, dropoffLocation + distanceKm, fareAmount, currency, status + status: REQUESTED | ACCEPTED | COMPLETED | CANCELLED + +Payment + id, order, ride, reference, productSubtotal, rideFare + amountMajor, amountSubunits, currency, status + status: PENDING | PAID | FAILED | ABANDONED | REFUNDED +``` + +## Main Application Flows + +### Authentication + +```text +User registers or logs in + | + v +Spring Security authenticates credentials + | + v +Frontend calls /api/auth/me + | + v +Routes and backend endpoints use the user's role +``` + +Roles drive most behavior: + +- Customers can browse, order, pay, request rides, and view their own orders. +- Drivers can see requested rides, accept rides, and complete assigned rides. +- Admins can manage users, products, orders, inventory, and ride status. + +### Product and Inventory + +```text +Admin creates product + | + v +Product stored with SKU, price, currency, active flag + | + v +InventoryItem tracks available quantity and reorder level + | + v +Inventory status is derived from available quantity +``` + +Inventory status is computed: + +- `OUT_OF_STOCK` when available quantity is `0` +- `LOW_STOCK` when available quantity is at or below the reorder level +- `IN_STOCK` otherwise + +### Checkout Quote + +```text +Customer submits cart + optional delivery ride + | + v +Backend validates products and stock + | + v +Backend calculates product subtotal + | + +--> optional fare calculation for delivery ride + | + v +Quote returns productSubtotal, rideFare, grandTotal, currency, expiresAt +``` + +The quote endpoint is useful before Paystack initialization so the customer can see the final total. + +### Order Placement + +```text +Customer places order + | + v +Backend loads active products + | + v +Backend locks inventory rows for update + | + +--> insufficient stock -> request fails + | + v +Inventory is deducted + | + v +Order and OrderItems are created as PENDING + | + +--> optional delivery ride is created as REQUESTED +``` + +Order items store `productName`, `unitPrice`, and `lineTotal`, so historical orders keep their checkout-time product snapshot even if the product changes later. + +### Payment With Paystack + +```text +Customer initializes Paystack checkout + | + v +Backend calculates quote and creates pending order + | + v +Payment row is created with unique reference + | + v +Backend initializes Paystack transaction + | + v +Frontend redirects user to Paystack authorization URL + | + v +Paystack callback/webhook triggers backend verification + | + v +Backend verifies reference, amount, and currency with Paystack + | + +--> success -> Payment becomes PAID + +--> failure -> Payment becomes FAILED / ABANDONED / REFUNDED +``` + +The backend verifies payment server-to-server. The frontend callback is not treated as proof of payment by itself. + +### Order Status + +```text +PENDING -> SHIPPED -> DELIVERED + | + +-> CANCELLED +``` + +Rules in the domain model: + +- Only pending orders can be cancelled by the normal cancellation flow. +- Cancelling restores inventory quantities. +- Final orders cannot be moved to another status. +- Completing an attached ride marks the order as delivered. + +### Ride Status + +```text +REQUESTED -> ACCEPTED -> COMPLETED + | + +-> CANCELLED +``` + +Ride behavior: + +- Customers request rides for their own orders. +- Drivers see requested rides plus rides assigned to them. +- Drivers accept requested rides. +- Drivers complete rides assigned to them. +- Completing a ride also marks the attached order as delivered. +- Admins can cancel or complete rides. + +## API Surface + +All backend endpoints are under `/api`. + +| Area | Endpoints | +| --- | --- | +| Auth | `POST /auth/register`, `POST /auth/login`, `POST /auth/logout`, `GET /auth/me`, `GET /auth/csrf` | +| Products | `GET /products`, `GET /products/{id}`, `POST /products`, `PUT /products/{id}`, `DELETE /products/{id}` | +| Inventory | `GET /inventory`, `GET /inventory/low-stock` | +| Orders | `POST /orders`, `GET /orders`, `GET /orders/{id}`, `DELETE /orders/{id}`, `PATCH /orders/{id}/status` | +| Checkout | `POST /checkout/quote` | +| Paystack | `POST /payments/paystack/initialize`, `POST /payments/paystack/verify/{reference}`, `POST /payments/paystack/webhook` | +| Rides | `GET /rides`, `POST /rides`, `POST /rides/{id}/accept`, `POST /rides/{id}/complete`, `DELETE /rides/{id}`, `PATCH /rides/{id}/status` | +| Users | `GET /users`, `GET /users/customers`, `GET /users/drivers`, `POST /users`, `PUT /users/{id}`, `DELETE /users/{id}` | +| Fare | `GET /fare/calculate` | +| Health | `GET /health` | + +Swagger UI is available locally at `http://localhost:8080/swagger-ui.html`. + +## Seed Data + +Local development seeds demo data when `SEED_DATA=true`. + +Demo users: + +| Role | Username | Password | +| --- | --- | --- | +| Admin | `admin` | `admin123` | +| Driver | `driver` | `driver123` | +| Driver | `driver2` | `driver123` | +| Driver | `driver3` | `driver123` | +| Customer | `customer` | `customer123` | +| Customer | `customer2` | `customer123` | +| Customer | `customer3` | `customer123` | +| Customer | `customer4` | `customer123` | + +Seeded products include accessories and office equipment such as wireless mice, keyboards, cables, stands, webcams, headphones, monitors, docks, backpacks, power banks, drawing tablets, and chairs. + +## Local Development + +### Prerequisites + +- Docker and Docker Compose +- Paystack test keys if you want to test checkout payments + +### Run Everything With Docker Compose ```sh cp .env.example .env @@ -12,17 +358,11 @@ docker compose up Services: - Web: `http://localhost:3000` -- API: `http://localhost:8080/api` +- API base URL: `http://localhost:8080/api` - Swagger UI: `http://localhost:8080/swagger-ui.html` -- Postgres: `localhost:5432`, database/user/password `cymelle` - -Paystack checkout requires test keys in `.env`: - -```sh -PAYSTACK_SECRET_KEY=sk_test_... -PAYSTACK_PUBLIC_KEY=pk_test_... -PAYSTACK_CALLBACK_URL=http://localhost:3000/payments/paystack/callback -``` +- OpenAPI JSON: `http://localhost:8080/api-docs` +- Postgres: `localhost:5432` +- Default database/user/password: `cymelle` / `cymelle` / `cymelle` Useful commands: @@ -34,4 +374,99 @@ docker compose down docker compose down -v ``` -The web service runs Vite from mounted source, so frontend edits reload without rebuilding. Restart `api` after backend code changes. Use `docker compose down -v` only when you want to delete the local Postgres and dependency cache volumes. +The web service mounts `orders-frontend/`, so frontend edits reload through Vite. Restart the `api` service after backend code changes. Use `docker compose down -v` only when you want to delete local Postgres data and dependency cache volumes. + +### Environment Variables + +Copy `.env.example` to `.env`, then adjust values as needed. + +```sh +POSTGRES_DB=cymelle +POSTGRES_USER=cymelle +POSTGRES_PASSWORD=cymelle +POSTGRES_PORT=5432 + +API_PORT=8080 +WEB_PORT=3000 +VITE_API_BASE_URL=http://localhost:8080/api +APP_CORS_ALLOWED_ORIGINS=http://localhost:3000 +SEED_DATA=true + +LOW_STOCK_THRESHOLD=10 + +FARE_BASE_FARE=100.00 +FARE_PER_KM_RATE=50.00 +FARE_MINIMUM_FARE=150.00 +FARE_DEFAULT_SURGE_MULTIPLIER=1.0 +FARE_CURRENCY=KES + +PAYSTACK_SECRET_KEY= +PAYSTACK_PUBLIC_KEY= +PAYSTACK_CALLBACK_URL=http://localhost:3000/payments/paystack/callback +PAYSTACK_WEBHOOK_SECRET= +PAYSTACK_API_BASE_URL=https://api.paystack.co +``` + +For Paystack checkout, set: + +```sh +PAYSTACK_SECRET_KEY=sk_test_... +PAYSTACK_PUBLIC_KEY=pk_test_... +PAYSTACK_CALLBACK_URL=http://localhost:3000/payments/paystack/callback +``` + +If `PAYSTACK_WEBHOOK_SECRET` is empty, the backend uses the Paystack secret key for webhook signature verification. + +### Run Services Manually + +Docker Compose is recommended, but the services can run separately. + +Backend: + +```sh +cd orders-backend +SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/cymelle \ +SPRING_DATASOURCE_USERNAME=cymelle \ +SPRING_DATASOURCE_PASSWORD=cymelle \ +APP_CORS_ALLOWED_ORIGINS=http://localhost:3000 \ +./gradlew bootRun +``` + +Frontend: + +```sh +cd orders-frontend +corepack enable +pnpm install +VITE_API_BASE_URL=http://localhost:8080/api pnpm dev +``` + +## Development Checks + +Backend tests: + +```sh +cd orders-backend +./gradlew test +``` + +Frontend checks: + +```sh +cd orders-frontend +pnpm test +pnpm lint +pnpm check +``` + +## Deployment + +See [DEPLOYMENT.md](./DEPLOYMENT.md) for Render deployment steps. The deployed setup uses: + +- `cymelle-api`: Spring Boot API Docker service +- `cymelle-web`: TanStack/Nitro frontend Docker service +- Neon PostgreSQL through `DATABASE_URL` + +## Real-Time Location Design + +See [PART_2.md](./PART_2.md) for the proposed backend architecture for real-time location updates across delivery and ride trips.