Files
ledgerr/docs/01-data-model.md
admin 51c54749cb docs + AGENTS: no-implementation rule, issue-driven workflow, Vico, Uncategorized, ClearAllData, AddEditRecurringScreen
- AGENTS.md: add No-implementation rule and Issue-driven workflow section
- docs/01: rename default fallback category from 'Other' to 'Uncategorized'
- docs/02, 03: update 'Other' references to 'Uncategorized' in delete/seed/default flows
- docs/04: replace Canvas charts with Vico 2.x dep, add data feature folder, add AddEditRecurringScreen, document tab-aware FAB and shared ExportAction helper
2026-06-28 15:47:28 +07:00

176 lines
4.7 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; "Uncategorized" 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 |
| Uncategorized | `0xFF9E9E9E` (grey) | **true** |
`Uncategorized` is the permanent fallback when a user-defined category is deleted — orphaned expenses are reassigned to it before the category is removed.
---
## 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`