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

1. Business Questions

1.1 Overview

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.

1.2 Implemented Business Questions

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

1.3 Changes from Sprint 1

  • 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.

2. Analytics Pipeline

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
Loading

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/telemetry validates 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 with source = 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 (walkMinutes on every spot and GET /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

3. Architectural Design

3.1 Overall System Architecture

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
Loading

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

3.2 Component Interaction and Communication

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.

A. Android App Architecture (Flutter · MVVM + Ports and Adapters)

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
Loading

Diagram Rationale

  • MVVM: each screen only draws the state of its ViewModel (ChangeNotifier, given to the widgets with provider) 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 into ApiException or NetworkException in one place. A 401 closes the session through onUnauthorized.
  • 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.

B. Android Interaction Sequences

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.

B.1 Login and Session Restore

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
Loading

Diagram Rationale

  • The token is validated against /auth/me when 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_opened once, which BQ3 uses to count active users.

B.2 Live Map and Offline Fallback

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
Loading

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_loaded measures from the moment the level is selected until the grid is painted, with success, errorType, deviceModel, osVersion and platform. A map served from the cache is reported as success: false with errorType: offline_cache.

B.3 Reservation, Countdown and Check-in

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)
Loading

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 expiresAt set 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.

B.4 Find My Car (GPS sensor)

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
Loading

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.

B.5 Campus Full and Nearby Lots

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
Loading

Diagram Rationale

  • Context-aware: campusFull comes 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 DirectionsLauncher port, so the ViewModel does not depend on url_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.

C. iOS App Architecture (SwiftUI · MVVM + Services)

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
Loading

Diagram Rationale

  • MVVM: SwiftUI views only render state and forward user actions. AuthViewModel holds the form state, validates it and publishes user, isLoading and errorMessage, so the views have no networking or storage code.
  • Service layer behind protocols: AuthViewModel depends on the AuthServicing protocol, 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-Backend REST 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 single APIClient. When an authenticated request receives a 401, APIClient posts a sessionExpired notification so the session can be closed in one place.
  • Offline-first reads: BackendParkingService saves 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 as fromCache instead of an error.
  • No third-party dependencies: only Apple frameworks (Foundation, SwiftUI, CoreLocation, Security and UIKit).

D. iOS Login & Session Restore Sequence

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
Loading

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.

E. iOS Live Map & Offline Fallback Sequence

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
Loading

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 == true is drawn as "You".
  • BQ1 instrumentation: durationMs is measured from the moment the level is selected until its spots arrive, and a cached or failed load is reported with success: false and its errorType.

F. Spot Reservation Flow Sequence (both apps)

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
Loading

Diagram Rationale

  • The reservation runs in one transaction. The UPDATE only 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 expiresAt set 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.

G. Occupancy Simulation

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"]
Loading

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.

3.3 Design Patterns and Architectural Tactics

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

Nicolas Bobadilla (iOS) — Security & Service Layer

  • 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 /auth endpoints (register, login, me).

  • Design Pattern: Facade (with dependency injection through the AuthServicing, ParkingServicing and SessionStoring protocols, 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.


4. List of Implemented Features

The occupancy of the spots is simulated by the backend. Reservations, check-ins, sessions, GPS positions and telemetry are real.

4.1 Android App (Flutter)

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).

4.2 iOS App (Swift)

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 and app_opened with 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 with GET /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).

4.3 Shared Backend

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.

5. Ethics Video

⚠️ **GitHub.com Fallback** ⚠️