Files
ledgerr/docs/01-data-model.md
T
Achmad Setyabudi Susilo 3e30423083 docs: add architecture docs, AGENTS.md, CLAUDE.md, and copy UI components
- 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
2026-06-28 15:08:37 +07:00

4.6 KiB

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

// 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

// 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

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

// 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