Final Submission ‐ Sprint 2 - Andres-acg-source/Moviles GitHub Wiki
Repositories and project tracking
| Item | Link |
|---|---|
| Android app (Flutter) | https://github.com/Rodri45/Moviles-Dart |
| iOS app (SwiftUI) | https://github.com/nbobadillah/Parkwise |
| Backend | https://github.com/Andres-acg-source/Moviles-Backend |
| Sprint 2 milestone (closed issues) | https://github.com/Andres-acg-source/Moviles/milestone/7?closed=1 |
For Sprint 2 we implemented six business questions from the list we defined in Sprint 1, one per team member. They cover how well the map works underground, how drivers choose and reserve a spot, which features they use and how much demand the campus cannot absorb. We use the types from the course book: Type 1 (App's telemetry), Type 2 (Direct UX improvement), Type 3 (Features analysis), Type 4 (Benefits from data) and Type * (Mixed).
ParkWise is not connected to the university's parking gates or to any external lot, so the occupancy of the spots is simulated by our backend (see section 3.1). Every BQ is answered with data our own system records: the telemetry sent by the apps, the reservations and check-ins made by users and the occupancy snapshots of the simulator.
| ID | Business Question | Type | In Sprint 1? | Justification | Owner | Evidence |
|---|---|---|---|---|---|---|
| BQ1 | What is the loading time of the interactive floor map and the rate of failed loads, by device model, OS version and parking level (P1, P2, P3)? | Type 1 | Yes | P2 and P3 have weak or no signal. Knowing which levels and phones load slowly or fail tells us where to improve the cache and the timeouts. | Andrés Cáceres |
map_loaded event in both apps, GET /analytics/summary
|
| BQ2 | What is the walking time from each available spot, or from the driver's current position, to the parked car or the destination entrance? | Type 2 | Yes | Drivers pick a spot by how far they will have to walk to class. Showing minutes instead of meters answers that directly, and the telemetry tells us how often drivers rely on it and which levels produce the longest walks. | Nicolas Bobadilla |
walkMinutes of every spot, walking_time_viewed event, live map in iOS |
| BQ3 | How many times per month does an active user apply each filter (Available, VIP, Electric, Accessible), and what percentage of users has never used one of them? | Type 3 | Yes | Each filter takes space and code. A filter that is almost never used can be merged with another or removed. | Felipe Sales |
filter_applied and app_opened events from Android, filter_applied from Find Spot in iOS, GET /analytics/summary
|
| BQ4 | What percentage of reservations are never completed with a check-in before expires, and how does that no-show rate vary by level and time of day | Type 4 | Yes | No-shows create phantom capacity: spots appear reserved while sitting physically empty. Measuring this by level and hour lets administrators recalibrate hold-time windows and auto-release rules, directly improving the accuracy of the availability data ParkWise relies on | Julián Bohórquez | Reservations and check-ins, GET /reservations/me and GET /reservations/active
|
| BQ5 | Given the predicted occupancy and the no-show rate per level and time slot, what reservation window and how many held spots maximize fulfilled reservations? | Type * (3 and 4) | Yes (changed) | It balances guaranteed spots against spots that stay empty because the driver never arrived. | Fabio Higuera | Reservations and snapshots, GET /analytics/summary
|
| BQ6 | How far in advance should this specific user reserve so the reservation is still valid on arrival, based on their own travel history? | Type 2 | Yes | It prevents expired reservations and spots held longer than needed. | Rodrigo García |
GET /recommendations/lead-time, Reserve screen in Android |
- BQ4 replaces Off-Campus Redirect Value, which is also on our Sprint 1 list. That question needs to know whether a redirected driver actually parked in a nearby lot, and we are not connected to any lot. Unmet demand only needs data our backend already records.
- BQ5 replaces Reported Versus Verified Occupancy, also on our Sprint 1 list. Without gate counts, the reported and the verified values would come from the same backend and the difference would always be zero. We also removed "overstay" from the wording, because without gates there is no reliable exit time.
Both apps send telemetry events, and the backend records reservations, check-ins, unmet-demand queries and the occupancy snapshots of the simulator while it serves the apps. Everything is stored in one PostgreSQL database, and each BQ is computed with a SQL query when its endpoint is called. The Type 2 answers go back to the driver inside the app, and the others are returned by the analytics summary.
flowchart TD
subgraph Collect["1. Collection (phone)"]
direction TB
Droid["Android app<br/>TelemetryClient"]
IOS["iOS app<br/>TelemetryService"]
Queue[("Offline queue<br/>max 200 events")]
end
subgraph Ingest["2. Ingestion (Moviles-Backend)"]
direction TB
Tel["POST /telemetry"]
Ops["/levels and<br/>/reservations"]
Sim["Occupancy<br/>simulator"]
end
subgraph Store["3. Storage (PostgreSQL on Neon)"]
direction TB
Events[("telemetry_events")]
Res[("reservations")]
Unmet[("unmet_demand")]
Snap[("occupancy_snapshots")]
end
subgraph Agg["4. Aggregation (one SQL query per BQ)"]
direction TB
Q1["BQ1 load time"]
Q3["BQ3 filter usage"]
Q4["BQ4 unmet demand"]
Q5["BQ5 reservation window"]
Q2["BQ2 walking minutes"]
Q6["BQ6 lead time"]
end
subgraph Use["5. Consumption"]
direction TB
Team["Team<br/>GET /analytics/summary"]
Driver["Driver<br/>inside the app"]
end
Collect --> Ingest --> Store --> Agg --> Use
Diagram Rationale
- Collection: both apps keep the events in a local queue of at most 200 events (UserDefaults in iOS, Hive in Android) and send them when there is a connection, so the events produced underground are not lost.
- Ingestion:
POST /api/v1/telemetryvalidates each event and saves the user taken from the JWT when there is one. The levels and reservations endpoints record their own data while they serve the apps, and the simulator saves one snapshot per level every 15 minutes. Simulated records are marked withsource = sim. - Storage and aggregation: one PostgreSQL database on Neon for both platforms, and one SQL query per BQ. With our volume, a separate warehouse or batch jobs would only add infrastructure.
- Consumption: BQ1, BQ3, BQ4 and BQ5 are returned to the team by
GET /analytics/summary. BQ2 and BQ6 are Type 2, so their answer goes back to the driver inside the app (walkMinuteson every spot andGET /recommendations/lead-time). - Privacy: the summary requires a session and only returns aggregated values. User ids never leave the backend, and the zone of BQ4 is rounded on the phone before it is sent.
| BQ | Data collected | How it is computed |
|---|---|---|
| BQ1 |
map_loaded (levelCode, durationMs, success, errorType, deviceModel, osVersion, platform) |
Loads, average and 90th percentile of the load time and failure rate, by device, OS and level |
| BQ2 | The walkMinutes value that Moviles-Backend returns for every spot in GET /api/v1/levels/:code/spots (computed from the spot-to-building distance at 1.3 m/s plus one minute per underground level), combined with the phone's GPS (CoreLocation) and the parked car coordinates saved on the device. Every time the driver opens a spot on the live map and sees its walking time, the iOS app sends a walking_time_viewed event (spotCode, levelCode, minutes, source) to POST /api/v1/telemetry; events are queued on the device while there is no signal. The backend aggregates them in the walkingTime section of GET /api/v1/analytics/summary (views and average minutes, total and by level). |
Minutes shown on the live map; views and average minutes, total and by level |
| BQ3 |
filter_applied (filter) and app_opened, with the user from the JWT |
Uses per filter, uses per active user and percentage of active users who never used each filter in the month |
| BQ4 | Position rounded to two decimals (about 1 km) sent with GET /levels
|
When every level is full, one record per user and 15-minute slot; count per weekday, slot and zone |
| BQ5 | Reservations of the last 28 days (real and simulated) and occupancy snapshots | No-show rate, 90th percentile of the check-in time, recommended window (10 to 30 minutes) and spots to hold, per level and slot |
| BQ6 | The user's own reservations | 80th percentile of the time from reserving to checking in, compared with the 15-minute hold |
ParkWise follows a client-server architecture. The Android app (Flutter) and the iOS app (SwiftUI) both follow MVVM and use the same REST backend, Moviles-Backend (Node.js, Express and TypeScript), deployed on Render. The backend has three layers: routes that validate the request and the JWT, services with the business rules, and a PostgreSQL database on Neon. Since both apps use the same database, a spot reserved on an iPhone appears reserved on an Android phone.
The parking data is simulated. The backend includes an occupancy simulator that fills and frees the spots of drivers who do not use the app, following an hourly profile for each level and weekday. Everything a ParkWise user does is real: a reservation blocks the spot for everyone, the check-in ("I parked") marks it as occupied, and leaving or letting the reservation expire frees it again. The simulator never touches a spot that belongs to a user, and the rest of the backend reads occupancy through an OccupancySource interface, so real gate data could replace it later.
flowchart LR
Android["Android app<br/>Flutter, MVVM"] -->|HTTPS + JSON| Routes
IOS["iOS app<br/>SwiftUI, MVVM"] -->|HTTPS + JSON| Routes
subgraph Backend["Moviles-Backend on Render"]
Routes["Routes<br/>+ JWT"] --> Services["Services"]
Services -.->|OccupancySource| Sim["Occupancy<br/>simulator"]
end
Services --> DB[("PostgreSQL<br/>on Neon")]
Sim --> DB
Diagram Rationale
- MVVM in both apps: views only draw the state of their ViewModel, so the logic can be tested without a screen and both platforms share the same structure.
- One shared backend: accounts, reservations and occupancy are the same on iOS and Android, and it is the single place where the analytics data is collected.
- Stateless backend: all the data lives in PostgreSQL, so the service can restart or be redeployed without losing anything, and the database enforces the rule that a spot has only one owner.
- Polling instead of push: while the map is open, the apps ask for the level every 5 seconds. Parking changes in minutes, so this is simpler than keeping a socket open.
Main endpoints of the backend
| Endpoint | Purpose |
|---|---|
/auth/register, /auth/login, /auth/me
|
Accounts and session with a JWT valid for 7 days |
/buildings, /levels, /levels/:code/spots
|
Destination buildings, counts per level with campusFull, and spots with status, filters and walkMinutes
|
/reservations, /reservations/:id/check-in, /reservations/:id/release
|
Reserve (201 or 409), "I parked", and cancel or "I left" |
/reservations/active, /reservations/me, /vehicle
|
Current reservation, history and where the car is parked |
/predictions, /recommendations/level, /recommendations/lead-time
|
Occupancy forecast, best level for the arrival time and BQ6 |
/nearby-lots |
Lots near campus, shown when the campus is full |
/telemetry, /analytics/summary
|
App events, and the answers of BQ1 to BQ5 |
/sim/scenario, /sim/reset
|
Demo only, protected with a key: force the occupancy, for example to show the campus full |
The components communicate over HTTP with JSON. Without connection (levels P2 and P3), each app shows the last copy it saved on the device and keeps its telemetry in the local queue until the network returns.
Repository: https://github.com/Rodri45/Moviles-Dart
flowchart TD
subgraph Views["View Layer (Flutter widgets)"]
AuthViews["Login · Register<br/>Profile"]
ParkViews["Home · Level map<br/>Find a spot"]
ResViews["Reserve · Find my car<br/>No spots · Offline"]
end
subgraph VMs["ViewModel Layer (ChangeNotifier + Provider)"]
AuthVM["AuthViewModel<br/>ProfileViewModel"]
ParkVM["HomeViewModel<br/>LevelMapViewModel<br/>FindSpotViewModel"]
ResVM["ReserveViewModel<br/>FindMyCarViewModel<br/>NoSpotsViewModel<br/>OfflineViewModel"]
end
subgraph Domain["Domain Layer (ports)"]
AuthPort["AuthRepository"]
ParkPort["ParkingRepository<br/>Telemetry"]
ResPort["ReservationRepository<br/>VehicleLocator"]
DevPort["LocationProvider<br/>ConnectivityPort<br/>DirectionsLauncher"]
end
subgraph Infra["Infrastructure Layer (adapters)"]
HttpAuth["HttpAuthRepository<br/>SessionStore"]
HttpData["HttpParkingRepository<br/>HttpReservationRepository<br/>HttpVehicleLocator<br/>TelemetryClient"]
DevAdapters["GeolocatorLocationProvider<br/>ConnectivityPlusAdapter<br/>UrlLauncherDirections"]
end
Secure[("Secure storage<br/>JWT")]
Api["ApiClient<br/>+ CircuitBreaker"]
Hive[("Hive<br/>cache, car, events")]
Phone["GPS · Network<br/>Google Maps"]
Backend["Moviles-Backend"]
AuthViews --> AuthVM --> AuthPort
ParkViews --> ParkVM --> ParkPort
ResViews --> ResVM --> ResPort
ResVM --> DevPort
AuthPort -. implemented by .-> HttpAuth
ParkPort -. implemented by .-> HttpData
ResPort -. implemented by .-> HttpData
DevPort -. implemented by .-> DevAdapters
HttpAuth --> Secure
HttpAuth --> Api
HttpData --> Api
HttpData --> Hive
DevAdapters --> Phone
Api -->|HTTPS / JSON| Backend
Diagram Rationale
- MVVM: each screen only draws the state of its ViewModel (
ChangeNotifier, given to the widgets withprovider) and forwards the driver's actions. The screens have no HTTP, storage or GPS code. - Ports and adapters: the ViewModels depend on interfaces of the domain layer (ports), never on a package. Each adapter wraps one technology (HTTP, Hive,
geolocator,connectivity_plus,url_launcher), so it can be replaced without touching the ViewModels, and the tests use the mocks of the visual prototype, which implement the same ports. - One composition root: every implementation is created in
app/dependencies.dart.AppDependencies.mock()builds the same app with the mock data. - One network client: every HTTP adapter goes through
ApiClient, which adds the JWT, applies the 4 second timeout and the circuit breaker, and turns errors intoApiExceptionorNetworkExceptionin one place. A 401 closes the session throughonUnauthorized. - Storage by sensitivity: the JWT goes to
flutter_secure_storage(encrypted by Android), while the cached levels and spots, the open reservation, the parked car and the telemetry queue go to Hive.
These sequences show how the components of diagram A work together in the main flows of the Android app. Each one covers requirements of section 4.1.
sequenceDiagram
autonumber
actor Driver
participant Gate as AuthGate
participant VM as AuthViewModel
participant Repo as HttpAuthRepository
participant Store as SessionStore
participant API as Moviles-Backend
Gate->>VM: restoreSession() on launch
VM->>Repo: cachedUser()
Repo->>Store: read JWT + user
alt Token saved
VM->>Repo: me()
Repo->>API: GET /auth/me (Bearer JWT)
alt 200 OK
API-->>Repo: user
Repo-->>VM: AppUser
VM-->>Gate: authenticated → Home
else 401 Invalid token
Repo->>Store: clear session and cache
VM-->>Gate: unauthenticated → Login
else No connection
VM-->>Gate: cached user → Home (offline)
end
else No token
VM-->>Gate: unauthenticated → Login
end
Driver->>VM: Sign in (email, password)
VM->>Repo: login()
Repo->>API: POST /auth/login
API-->>Repo: token + user
Repo->>Store: save JWT + user
VM-->>Gate: authenticated → Home
Diagram Rationale
- The token is validated against
/auth/mewhen the app opens, so an expired JWT sends the driver to login once instead of failing on every request. - Without connection the app opens with the saved user (
offlineSession), because P2 and P3 have no signal and the driver still needs the saved data. - On a 401 or a logout the JWT and the Hive cache are deleted, so the next account does not see the previous reservation or parked car.
- After a successful login or restore the app sends
app_openedonce, which BQ3 uses to count active users.
sequenceDiagram
autonumber
actor Driver
participant VM as LevelMapViewModel
participant Repo as HttpParkingRepository
participant Api as ApiClient
participant Hive as Hive cache
participant API as Moviles-Backend
participant Tel as TelemetryClient
Driver->>VM: Opens level P2
VM->>Repo: getSpots("P2", destination)
Repo->>Api: GET /levels/P2/spots
alt Backend answers
Api->>API: request (4 s timeout)
API-->>Api: 200 spots
Api-->>Repo: spots
Repo->>Hive: save copy with time
Repo-->>VM: live spots
else No signal or circuit open
Api-->>Repo: network error
Repo->>Hive: read last copy
Hive-->>Repo: spots + time
Repo-->>VM: cached spots
end
VM-->>Driver: grid (offline banner if cached)
VM->>Tel: map_loaded (BQ1)
loop Every 5 s while the map is visible
VM->>Repo: getSpots("P2")
Repo-->>VM: updated spots
end
Diagram Rationale
- Every request has a 4 second timeout. After three network failures in a row the circuit breaker opens for 30 seconds and the repository answers at once from the Hive cache. HTTP errors like 409 do not count as failures.
- The offline banner shows the time of the saved copy, and the data reloads by itself when the network returns (
ConnectivityAware). - The map refreshes every 5 seconds only while it is visible and the app is in the foreground.
- BQ1:
map_loadedmeasures from the moment the level is selected until the grid is painted, withsuccess,errorType,deviceModel,osVersionandplatform. A map served from the cache is reported assuccess: falsewitherrorType: offline_cache.
sequenceDiagram
autonumber
actor Driver
participant VM as ReserveViewModel
participant Repo as HttpReservationRepository
participant API as Moviles-Backend
participant GPS as LocationProvider
participant Car as HttpVehicleLocator
VM->>Repo: leadTime()
Repo->>API: GET /recommendations/lead-time
API-->>VM: best time to reserve (BQ6)
Driver->>VM: Confirm spot P1-B-01
VM->>Repo: create(spotId)
Repo->>API: POST /reservations
alt 201 Created
API-->>Repo: reservation + expiresAt
Repo-->>VM: reservation (copy in Hive)
VM-->>Driver: countdown from expiresAt
else 409 Conflict
API-->>Repo: 409
Repo-->>VM: error
VM-->>Driver: "Spot taken, choose another"
end
opt 3 minutes left
VM->>GPS: current position
GPS-->>VM: distance to campus
VM-->>Driver: warning if more than 1 km away
end
Driver->>VM: I parked
VM->>Repo: checkIn(id)
Repo->>API: POST /reservations/:id/check-in
API-->>Repo: fulfilled
VM->>GPS: current position
VM->>Car: saveLocation(level, spot, position)
Diagram Rationale
- BQ6: the Reserve screen shows the best time to reserve for this driver, computed by the backend from their own reservations.
- The countdown uses the
expiresAtset by the server, not a local timer, so it is the same on both apps and stays correct if the app is closed. - The backend answers 409 for different reasons, and the ViewModel shows a specific message for each one: spot taken, reservation already active or reservation expired.
- Context-aware: with 3 minutes left, if the location permission was already granted and the driver is more than 1 km from campus, the app warns that the hold is about to expire. It never asks for the permission in the middle of the countdown.
- At check-in the phone saves the level, the spot and the GPS position of the car, because inside P2 and P3 the GPS may not work later. The open reservation is also kept in Hive for the Offline screen.
sequenceDiagram
autonumber
actor Driver
participant VM as FindMyCarViewModel
participant Car as HttpVehicleLocator
participant API as Moviles-Backend
participant Hive as Hive cache
participant GPS as LocationProvider
Driver->>VM: Opens Find my car
VM->>Car: getParkedVehicle()
Car->>API: GET /vehicle
alt Backend answers
API-->>VM: level, spot and zone
else No signal
Car-->>VM: network error (offline)
end
VM->>Car: savedLocation()
Car->>Hive: read position saved at check-in
Hive-->>VM: car position
VM->>GPS: checkAccess()
opt Permission not granted
VM-->>Driver: card explaining why GPS is needed
Driver->>VM: Allow location
VM->>GPS: requestAccess()
end
alt GPS signal
loop Each new position
GPS-->>VM: my position
VM-->>Driver: distance and walking minutes
end
else No signal underground
VM->>GPS: lastKnownPosition()
VM-->>Driver: last position, level and spot
end
Diagram Rationale
- Two sources: the level and spot come from the backend (
GET /vehicle) and the position comes from the phone. If one of them fails, the other one still helps the driver. - The permission is explained in a card before the system dialog appears, and if it was denied forever the card opens the phone settings.
- The live position comes from a stream, and the distance and walking minutes (1.3 m/s) are computed on the phone, so they work without network.
- Underground the GPS may never answer (5 second limit), so the app uses the last known position and says so on screen.
- The saved position is deleted when the backend says there is no parked car, and it is ignored if its level and spot do not match, because the same spot code exists on several levels.
sequenceDiagram
autonumber
actor Driver
participant Home as HomeViewModel
participant NS as NoSpotsViewModel
participant Repo as HttpParkingRepository
participant API as Moviles-Backend
participant Maps as DirectionsLauncher
Home->>Repo: getLevels(zone)
Repo->>API: GET /levels?zone=4.60,-74.07
API-->>Home: campusFull = true (BQ4 record)
Home-->>Driver: opens No campus spots
NS->>Repo: getNearbyLots()
Repo->>API: GET /nearby-lots
API-->>NS: nearby lots
NS-->>Driver: lots sorted by walking minutes
alt Navigate
Driver->>NS: Navigate to the closest lot
NS->>Maps: openDirections(lot)
Maps-->>Driver: route in Google Maps
else Notify me
Driver->>NS: Notify me
loop Every 30 s while the campus is full
NS->>Repo: getLevels()
end
NS-->>Driver: "A spot just opened up"
end
Diagram Rationale
- Context-aware:
campusFullcomes from the backend. Home opens the No campus spots screen once, when the campus becomes full. - BQ4: the zone is rounded on the phone to two decimals (about 1 km) before it is sent, and the backend records the unmet demand only when every level is full.
- The lots are sorted by walking minutes and the closest one is highlighted. Directions open in Google Maps through the
DirectionsLauncherport, so the ViewModel does not depend onurl_launcher. - Notify me checks the campus every 30 seconds only while it is on and the app is open. A copy from the cache is ignored because it cannot show that a spot was freed, and the check stops when a spot opens or the driver logs out.
Repository: https://github.com/nbobadillah/Parkwise
flowchart TD
subgraph Views["View Layer (SwiftUI)"]
LoginView["LoginView / RegisterView"]
ProfileView["ProfileView"]
FeatureViews["Home · Map · FindSpot · FindCar · Reserve"]
end
subgraph ViewModels["ViewModel Layer (@MainActor ObservableObject)"]
AuthVM["AuthViewModel"]
HomeVM["HomeViewModel"]
MapVM["ParkingMapViewModel"]
end
subgraph Services["Service Layer (behind protocols)"]
AuthService["BackendAuthService (AuthServicing)"]
ParkingService["BackendParkingService (ParkingServicing)"]
Telemetry["TelemetryService (offline queue)"]
APIClient["APIClient (URLSession)"]
Cache["ResponseCache"]
SessionStore["KeychainSessionStore"]
LocationService["LocationService (CoreLocation)"]
ParkedCarStore["ParkedCarStore"]
end
subgraph Device["Device"]
Keychain[("Keychain - JWT")]
Defaults[("UserDefaults - user, parked car, pending events")]
Caches[("Caches - levels & spots JSON")]
GPS["GPS sensor"]
end
Backend["Moviles-Backend (Node.js / Express REST + JWT + PostgreSQL)"]
LoginView --> AuthVM
ProfileView --> AuthVM
FeatureViews --> HomeVM
FeatureViews --> MapVM
AuthVM --> AuthService
HomeVM --> ParkingService
MapVM --> ParkingService
MapVM --> Telemetry
AuthService --> APIClient
AuthService --> SessionStore
ParkingService --> APIClient
ParkingService --> Cache
Telemetry --> APIClient
SessionStore --> Keychain
SessionStore --> Defaults
Cache --> Caches
Telemetry --> Defaults
APIClient -->|HTTPS / JSON| Backend
APIClient -.->|401 → sessionExpired| AuthVM
FeatureViews --> LocationService
FeatureViews --> ParkedCarStore
LocationService --> GPS
ParkedCarStore --> Defaults
Diagram Rationale
- MVVM: SwiftUI views only render state and forward user actions.
AuthViewModelholds the form state, validates it and publishesuser,isLoadinganderrorMessage, so the views have no networking or storage code. - Service layer behind protocols:
AuthViewModeldepends on theAuthServicingprotocol, not on a concrete class. We replaced Firebase with our own backend without touching the views or the ViewModel, and a mock service can be injected for tests. - One shared backend: the iOS app and the Android (Flutter) app use the same
Moviles-BackendREST API, so accounts and data are shared between platforms. - Sensitive data stays on the device in secure storage: the JWT goes to the Keychain, never to UserDefaults.
- Every screen with logic has its own ViewModel (
HomeViewModel,ParkingMapViewModel), and every request goes through the singleAPIClient. When an authenticated request receives a 401,APIClientposts asessionExpirednotification so the session can be closed in one place. - Offline-first reads:
BackendParkingServicesaves every successful levels and spots response to a JSON file in the Caches folder with its date. If the network fails (P2/P3 have no signal), it returns that copy marked asfromCacheinstead of an error. - No third-party dependencies: only Apple frameworks (Foundation, SwiftUI, CoreLocation, Security and UIKit).
sequenceDiagram
autonumber
actor Driver
participant App as SmartParkingApp
participant VM as AuthViewModel
participant S as BackendAuthService
participant K as Keychain
participant API as Moviles-Backend
App->>VM: restoreSession() on launch
VM->>S: restoreSession()
S->>K: read saved JWT + user
alt Token found
S->>API: GET /api/v1/auth/me (Bearer JWT)
alt 200 OK
API-->>S: { id, email, name }
S-->>VM: AppUser → Home
else 401 Invalid token
S->>K: clear session
S-->>VM: nil → LoginView
else No connection (underground)
S-->>VM: cached AppUser → Home (offline)
end
else No token
S-->>VM: nil → LoginView
end
Driver->>VM: Sign in (email, password)
VM->>S: signIn()
S->>API: POST /api/v1/auth/login
API-->>S: { token, user }
S->>K: save JWT + user
S-->>VM: AppUser → Home
Diagram Rationale
- Session restore validates the token against the server (
/auth/me), so an expired or invalid JWT logs the user out instead of failing later on every request. - Offline tolerance: if there is no connection when the app opens (levels P2/P3 have no signal), the app keeps the cached user instead of sending the driver back to the login screen.
- Errors are mapped once in the service (401 → wrong credentials, 409 → email already registered, network → no connection), so the views only show a message.
sequenceDiagram
autonumber
actor Driver
participant View as ParkingMapView
participant VM as ParkingMapViewModel
participant S as BackendParkingService
participant C as ResponseCache
participant API as Moviles-Backend
participant T as TelemetryService
Driver->>View: Opens level P2
View->>VM: .task(id: "P2") → run(levelCode: "P2")
VM->>S: spots(level: "P2")
S->>API: GET /api/v1/levels/P2/spots (Bearer JWT)
alt 200 OK
API-->>S: [Spot] with status, mine, walkMinutes
S->>C: save copy with date
S-->>VM: Fetched(fromCache: false)
else No connection (underground)
S->>C: load last copy
C-->>S: cached spots + date
S-->>VM: Fetched(fromCache: true)
end
VM-->>View: zones (free / reserved / taken / you)
VM->>T: map_loaded (levelCode, durationMs, success, deviceModel, osVersion, platform)
loop Every 5 seconds while the view is visible
VM->>S: spots(level: "P2")
S-->>VM: updated spots
VM-->>View: refreshed grid
end
Driver->>View: Leaves the map
View-->>VM: task cancelled → refresh stops
Diagram Rationale
- Live data with a bounded cost: the map refreshes every 5 seconds only while it is on screen. SwiftUI's
.task(id:)cancels the loop when the driver leaves the map or changes level, so the app never polls in the background. - The grid is built from the backend spots: they are grouped by zone and laid out in rows of six, keeping the original design.
mine == trueis drawn as "You". - BQ1 instrumentation:
durationMsis measured from the moment the level is selected until its spots arrive, and a cached or failed load is reported withsuccess: falseand itserrorType.
sequenceDiagram
autonumber
actor Driver
participant App as App (Android or iOS)
participant API as Moviles-Backend
participant DB as PostgreSQL
Driver->>App: Reserve spot P1-B-01
App->>API: POST /reservations
API->>DB: Update the spot only if it is free
alt The spot was free
API->>DB: Insert reservation (15 min)
API-->>App: 201 + expiresAt
App-->>Driver: Countdown
else Someone took it first
API-->>App: 409 Conflict
App-->>Driver: "Spot taken, choose another"
end
alt Driver arrives
Driver->>App: I parked
App->>API: POST /reservations/:id/check-in
API->>DB: Reservation fulfilled
else Countdown reaches zero
Note over API,DB: The simulator expires it and frees the spot
end
Diagram Rationale
- The reservation runs in one transaction. The
UPDATEonly changes the spot if it is still free, so when two requests arrive at the same time only one gets 201 and the other gets 409. Two unique indexes back this up: one active reservation per user and one open reservation per spot. - The countdown uses the
expiresAtset by the server, so it is the same on both platforms and stays correct if the app is closed. - The check-in replaces the gate sensor we do not have, and its time is the arrival time used by BQ5 and BQ6.
flowchart LR
Tick["Every 60 s"] --> Expire["Expire old<br/>reservations"]
Expire --> Target["Target per level<br/>(hourly profile)"]
Target --> Adjust["Occupy or free<br/>sim spots"]
Adjust --> Res["Simulated<br/>reservations"]
Res --> Snap["Snapshot<br/>every 15 min"]
Diagram Rationale
- Only the drivers who do not use ParkWise are simulated, and the simulator never overwrites a reservation or a check-in made from the apps.
- The profile follows a university day in Bogotá time (about 95% between 8:00 and 11:00, lower at lunch, at night and on weekends). P1 fills first.
- On the first start, the simulator generates four weeks of history, so the forecast and BQ5 have data from day one.
| Team Member | Tactic and Quality Attribute | Design Pattern | Layer | Why |
|---|---|---|---|---|
| Rodrigo García (Android) | Modifiability: use an intermediary and defer binding | Adapter | Infrastructure (Android) and backend services | The ViewModels use the LocationProvider port instead of the geolocator plugin, and the backend reads occupancy through OccupancySource instead of the simulator, chosen by configuration at startup. A fake GPS or real gate data can replace them without touching the code that uses them. See diagram A. |
| Andrés Cáceres (Android) | Performance and availability: maintain multiple copies of data | Repository | Repository layer (Android) | P2 and P3 have no signal. ParkingRepository saves every answer in Hive with its time and returns that copy when the backend cannot be reached, so the ViewModels never know where the data came from. See diagram B.2. |
| Fabio Higuera (Android) | Availability: detect faults with a timeout and recover | Circuit Breaker | Network client (Android) | Underground, every request would wait for its timeout. After three failures in a row the breaker stops calling the backend for 30 seconds, so the app answers at once from the cache. See diagram B.2. |
| Nicolas Bobadilla (iOS) | Security: Authenticate actors (JWT) & protect stored credentials (Keychain) | Facade | Service Layer (iOS) |
Why: BackendAuthService, BackendParkingService and LocationService expose a few simple methods (signIn, restoreSession, levels, spots, recommendedLevel, start, walkingMinutes) and hide URLSession, the Keychain, the disk cache and CLLocationManager. The views and ViewModels don't depend on those frameworks, and the implementation can change, as it did from Firebase to our backend, without touching the UI. |
| Julián Bohórquez (iOS) | Correctness under concurrency: resource locking | Observer | ViewModel layer (iOS) | is implemented so AuthViewModel can react immediately when the API client reports an expired session. It signs the user out on the main actor, keeping authentication state and the UI in sync. |
| Felipe Sales (iOS) | Availability: graceful degradation without network | Strategy | ViewModel layer (iOS, Find Spot) | Each filter is an interchangeable strategy (SpotFilterStrategy), so adding or removing one does not change the search code. The search also uses the cached backend response when the network is unavailable, while the app displays an offline banner |
-
Layer: Service Layer of the iOS app (
BackendAuthService,BackendParkingService,APIClient,ResponseCache,KeychainSessionStore,TelemetryService,LocationService,ParkedCarStore) -
Architectural Tactic: Security. Users authenticate with a JWT signed by the backend (7-day expiry), passwords are stored as salted scrypt hashes on the server, and the token is kept in the iOS Keychain (
AfterFirstUnlockThisDeviceOnly). -
Architectural contribution: the MVVM Service layer of the iOS app (View → ViewModel → Service → Backend) and the backend
/authendpoints (register,login,me). -
Design Pattern: Facade (with dependency injection through the
AuthServicing,ParkingServicingandSessionStoringprotocols, so mocks can replace them in tests) -
Rationale: One entry point per concern keeps security-sensitive code (tokens, hashing, Keychain) in a single place that can be reviewed and tested, instead of spread across the views.
The occupancy of the spots is simulated by the backend. Reservations, check-ins, sessions, GPS positions and telemetry are real.
| Requirement | Feature | Contribution |
|---|---|---|
| a. Sensor | Find my car with GPS | Rodrigo: GPS adapter with the permission explained first. Andrés: Find my car screen with live distance and walking minutes. Fabio: car position saved at check-in. |
| b. Type 2 BQ | Lead time (BQ6) and walking time | Rodrigo: "Best time to reserve" on the Reserve screen. Andrés: walking minutes on the map and in Find a spot. Fabio: destination building selector. |
| c. Context-aware | Offline mode, campus full and expiring reservation | Rodrigo: warning when the hold has 3 minutes left and the driver is more than 1 km from campus. Andrés: offline mode with the saved copy and its time. Fabio: No campus spots screen with nearby lots, directions in Google Maps and Notify me. |
| d. Smart feature | Forecast and recommendations | Rodrigo: recommended spot (free, closest on foot, matching the filters). Andrés: recommended level for the arrival time. Fabio: occupancy forecast on Home. |
| e. Authentication | Email and password with JWT | Rodrigo: login and register. Andrés: session restored at startup, also without network. Fabio: profile with history, and back to login when the token expires. |
| f. External service | Live spots and reservations on Moviles-Backend | Rodrigo: check-in and "I left". Andrés: levels and spots refreshed every 5 seconds. Fabio: reservation with countdown and the 409 cases. |
Views implemented (Android):
- Rodrigo García: Login and Register (inline validation and backend errors), Find a spot (filters and recommended spot) and Reserve (countdown, check-in and best time to reserve).
- Andrés Cáceres: Home (level summary, destination, forecast and recommended level) and Level map (live grid per zone with the offline banner).
- Fabio Higuera: No campus spots (nearby lots, directions and Notify me), Find my car (distance and walking minutes), Offline (saved copy and open reservation) and Profile (history and sign out).
| Requirement | Feature | Contribution |
|---|---|---|
| a. Sensor | Find My Car with GPS (CoreLocation) | Nicolas: LocationService (location permission, live location, distance and walking time) and ParkedCarStore (saves the car position on the device). Julián: helps users understand why location access is needed and find their car later using GPS. Felipe: FindCarView shows the distance from the user’s current location to the parked car and estimates the walking time to reach it. |
| b. Type 2 BQ | Walking time per spot (BQ2) | Nicolas: walking minutes from the backend shown on the live map and the walking_time_viewed telemetry event, aggregated by the backend. Julián: Destination building selector Lets users choose where they’re going, so the app can show walking times that match their destination. Felipe: Find Spot sorts available spots by walking minutes, helping users identify the spots that require the shortest walk. |
| c. Context-aware | Offline data and campus full | Nicolas: ResponseCache saves levels and spots with their date and serves them as fromCache when there is no signal; the session and pending telemetry events also survive without connection. Julián: CampusFullView Nearby lots in CampusFullView offers nearby parking alternatives when the campus is full. Felipe: NetworkMonitor uses NWPathMonitor to detect connectivity changes, while Find Spot and Find Car reload when the connection is restored and the app shows an offline banner. |
| d. Smart feature | Recommendations and forecast | Nicolas: Home card with GET /recommendations/level; the arrival time defaults to now + 20 minutes, the driver can change it, and the app suggests the level with the lowest predicted occupancy. Julián: forecast from /predictions shows occupancy predictions relevant to the user’s current search, helping them choose a level. Felipe: Find Spot recommends the available spot with the shortest walking time among the spots matching with the selected filter |
| e. Authentication | Email and password with JWT | Nicolas: BackendAuthService, AuthViewModel and KeychainSessionStore; register, login and logout against /auth, session restored on launch with GET /auth/me, and sessionExpired notification on 401. Julián: Clear data on logout: Removes the saved reservation and parked car so another account on the same device cannot see the previous user’s data. Felipe: "Home" displays the authenticated users information, while "Profile" loads and display the users reservation history from the backend. |
| f. External service | Live data and reservations on Moviles-Backend | Nicolas: APIClient and BackendParkingService: levels, spots, buildings, predictions, recommended level and nearby lots from the shared backend, so what Android reserves is visible on iOS. Julián: Reservation countdown and 409 handling: Shows how much time remains and handles conflicts when a spot has already been reserved, avoiding a false booking confirmation. Felipe: Check-in and "I left" update the reservation through the backend and synchronize the car parked state used by "Find Car" |
- Sensor (a): since GPS barely works underground, the position is captured when the driver parks or at the entrance, and inside P2/P3 the app shows the last saved position together with the spot code.
-
Analytics instrumentation: besides BQ2, the iOS app sends
map_loaded(level, duration, success, error type, device model, OS version, platform) for BQ1 andapp_openedwith the session token for BQ3. Telemetry is sent with the token, queued on the device without signal, and retried when the backend answers 429. Felipe: find Spot sends the filter applied event with the selected filter type, BQ3. Julián: rounded zone sent withGET /levels(BQ4).
Views implemented (iOS):
- Nicolas Bobadilla: Login (inline validation, loading state and error banners mapped from the backend), Register (the button stays disabled until the form is valid and the passwords match), Profile (signed-in user and sign out, which clears the Keychain session), Home (live level summary and recommended-level card with the editable arrival time) and Parking map (live spot grid per level, refreshed every 5 seconds while visible, with the walking time of the selected spot).
- Julián Bohórquez: Campus full (real nearby lots from the backend, dropping any availability figures or badges the contract doesn't provide) and Reserve spot (Confirm reservation creates a real reservation, live countdown from the server's expiresAt, distinct messages for both 409 cases, check-in/release actions on the same button, and history pulled from the backend).
- Felipe Sales: Find Spot (filter selection for Available, VIP, Electric and Accessible spots, filtering, recommended spot based on the shortest walking time, search by spot code/zone/level, and results sorted by walking minutes) and Find Car (the parked car location, the current distance to the car, the time walking estimation, level and spot information, and fallback to the locally stored parking position when backend data is unavailable for network disconection).
Repository: https://github.com/Andres-acg-source/Moviles-Backend
PostgreSQL on Neon and deployed it on Render, with:
- Database with versioned migrations and an initial seed: levels P1, P2 and P3 (156 spots), four destination buildings and two nearby lots.
- Security: JWT required in production, passwords hashed with scrypt and rate limits on login, register and telemetry.
- Atomic reservations with expiration after 15 minutes, check-in, release, history and parked vehicle.
- Occupancy simulator, forecast, recommendations and one SQL query per BQ.