docs architecture - AlexandreZanata/Abastevo GitHub Wiki
Status: Definitive
Stack reference: tech-stack.md
Product contract: user-business-logic.md
Engineering contract:.cursor/rules/
This document defines how the ANP Fuel Prices Android app is structured. All code must conform to the layer rules below.
Clean Architecture mapped to four Gradle modules, enforcing unidirectional dependencies from outer layers toward the Domain core.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INTERFACES โ :app โ
โ Jetpack Compose ยท ViewModels ยท Navigation ยท Theme ยท i18n โ
โ Responsibility: deliver UI, map UiState, dispatch user events โ
โ Must NOT: business rules, SQL, HTTP, XLSX parsing โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ calls
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ APPLICATION โ :application โ
โ Use Cases ยท AppError mapping ยท orchestration โ
โ Responsibility: execute UC-001โฆUC-008, enforce flow order โ
โ Must NOT: Android SDK, Room, OkHttp, Compose โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ uses
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DOMAIN โ :domain โ
โ Entities ยท Value Objects ยท Business Rules ยท Events ยท Ports โ
โ Responsibility: all business truth (BR-001โฆBR-015) โ
โ Must NOT: any framework, Android, or I/O โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฒโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ implements ports
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INFRASTRUCTURE โ :data โ
โ Room ยท OkHttp ยท Jsoup ยท XlsxParser ยท WorkManager ยท Repositories โ
โ Responsibility: I/O, persistence, external ANP integration โ
โ Must NOT: Compose, ViewModels, UI logic โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
| Rule | Description |
|---|---|
| R-ARCH-01 | Dependencies point inward only โ :domain has zero module dependencies |
| R-ARCH-02 |
:application depends only on :domain
|
| R-ARCH-03 |
:data depends on :domain and :application (worker orchestration) |
| R-ARCH-04 |
:app depends on :application and :data (for Hilt wiring) |
| R-ARCH-05 | UI never imports :data repository implementations โ only use cases |
| R-ARCH-06 | Domain repository interfaces (ports) live in :domain
|
| R-ARCH-07 | ANP Portuguese strings are normalized to domain enums in :data mappers |
domain/src/main/kotlin/com/anpfuel/domain/
โโโ model/ # Entities & aggregate roots
โ โโโ PriceSurvey.kt
โ โโโ AveragePrice.kt
โ โโโ RetailStation.kt
โ โโโ StationPrice.kt
โโโ valueobject/ # Immutable, self-validating
โ โโโ SurveyWeek.kt # BR-001
โ โโโ FuelProduct.kt
โ โโโ BrazilianState.kt
โ โโโ BrazilianRegion.kt
โ โโโ GeographicScope.kt
โ โโโ PriceAmount.kt
โ โโโ GeoCoordinates.kt # UC-015
โ โโโ Cnpj.kt
โโโ rule/ # Named business rules
โ โโโ SurveyWeekValidationRule.kt # BR-001
โ โโโ FuelProductNormalizationRule.kt # BR-002
โ โโโ GeoDistanceRule.kt # UC-015 (Haversine)
โ โโโ NearestBestPriceStationRule.kt # BR-028
โ โโโ SyncJobConcurrencyRule.kt # BR-015
โโโ state/ # State machines
โ โโโ SyncJobState.kt
โ โโโ DataReadinessState.kt
โโโ event/ # Domain events (past tense)
โ โโโ PriceTableDiscovered.kt
โ โโโ PriceTableDownloaded.kt
โ โโโ PriceTableImported.kt
โ โโโ SyncJobCompleted.kt
โ โโโ CitySelected.kt
โโโ repository/ # Ports (interfaces only)
โ โโโ PriceTableRepository.kt
โ โโโ AveragePriceRepository.kt
โ โโโ StationPriceRepository.kt
โ โโโ MunicipalitySearchRepository.kt
โ โโโ ReverseGeocodeRepository.kt # UC-012
โ โโโ AddressGeocodeRepository.kt # UC-015
โ โโโ UserPreferencesRepository.kt
โโโ exception/
โโโ DomainException.kt
application/src/main/kotlin/com/anpfuel/application/
โโโ usecase/
โ โโโ sync/
โ โ โโโ SyncPriceTablesUseCase.kt # UC-001
โ โ โโโ DownloadStationDetailUseCase.kt # UC-007 subset
โ โโโ price/
โ โ โโโ GetMunicipalityPricesUseCase.kt # UC-005
โ โ โโโ GetPriceHistoryUseCase.kt # UC-006
โ โ โโโ GetStationPricesUseCase.kt # UC-007
โ โโโ vehicle/
โ โ โโโ ListVehiclesUseCase.kt # UC-010
โ โ โโโ SaveVehicleUseCase.kt # UC-010
โ โ โโโ DeleteVehicleUseCase.kt # UC-010
โ โ โโโ GetTankFillCostEstimatesUseCase.kt # UC-011
โ โโโ location/
โ โ โโโ SelectLocationUseCase.kt # UC-003
โ โ โโโ SearchMunicipalityUseCase.kt # UC-004
โ โ โโโ ResolveDeviceLocationUseCase.kt # UC-012
โ โโโ station/
โ โ โโโ BuildStationNavigationQueryUseCase.kt # UC-013
โ โ โโโ FindNearestBestPriceStationUseCase.kt # UC-015
โ โโโ alert/
โ โ โโโ ConfigurePriceDropAlertUseCase.kt # UC-014
โ โ โโโ EvaluatePriceDropAlertsUseCase.kt # UC-014
โ โโโ settings/
โ โ โโโ GetSettingsUseCase.kt # UC-008
โ โ โโโ UpdatePreferencesUseCase.kt # UC-008
โ โ โโโ ClearCacheUseCase.kt # UC-008
โ โโโ onboarding/
โ โโโ CompleteOnboardingUseCase.kt # UC-002
โโโ error/
โ โโโ AppError.kt # Maps to i18n in :app
โโโ dispatcher/
โโโ AppDispatchers.kt # IO / Default abstraction
data/src/main/kotlin/com/anpfuel/data/
โโโ local/
โ โโโ AnpFuelDatabase.kt
โ โโโ dao/
โ โ โโโ SurveyWeekDao.kt
โ โ โโโ AveragePriceDao.kt
โ โ โโโ StationPriceDao.kt
โ โ โโโ MunicipalityFtsDao.kt
โ โ โโโ VehicleDao.kt
โ โโโ entity/ # Room entities (infra, not domain)
โ โโโ preferences/
โ โโโ UserPreferencesDataStore.kt
โ โโโ GeocodeCacheDataStore.kt # UC-012 Nominatim cache
โ โโโ AddressGeocodeCacheDataStore.kt # UC-015 Nominatim cache
โโโ remote/
โ โโโ AnpListingScraper.kt # Jsoup + OkHttp
โ โโโ AnpFileDownloader.kt # OkHttp streaming download
โ โโโ NominatimReverseGeocodeClient.kt # UC-012
โ โโโ NominatimSearchClient.kt # UC-015 forward geocoding
โโโ parser/
โ โโโ StreamingXlsxParser.kt # Low-level ZIP/XML streaming
โ โโโ WeeklySummarySheetParser.kt # resumo_semanal_lpc
โ โโโ StationDetailSheetParser.kt # revendas_lpc
โโโ mapper/
โ โโโ AnpProductMapper.kt # BR-002 Portuguese โ FuelProduct
โ โโโ AnpDateMapper.kt # Excel serial โ LocalDate
โ โโโ EntityDomainMapper.kt
โโโ repository/
โ โโโ PriceTableRepositoryImpl.kt
โ โโโ AveragePriceRepositoryImpl.kt
โ โโโ StationPriceRepositoryImpl.kt
โ โโโ MunicipalitySearchRepositoryImpl.kt
โ โโโ UserPreferencesRepositoryImpl.kt
โ โโโ VehicleRepositoryImpl.kt # UC-010
โ โโโ ReverseGeocodeRepositoryImpl.kt # UC-012
โ โโโ AddressGeocodeRepositoryImpl.kt # UC-015
โโโ worker/
โ โโโ SyncWorker.kt # WorkManager โ UC-001
โ โโโ RetentionCleanupWorker.kt # BR-013
โ โโโ PriceDropEvaluationWorker.kt # UC-014
โโโ di/
โโโ DatabaseModule.kt
โโโ NetworkModule.kt
โโโ RepositoryModule.kt
app/src/main/kotlin/com/anpfuel/app/
โโโ AnpFuelApplication.kt # @HiltAndroidApp
โโโ navigation/
โ โโโ AnpNavGraph.kt
โ โโโ Routes.kt
โโโ theme/
โ โโโ Theme.kt # Material 3
โ โโโ Color.kt
โ โโโ Type.kt
โโโ ui/
โ โโโ onboarding/ # UC-002
โ โโโ home/ # UC-005 entry
โ โโโ search/ # UC-004
โ โโโ location/ # UC-003
โ โโโ prices/ # UC-005, UC-006
โ โโโ stations/ # UC-007, UC-013
โ โโโ vehicle/ # UC-010
โ โโโ settings/ # UC-008
โโโ viewmodel/ # One ViewModel per screen
โ โโโ HomeViewModel.kt
โ โโโ SearchViewModel.kt
โ โโโ ...
โโโ mapper/
โ โโโ AppErrorMapper.kt # AppError โ stringResource key
โโโ di/
โโโ AppModule.kt
sequenceDiagram
participant UI as :app Compose
participant VM as ViewModel
participant UC as GetMunicipalityPricesUseCase
participant Repo as AveragePriceRepository
participant Room as Room DAO
UI->>VM: UserEvent.LoadPrices
VM->>UC: invoke(municipality, state)
UC->>Repo: getLatestPrices(municipality, state)
Repo->>Room: SQL query (indexed)
Room-->>Repo: entities
Repo-->>UC: domain AveragePrice list
UC-->>VM: Result<List<AveragePrice>>
VM-->>UI: UiState.Success(prices)
sequenceDiagram
participant WM as SyncWorker
participant UC as SyncPriceTablesUseCase
participant Scraper as AnpListingScraper
participant DL as AnpFileDownloader
participant Parser as StreamingXlsxParser
participant Repo as PriceTableRepository
participant Room as Room DAO
WM->>UC: invoke(SyncSource.SCHEDULED)
UC->>Scraper: discoverNewTables()
Scraper-->>UC: List<PriceTableUrl>
UC->>DL: download(url)
DL-->>UC: localFile
UC->>Parser: parseSummary(localFile)
Parser-->>UC: Flow<AveragePriceRow>
UC->>Repo: importBatch(rows)
Repo->>Room: INSERT batch 1000
UC-->>WM: SyncJobCompleted
| Use Case | Application class | Domain ports | Infrastructure |
|---|---|---|---|
| UC-001 Sync | SyncPriceTablesUseCase |
PriceTableRepository |
Scraper, Downloader, Parser, Worker |
| UC-002 Onboarding | CompleteOnboardingUseCase |
UserPreferencesRepository |
DataStore |
| UC-003 Select location | SelectLocationUseCase |
AveragePriceRepository, UserPreferencesRepository
|
Room |
| UC-004 Search | SearchMunicipalityUseCase |
MunicipalitySearchRepository |
Room FTS4 (baseline) |
| UC-005 View prices | GetMunicipalityPricesUseCase |
AveragePriceRepository |
Room |
| UC-006 History | GetPriceHistoryUseCase |
AveragePriceRepository |
Room |
| UC-007 Stations | GetStationPricesUseCase |
StationPriceRepository |
Room + on-demand download |
| UC-008 Settings |
UpdatePreferencesUseCase, ClearCacheUseCase
|
UserPreferencesRepository, all repos |
DataStore, Room |
| UC-009 Week picker |
SelectSurveyWeekUseCase, SelectWeekAndSyncUseCase
|
UserPreferencesRepository, PriceTableRepository
|
DataStore, Worker |
| UC-010 Vehicles |
SaveVehicleUseCase, ListVehiclesUseCase, DeleteVehicleUseCase
|
VehicleRepository |
Room |
| UC-011 Tank fill cost | GetTankFillCostEstimatesUseCase |
VehicleRepository, StationPriceRepository, AveragePriceRepository
|
Room |
| UC-012 Device location | ResolveDeviceLocationUseCase |
ReverseGeocodeRepository, MunicipalityCatalog port |
Nominatim, DataStore cache |
| UC-013 Station navigation | BuildStationNavigationQueryUseCase |
Domain rules only |
:app Intents |
| UC-014 Price drop alerts | EvaluatePriceDropAlertsUseCase |
VehicleRepository, price repos |
WorkManager, NotificationManager |
| UC-015 Nearest station | FindNearestBestPriceStationUseCase |
AddressGeocodeRepository, price repos |
Nominatim /search, DataStore cache |
Room database name: anp_fuel.db
Baseline schema is version 4 (see AnpFuelDatabase.kt and data/schemas/); the SQL below shows the version-1 shape.
-- Survey metadata (immutable once imported โ BR-003)
CREATE TABLE survey_week (
id TEXT PRIMARY KEY NOT NULL,
start_date TEXT NOT NULL, -- ISO-8601 LocalDate
end_date TEXT NOT NULL,
summary_imported_at INTEGER NOT NULL,
station_imported_at INTEGER -- nullable until station sync
);
-- Municipality averages (WEEKLY_SUMMARY PriceTable)
CREATE TABLE average_price (
id TEXT PRIMARY KEY NOT NULL,
survey_week_id TEXT NOT NULL,
state TEXT NOT NULL,
municipality TEXT NOT NULL,
fuel_product TEXT NOT NULL, -- FuelProduct enum name
station_count INTEGER,
unit TEXT,
avg_price REAL,
min_price REAL,
max_price REAL,
std_dev REAL,
FOREIGN KEY (survey_week_id) REFERENCES survey_week(id),
UNIQUE (survey_week_id, state, municipality, fuel_product)
);
-- FTS4 for UC-004 municipality search (baseline: Room @Fts4)
CREATE VIRTUAL TABLE municipality_fts USING fts4(
municipality,
state,
content='average_price',
content_rowid='rowid',
tokenize='unicode61 remove_diacritics 2'
);
-- Station-level prices (STATION_DETAIL PriceTable โ rolling window BR-013)
CREATE TABLE station_price (
id TEXT PRIMARY KEY NOT NULL,
survey_week_id TEXT NOT NULL,
cnpj TEXT NOT NULL,
legal_name TEXT,
trade_name TEXT,
address TEXT NOT NULL,
municipality TEXT NOT NULL,
state TEXT NOT NULL,
brand TEXT,
fuel_product TEXT NOT NULL,
price REAL NOT NULL,
collected_at TEXT, -- ISO-8601 LocalDate
FOREIGN KEY (survey_week_id) REFERENCES survey_week(id)
);
-- Import audit log (immutable history โ agent core ยง6)
CREATE TABLE import_audit_log (
id TEXT PRIMARY KEY NOT NULL,
survey_week_id TEXT,
action TEXT NOT NULL, -- DISCOVERED, DOWNLOADED, IMPORTED, FAILED
detail TEXT,
occurred_at INTEGER NOT NULL
);
CREATE INDEX idx_avg_survey_state_muni
ON average_price(survey_week_id, state, municipality);
CREATE INDEX idx_avg_muni_product
ON average_price(municipality, fuel_product);
CREATE INDEX idx_station_survey_muni_product
ON station_price(survey_week_id, municipality, fuel_product);
CREATE INDEX idx_station_price_asc
ON station_price(survey_week_id, municipality, fuel_product, price);
-- User vehicles (UC-010 โ v3 migration)
CREATE TABLE vehicle (
id TEXT PRIMARY KEY NOT NULL,
display_name TEXT NOT NULL,
tank_capacity_liters REAL NOT NULL,
fuel_product TEXT NOT NULL,
price_source_mode TEXT NOT NULL,
specific_station_cnpj TEXT,
price_drop_alert_enabled INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0
);@Database(
entities = [SurveyWeekEntity::class, AveragePriceEntity::class,
StationPriceEntity::class, ImportAuditLogEntity::class],
version = 1,
exportSchema = true,
)
abstract class AnpFuelDatabase : RoomDatabase() {
abstract fun surveyWeekDao(): SurveyWeekDao
abstract fun averagePriceDao(): AveragePriceDao
abstract fun stationPriceDao(): StationPriceDao
abstract fun municipalityFtsDao(): MunicipalityFtsDao
}-
exportSchema = trueโ schemas committed todata/schemas/for migration tests - WAL mode enabled in
RoomDatabase.Builder - Domain IDs generated in
:domain(UUID / deterministic hash) โ not auto-increment
Owned by :data:worker, orchestrated by :application:SyncPriceTablesUseCase.
stateDiagram-v2
[*] --> Idle
Idle --> Discovering: SyncWorker triggered
Discovering --> Downloading: new URLs found
Discovering --> Completed: up to date
Downloading --> Parsing: file on disk
Parsing --> Importing: rows validated BR-001/BR-002
Importing --> Completed: batch insert done
Discovering --> Failed: scrape error
Downloading --> Failed: network error
Parsing --> Failed: parse error
Importing --> Failed: db error
Completed --> Idle
Failed --> Idle: retry/backoff
| Worker | Trigger | Constraints |
|---|---|---|
SyncWorker |
Periodic (7 days) + manual | Network connected; unmetered if BR-014 |
RetentionCleanupWorker |
After successful station import | None |
BR-015: SyncPriceTablesUseCase checks for active job before starting โ rejects concurrent runs.
Default sync scope:
- Always download + import
WEEKLY_SUMMARY(~300 KB) - Download + import
STATION_DETAILwhensyncStationDetail = true(default) or UC-007 on-demand when disabled
MainActivity calls enableEdgeToEdge() and WindowCompat.setDecorFitsSystemWindows(window, false) so content draws behind system bars. Every screen must respect insets so text and touch targets are never clipped.
| Composable | Role |
|---|---|
AnpScaffold |
Wraps Material3 Scaffold with contentWindowInsets = WindowInsets.safeDrawing; passes innerPadding to the content lambda |
AnpTopAppBar |
Wraps TopAppBar with WindowInsets.statusBars by default; set applyStatusBarInsets = false only for full-bleed hero layouts |
Rules:
- New screens and refactors must use
AnpTopAppBarinsideAnpScaffoldโ never rawScaffold/TopAppBar(all navigation destinations migrated in Phase 13.2). - Apply
Modifier.padding(innerPadding)fromAnpScaffoldto scrollable content; do not add manual status-bar padding on standard screens. - Bottom sheets and dialogs inherit system insets from Material3; list content inside sheets needs bottom padding โฅ navigation-bar inset (Phase 13.2.8).
// Pattern enforced across all screens
data class PriceHomeUiState(
val readiness: DataReadinessState,
val municipality: String?,
val state: BrazilianState?,
val surveyWeek: SurveyWeek?,
val prices: List<AveragePriceUiModel>,
val isLoading: Boolean,
val error: AppError?,
)
sealed interface PriceHomeEvent {
data object Refresh : PriceHomeEvent
data class SelectFuel(val product: FuelProduct) : PriceHomeEvent
data object OpenSearch : PriceHomeEvent
}- ViewModel injects use cases only (never DAOs or OkHttp)
- State exposed as
StateFlow<UiState> - One-shot events via
ChannelorSharedFlowif needed
onboarding โ home โ search
โ
location settings
โ
prices โ history
โ
stations โ navigate (external maps)
โ
vehicles
tests/
โโโ domain/ # 75% pyramid base โ JUnit 5, pure Kotlin
โ โโโ SurveyWeekTest.kt # BR-001
โ โโโ FuelProductMapperTest.kt # BR-002
โ โโโ SyncJobStateTest.kt # state machine
โโโ application/ # Use case tests โ MockK + Turbine
โ โโโ SyncPriceTablesUseCaseTest.kt
โ โโโ SearchMunicipalityUseCaseTest.kt
โโโ data/ # Parser + repository โ in-memory Room
โ โโโ StreamingXlsxParserTest.kt # validated against data/samples/
โ โโโ AveragePriceRepositoryTest.kt
โโโ app/ # Compose UI tests (5% pyramid top)
โโโ SearchScreenTest.kt
Parser tests must run against real files in data/samples/ (see data-sources.md).
| Concern | Layer | Implementation |
|---|---|---|
| TLS for ANP downloads | :data |
OkHttp โ HTTPS only, no cleartext |
| TLS for Nominatim | :data |
OkHttp โ HTTPS only (UC-012, UC-015) |
| No PII cloud storage | :domain |
No user accounts; vehicles local only |
| Ephemeral GPS | :app |
One-shot location; coordinates not persisted (UC-012, UC-015) |
| CNPJ is public data | :data |
Stored as-is from ANP |
| Local preferences | :data |
DataStore (not SharedPreferences) |
| Local notifications |
:data / :app
|
NotificationManager โ UC-014, no FCM |
| Cache wipe | :application |
ClearCacheUseCase โ UC-008 |
| Document | Content |
|---|---|
| tech-stack.md | Libraries, versions, module deps |
| adr/001-kotlin-compose-stack.md | Why this stack |
| adr/003-nominatim-reverse-geocode.md | Nominatim for UC-012 |
| user-business-logic.md | UC-001โฆUC-014, BR-001โฆBR-027 |
| glossary.md | Domain language |
| data-sources.md | ANP XLSX formats |
-
:domainโ Value Objects + BR-001/BR-002 unit tests (TDD) -
:data:parserโ Streaming XlsxParser validated against samples -
:data:localโ Room schema + DAOs + FTS -
:applicationโ SyncPriceTablesUseCase, GetMunicipalityPricesUseCase -
:data:remote+ SyncWorker -
:appโ Theme + Home + Search (UC-004, UC-005)
Do not skip steps or implement UI before domain tests pass.