3e30423083
- Add docs/01-04 covering data model, interfaces, function TODOs, and implementation plan - Add AGENTS.md with project conventions, architecture rules, feature workflow, and git policy - Add CLAUDE.md pointing to AGENTS.md - Copy UI components and utils from info-krl-android (preference widgets, scrollbars, etc.) - Delete obsolete UiModule.kt
176 lines
4.6 KiB
Markdown
176 lines
4.6 KiB
Markdown
# 01 — Data Model
|
|
|
|
Models are scoped to their feature under `dev.achmad.ledgerr.domain.<feature>.model`. No shared/common package — features import from each other's model package where needed.
|
|
|
|
---
|
|
|
|
## Feature: expense
|
|
|
|
```kotlin
|
|
// DateRange.kt
|
|
data class DateRange(
|
|
val start: LocalDate,
|
|
val end: LocalDate
|
|
) {
|
|
companion object {
|
|
fun thisMonth(): DateRange {
|
|
val now = LocalDate.now()
|
|
return DateRange(now.withDayOfMonth(1), now.withDayOfMonth(now.lengthOfMonth()))
|
|
}
|
|
fun thisWeek(): DateRange {
|
|
val now = LocalDate.now()
|
|
return DateRange(now.with(DayOfWeek.MONDAY), now.with(DayOfWeek.SUNDAY))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Expense.kt
|
|
data class Expense(
|
|
val id: Long = 0,
|
|
val amount: Double, // always positive; represents outflow
|
|
val categoryId: Long,
|
|
val date: LocalDate,
|
|
val note: String? = null,
|
|
val recurringExpenseId: Long? = null, // set if auto-generated from a RecurringExpense
|
|
val createdAt: Long = System.currentTimeMillis()
|
|
)
|
|
|
|
// ExpenseWithCategory.kt
|
|
data class ExpenseWithCategory(
|
|
val expense: Expense,
|
|
val category: Category // imported from domain.category.model
|
|
)
|
|
|
|
// ExpenseSummary.kt
|
|
data class ExpenseSummary(
|
|
val totalAmount: Double,
|
|
val byCategory: List<Pair<Category, Double>>, // sorted by amount DESC
|
|
val period: DateRange
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Feature: category
|
|
|
|
```kotlin
|
|
// Category.kt
|
|
data class Category(
|
|
val id: Long = 0,
|
|
val name: String,
|
|
val color: Int, // ARGB
|
|
val iconName: String? = null, // Material icon name
|
|
val isDefault: Boolean = false // non-deletable; "Other" is always true
|
|
)
|
|
```
|
|
|
|
Default categories seeded on first install:
|
|
|
|
| Name | color (ARGB hex) | isDefault |
|
|
|------|------------------|-----------|
|
|
| Food & Drink | `0xFFFF9800` (orange) | false |
|
|
| Transport | `0xFF2196F3` (blue) | false |
|
|
| Housing | `0xFF795548` (brown) | false |
|
|
| Health | `0xFFF44336` (red) | false |
|
|
| Entertainment | `0xFF9C27B0` (purple) | false |
|
|
| Shopping | `0xFFE91E63` (pink) | false |
|
|
| Education | `0xFF4CAF50` (green) | false |
|
|
| Other | `0xFF9E9E9E` (grey) | **true** |
|
|
|
|
`Other` is the permanent fallback when a user-defined category is deleted.
|
|
|
|
---
|
|
|
|
## Feature: recurring
|
|
|
|
```kotlin
|
|
// RecurringInterval.kt
|
|
enum class RecurringInterval {
|
|
DAILY, WEEKLY, MONTHLY, YEARLY;
|
|
|
|
fun advance(from: LocalDate): LocalDate = when (this) {
|
|
DAILY -> from.plusDays(1)
|
|
WEEKLY -> from.plusWeeks(1)
|
|
MONTHLY -> from.plusMonths(1)
|
|
YEARLY -> from.plusYears(1)
|
|
}
|
|
}
|
|
|
|
// RecurringExpense.kt
|
|
data class RecurringExpense(
|
|
val id: Long = 0,
|
|
val amount: Double,
|
|
val categoryId: Long,
|
|
val note: String? = null,
|
|
val interval: RecurringInterval,
|
|
val startDate: LocalDate,
|
|
val nextDueDate: LocalDate, // initialized = startDate; advances after each processing
|
|
val isActive: Boolean = true
|
|
)
|
|
|
|
// RecurringExpenseWithCategory.kt
|
|
data class RecurringExpenseWithCategory(
|
|
val recurring: RecurringExpense,
|
|
val category: Category
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Feature: bankstatement
|
|
|
|
Each bank has its own raw model representing a single parsed row from the bank's PDF format. These exist so bank-specific parsing logic produces typed data before converting to the common `PendingImportExpense`.
|
|
|
|
```kotlin
|
|
// PendingImportExpense.kt — common output for the review screen
|
|
data class PendingImportExpense(
|
|
val amount: Double,
|
|
val date: LocalDate,
|
|
val description: String, // raw text from PDF
|
|
val suggestedCategoryId: Long? = null,
|
|
val isSelected: Boolean = true // user can deselect before committing
|
|
)
|
|
|
|
// BRIStatementEntry.kt
|
|
data class BRIStatementEntry(
|
|
val date: String, // raw string as it appears in PDF
|
|
val description: String,
|
|
val debit: String?, // raw amount string, null if not a debit
|
|
val credit: String?,
|
|
val balance: String?
|
|
)
|
|
|
|
// JagoStatementEntry.kt
|
|
data class JagoStatementEntry(
|
|
val date: String,
|
|
val description: String,
|
|
val amount: String,
|
|
val type: String // e.g. "DEBIT" / "KREDIT"
|
|
)
|
|
|
|
// BNIStatementEntry.kt
|
|
data class BNIStatementEntry(
|
|
val date: String,
|
|
val description: String,
|
|
val debit: String?,
|
|
val credit: String?,
|
|
val balance: String?
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Feature: export
|
|
|
|
No domain models — uses `Expense`, `ExpenseWithCategory`, and `DateRange` from the expense feature.
|
|
|
|
---
|
|
|
|
## Room Entities (data layer only)
|
|
|
|
Live in `data/local/entity/`. Mirror domain models with Room annotations; dates stored as `Long` (epoch day via `LocalDateConverter`).
|
|
|
|
- `CategoryEntity`
|
|
- `ExpenseEntity`
|
|
- `RecurringExpenseEntity`
|