mirror of
https://github.com/andwati/ledga.git
synced 2026-10-18 03:12:55 +00:00
Complete Android app (Kotlin/Jetpack Compose) with: - SMS parser for all 17 M-Pesa transaction types - Room database with auto-categorization engine - Real-time SMS capture via BroadcastReceiver - Onboarding flow with SMS history import - Home dashboard with balance, spending, donut chart - Transactions list with search, filters, day grouping - Trends screen with Vico bar charts, top merchants - Budget system with progress tracking - Transaction detail sheet with re-categorization - Notifications (daily/weekly summaries, budget alerts) - Google Drive backup & restore - Manual export/import (CSV + JSON ZIP) - Self-update system via GitHub Releases API - Theme system (System/Light/Dark) + font scaling - Adaptive app icon with splash screen - GitHub Actions CI/CD for release APK builds
747 lines
32 KiB
Markdown
747 lines
32 KiB
Markdown
# Ledga — M-Pesa Spending Tracker
|
|
|
|
## Overview
|
|
|
|
A native Android app that automatically tracks M-Pesa spending by reading SMS confirmations. Zero manual entry — install, grant SMS permission, done. Built for non-technical users with a clean, Canva-like UI.
|
|
|
|
**Target users:** Family members (all Android), non-technical
|
|
**Distribution:** Direct APK install + in-app self-updates via GitHub Releases (no Play Store)
|
|
**Data model:** Fully offline, local-only per device. No accounts, no server.
|
|
|
|
---
|
|
|
|
## Tech Stack
|
|
|
|
| Component | Technology |
|
|
|-----------|------------|
|
|
| Language | Kotlin |
|
|
| UI Framework | Jetpack Compose + Material 3 |
|
|
| Local Database | Room (SQLite) |
|
|
| Background SMS | BroadcastReceiver + WorkManager |
|
|
| DI | Hilt (dependency injection) |
|
|
| Settings Storage | Jetpack DataStore (Preferences) |
|
|
| Charts | Vico (Compose-native charting library) |
|
|
| Notifications | Android NotificationManager + AlarmManager |
|
|
| Backup | Google Drive API (auto), manual file export |
|
|
| Self-Update | GitHub Releases API |
|
|
| Min SDK | API 26 (Android 8.0) |
|
|
| Build | Gradle (Kotlin DSL) |
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ UI Layer │
|
|
│ Jetpack Compose + Material 3 │
|
|
│ ┌──────┐ ┌──────────┐ ┌──────┐ ┌────────┐ │
|
|
│ │ Home │ │Transactions│ │Trends│ │Settings│ │
|
|
│ └──────┘ └──────────┘ └──────┘ └────────┘ │
|
|
├─────────────────────────────────────────────┤
|
|
│ ViewModel Layer │
|
|
│ StateFlow + Coroutines │
|
|
├─────────────────────────────────────────────┤
|
|
│ Repository Layer │
|
|
│ TransactionRepo │ CategoryRepo │ │
|
|
│ SettingsRepo │ BackupRepo │ │
|
|
├─────────────────────────────────────────────┤
|
|
│ Data Layer │
|
|
│ ┌──────┐ ┌───────────┐ ┌─────────────┐ │
|
|
│ │ Room │ │SMS Parser │ │Google Drive │ │
|
|
│ │ DB │ │ │ │ Backup │ │
|
|
│ └──────┘ └───────────┘ └─────────────┘ │
|
|
├─────────────────────────────────────────────┤
|
|
│ System Services │
|
|
│ SMS BroadcastReceiver │ WorkManager │
|
|
│ NotificationManager │ AlarmManager │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## SMS Parsing
|
|
|
|
### Sender Filter
|
|
Only process SMS from sender: `MPESA`
|
|
|
|
### Transaction Types & Patterns
|
|
|
|
**1. Send Money**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Ksh500.00 sent to JOHN DOE 0712345678 on 21/3/26 at 1:30 PM. New M-PESA balance is Ksh1,200.00. Transaction cost, Ksh0.00.
|
|
```
|
|
→ Extract: code, amount, recipient name, recipient phone, date/time, balance, cost
|
|
|
|
**2. Buy Goods (Till)**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Ksh1,200.00 paid to NAIVAS SUPERMARKET. on 21/3/26 at 2:15 PM.New M-PESA balance is Ksh3,500.00. Transaction cost, Ksh0.00.
|
|
```
|
|
→ Extract: code, amount, merchant name, date/time, balance, cost
|
|
|
|
**3. Pay Bill**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Ksh2,500.00 paid to KPLC PREPAID. Account Number 12345678. on 21/3/26 at 3:00 PM. New M-PESA balance is Ksh1,000.00. Transaction cost, Ksh0.00.
|
|
```
|
|
→ Extract: code, amount, business name, account number, date/time, balance, cost
|
|
|
|
**4. Withdraw (Agent)**
|
|
```
|
|
RK31B7X4ZQ Confirmed.You have withdrawn Ksh1,000.00 from JAMES AGENT 543210 on 21/3/26 at 4:00 PM.New M-PESA balance is Ksh500.00. Transaction cost, Ksh28.00.
|
|
```
|
|
→ Extract: code, amount, agent name, agent number, date/time, balance, cost
|
|
|
|
**5. Withdraw (ATM)**
|
|
```
|
|
RK31B7X4ZQ Confirmed. You have withdrawn Ksh5,000.00 from an ATM on 21/3/26 at 4:30 PM. New M-PESA balance is Ksh2,000.00. Transaction cost, Ksh34.00.
|
|
```
|
|
→ Extract: code, amount, date/time, balance, cost
|
|
→ Note: No agent name/number — just "from an ATM"
|
|
|
|
**6. Deposit**
|
|
```
|
|
RK31B7X4ZQ Confirmed.You have deposited Ksh5,000.00 to your M-PESA account on 21/3/26 at 10:00 AM.New M-PESA balance is Ksh5,500.00.
|
|
```
|
|
→ Extract: code, amount, date/time, balance
|
|
|
|
**7. Received Money**
|
|
```
|
|
RK31B7X4ZQ Confirmed.You have received Ksh2,000.00 from JANE DOE 0798765432 on 21/3/26 at 11:00 AM.New M-PESA balance is Ksh7,500.00.
|
|
```
|
|
→ Extract: code, amount, sender name, sender phone, date/time, balance
|
|
|
|
**8. Airtime Purchase (Self)**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Ksh100.00 of airtime purchased on 21/3/26 at 12:00 PM.New M-PESA balance is Ksh7,400.00. Transaction cost, Ksh0.00.
|
|
```
|
|
→ Extract: code, amount, date/time, balance, cost
|
|
|
|
**9. Airtime Purchase (For Others)**
|
|
```
|
|
RK31B7X4ZQ Confirmed. You bought Ksh100.00 of airtime for 0712345678 on 21/3/26 at 12:30 PM.New M-PESA balance is Ksh7,300.00. Transaction cost, Ksh0.00.
|
|
```
|
|
→ Extract: code, amount, recipient phone, date/time, balance, cost
|
|
|
|
**10. M-Pesa Global (International Transfer)**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Ksh5,000.00 sent to JOHN DOE +44712345678 (United Kingdom) via M-PESA Global on 21/3/26 at 1:30 PM. New M-PESA balance is Ksh10,000.00. Transaction cost, Ksh150.00.
|
|
```
|
|
→ Extract: code, amount, recipient name, recipient phone (international), destination country, date/time, balance, cost
|
|
|
|
**11. Fuliza (Borrow)**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Ksh500.00 sent to JOHN DOE 0712345678 on 21/3/26 at 1:30 PM. New M-PESA balance is Ksh0.00. Fuliza M-PESA amount is Ksh500.00. Fuliza M-PESA outstanding amount is Ksh500.00.
|
|
```
|
|
→ Extract: all standard fields + fuliza amount, fuliza outstanding
|
|
|
|
**12. Fuliza Repayment**
|
|
```
|
|
RK31B7X4ZQ Confirmed. You have paid Ksh200.00 to Fuliza M-PESA on 21/3/26 at 2:00 PM. Fuliza M-PESA outstanding amount is Ksh300.00.
|
|
```
|
|
→ Extract: code, amount, date/time, fuliza outstanding
|
|
|
|
**13. Fuliza Reversal**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Fuliza M-PESA of Ksh500.00 has been reversed on 21/3/26 at 3:00 PM. Fuliza M-PESA outstanding amount is Ksh0.00.
|
|
```
|
|
→ Extract: code, amount, date/time, fuliza outstanding
|
|
|
|
**14. Reversal**
|
|
```
|
|
RK31B7X4ZQ Confirmed. Transaction RJ12345678 has been reversed. Your account balance is Ksh2,000.00.
|
|
```
|
|
→ Extract: code, reversed transaction code, balance
|
|
|
|
### M-Shwari & KCB M-Pesa (Acknowledged, Not Parsed)
|
|
|
|
These services send SMS from the `MPESA` sender but are savings/loan products, not spending transactions:
|
|
- **M-Shwari:** `Confirmed. Ksh1,000.00 transferred to M-Shwari account...` / `Ksh1,000.00 transferred from M-Shwari...`
|
|
- **KCB M-Pesa:** `Confirmed. Ksh2,000.00 transferred to KCB M-Pesa account...` / loan disbursements
|
|
|
|
**Strategy:** Detect these by keywords ("M-Shwari", "KCB M-Pesa") and store with type `MSHWARI` or `KCB_MPESA`. Show them in the transaction list but don't count them as spending. They're internal transfers between M-Pesa wallet and savings/loan products.
|
|
|
|
### Parser Strategy
|
|
- Regex-based pattern matching per transaction type
|
|
- Keyword detection: "sent to", "paid to", "withdrawn.*from.*AGENT" (agent), "withdrawn.*from an ATM" (ATM), "deposited", "received", "airtime purchased", "bought.*airtime for", "M-PESA Global", "reversed", "Fuliza M-PESA amount", "paid to Fuliza", "Fuliza M-PESA of", "M-Shwari", "KCB M-Pesa"
|
|
- Amount extraction: `Ksh[\d,]+\.\d{2}` pattern
|
|
- Date extraction: `\d{1,2}/\d{1,2}/\d{2,4} at \d{1,2}:\d{2} [AP]M`
|
|
- International phone detection: `\+\d{7,15}` pattern (for M-Pesa Global)
|
|
- Country extraction: text in parentheses after international phone number
|
|
- Fallback: store unparsed M-Pesa SMS with type `UNKNOWN` for later parsing improvements
|
|
|
|
---
|
|
|
|
## Database Schema (Room)
|
|
|
|
### transactions
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| id | Long (PK, auto) | Primary key |
|
|
| transactionCode | String (unique) | M-Pesa transaction code |
|
|
| type | Enum | SEND, BUY_GOODS, PAY_BILL, WITHDRAW_AGENT, WITHDRAW_ATM, DEPOSIT, RECEIVED, AIRTIME_SELF, AIRTIME_OTHER, MPESA_GLOBAL, FULIZA, FULIZA_REPAYMENT, FULIZA_REVERSAL, MSHWARI, KCB_MPESA, REVERSAL, UNKNOWN |
|
|
| amount | Double | Transaction amount in Ksh |
|
|
| transactionCost | Double | M-Pesa fee |
|
|
| recipientName | String? | Name of recipient/merchant |
|
|
| recipientPhone | String? | Phone number (if person) |
|
|
| accountNumber | String? | Account number (paybill) |
|
|
| destinationCountry | String? | Country name for M-Pesa Global transfers |
|
|
| balance | Double | M-Pesa balance after transaction |
|
|
| direction | Enum | INFLOW, OUTFLOW (derived at parse time — simplifies totals/chart queries) |
|
|
| categoryId | Long? (FK) | Assigned category |
|
|
| fulizaAmount | Double? | Fuliza amount (if applicable) |
|
|
| fulizaOutstanding | Double? | Fuliza outstanding (if applicable) |
|
|
| rawSms | String | Original SMS text (for re-parsing) |
|
|
| timestamp | Long | Transaction timestamp (epoch ms) |
|
|
| createdAt | Long | Record creation time |
|
|
|
|
### categories
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| id | Long (PK, auto) | Primary key |
|
|
| name | String | Category name |
|
|
| icon | String | Material icon name |
|
|
| color | String | Hex color code |
|
|
| isDefault | Boolean | System default vs user-created |
|
|
|
|
### budgets
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| id | Long (PK, auto) | Primary key |
|
|
| categoryId | Long? (FK) | Null = overall budget |
|
|
| monthlyLimit | Double | Budget limit in Ksh |
|
|
| isActive | Boolean | Whether alerts are enabled |
|
|
|
|
### category_rules
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| id | Long (PK, auto) | Primary key |
|
|
| categoryId | Long (FK) | Target category |
|
|
| matchType | Enum | RECIPIENT_NAME, PHONE, TILL, PAYBILL |
|
|
| matchValue | String | Value to match |
|
|
|
|
### Default Categories
|
|
| Category | Icon | Color | Auto-match examples |
|
|
|----------|------|-------|---------------------|
|
|
| Groceries | shopping_cart | #4CAF50 | NAIVAS, QUICKMART, CARREFOUR |
|
|
| Transport | directions_car | #2196F3 | Uber, Bolt, matatu paybills |
|
|
| Bills & Utilities | receipt | #FF9800 | KPLC, NAIROBI WATER, DSTV |
|
|
| Airtime & Data | phone_android | #9C27B0 | Airtime purchases |
|
|
| Food & Dining | restaurant | #E91E63 | Restaurants, fast food |
|
|
| Send Money | person | #607D8B | Person-to-person transfers |
|
|
| Received | account_balance_wallet | #00BCD4 | Money received |
|
|
| Withdrawal | atm | #795548 | Agent withdrawals |
|
|
| Deposit | savings | #8BC34A | Cash deposits |
|
|
| Shopping | shopping_bag | #FF5722 | General merchants |
|
|
| International | public | #3F51B5 | M-Pesa Global transfers |
|
|
| Savings & Loans | account_balance | #009688 | M-Shwari, KCB M-Pesa |
|
|
| Other | category | #9E9E9E | Uncategorized |
|
|
|
|
---
|
|
|
|
## Screens & UI
|
|
|
|
### Design System
|
|
- **Color palette:** M-Pesa green (#4CAF50) as primary, warm neutrals, category-specific accent colors
|
|
- **Typography:** System font, scalable (Small/Medium/Large/Extra Large + System Default)
|
|
- **Cards:** Rounded corners (16dp), subtle elevation, clean spacing
|
|
- **Animations:** Smooth transitions between screens, animated chart loading
|
|
- **Dark mode:** Full dark theme, follows system by default with manual override
|
|
|
|
### 1. Home Dashboard
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ Good morning 👋 │
|
|
│ │
|
|
│ M-PESA Balance │
|
|
│ Ksh 12,500.00 │
|
|
│ (from last transaction) │
|
|
│ │
|
|
│ ┌─────────────────────────┐ │
|
|
│ │ March 2026 │ │
|
|
│ │ Spent: Ksh 45,200 │ │
|
|
│ │ ▼ 12% vs last month │ │
|
|
│ │ Fees: Ksh 320 │ │
|
|
│ └─────────────────────────┘ │
|
|
│ │
|
|
│ [Today] [This Week] [Month] │
|
|
│ │
|
|
│ ┌─────────────────────────┐ │
|
|
│ │ 🍩 Donut Chart │ │
|
|
│ │ Category Breakdown │ │
|
|
│ └─────────────────────────┘ │
|
|
│ │
|
|
│ Recent Transactions │
|
|
│ ┌─────────────────────────┐ │
|
|
│ │ 🛒 Naivas -Ksh 1,200 │ │
|
|
│ │ 📱 Airtime -Ksh 100 │ │
|
|
│ │ 💰 Received +Ksh 5,000 │ │
|
|
│ └─────────────────────────┘ │
|
|
│ │
|
|
│ ┌────┐┌────┐┌────┐┌────┐ │
|
|
│ │Home││List││Stats││ ⚙️ │ │
|
|
│ └────┘└────┘└────┘└────┘ │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
> **Balance display:** Shows the `balance` field from the most recent transaction. This is the last known M-Pesa balance — not a live query. Labeled "from last transaction" so users don't confuse it with a real-time check.
|
|
|
|
### 2. Transactions List
|
|
- Grouped by day with day totals
|
|
- Each item: category icon + recipient/merchant + amount (red spent, green received)
|
|
- Search bar at top
|
|
- Filter chips: All, Sent, Received, Bills, Goods, Withdraw
|
|
- Tap → transaction detail bottom sheet
|
|
- Long press → change category
|
|
|
|
### 3. Trends / Analytics
|
|
- Bar chart: daily spending for current month
|
|
- Line chart: monthly spending over time
|
|
- Top 5 recipients/merchants list
|
|
- Category comparison vs previous period
|
|
- **Transaction fees summary** — total fees paid this period (most people don't realize how much they spend on M-Pesa fees)
|
|
- Period selector: 7D / 30D / 90D / 1Y
|
|
|
|
### 4. Settings
|
|
```
|
|
Appearance
|
|
├── Theme: System / Light / Dark
|
|
└── Font Size: System Default / Small / Medium / Large / Extra Large
|
|
|
|
Notifications
|
|
├── Daily Summary: ON/OFF + time picker
|
|
├── Weekly Summary: ON/OFF
|
|
├── Budget Alerts: ON/OFF
|
|
└── Large Transaction Alert: ON/OFF + threshold (Ksh)
|
|
|
|
Budgets
|
|
├── Overall Monthly Budget: Ksh ___
|
|
└── Per Category: [list of categories with limits]
|
|
|
|
Data
|
|
├── Import SMS History
|
|
├── Export Data (CSV/JSON)
|
|
├── Unparsed Messages (X) — view SMS that failed to parse
|
|
└── Clear All Data (with confirmation)
|
|
|
|
Backup
|
|
├── Auto Backup: ON/OFF
|
|
├── Backup Frequency: Daily / Weekly
|
|
├── Google Account: shown
|
|
├── Last Backup: date
|
|
├── Back Up Now
|
|
└── Restore from Backup
|
|
|
|
About
|
|
├── Version: 1.0.0
|
|
├── Check for Updates
|
|
└── What's New
|
|
```
|
|
|
|
---
|
|
|
|
## Notification System
|
|
|
|
| Notification | Trigger | Default |
|
|
|-------------|---------|---------|
|
|
| Daily Summary | Scheduled (user picks time, default 8 PM) | ON |
|
|
| Weekly Summary | Every Sunday evening | ON |
|
|
| Budget Warning | 80% of budget reached | ON |
|
|
| Budget Exceeded | 100% of budget reached | ON |
|
|
| Large Transaction | Single transaction > threshold | OFF (default Ksh 5,000) |
|
|
|
|
Implementation:
|
|
- WorkManager for periodic summary jobs
|
|
- Immediate check on each new transaction for budget/large amount alerts
|
|
- NotificationChannel: "Spending Summaries", "Budget Alerts"
|
|
|
|
---
|
|
|
|
## Backup System
|
|
|
|
### Google Drive Auto-Backup
|
|
- Uses Google Sign-In (already on every Android phone)
|
|
- Backs up Room database + settings to app-specific Google Drive folder
|
|
- No extra permissions needed (app data folder is automatic)
|
|
- Frequency: daily or weekly (user choice)
|
|
- On fresh install: detect existing backup → prompt restore
|
|
|
|
### Manual Export/Import
|
|
- Export: generates a ZIP file containing:
|
|
- `transactions.csv` — human-readable, can open in Excel
|
|
- `data.json` — full database export for app restore
|
|
- Import: select ZIP file → restore all data
|
|
- Share via WhatsApp, Bluetooth, email, etc.
|
|
|
|
### SMS Re-import (Fallback)
|
|
- Scan all existing SMS from "MPESA" sender
|
|
- Parse and import, skipping duplicates (by transaction code)
|
|
- Progress indicator during import
|
|
|
|
---
|
|
|
|
## Self-Update System
|
|
|
|
### Flow
|
|
1. App checks GitHub Releases API on launch (max once per day)
|
|
2. Compares current version with latest release tag
|
|
3. If newer version exists:
|
|
- Shows non-intrusive banner: "Update available (v1.2.0)"
|
|
- Tap → shows changelog (from release notes)
|
|
- "Download & Install" button
|
|
4. Downloads APK to app cache
|
|
5. Triggers Android package installer intent
|
|
6. User confirms install (standard Android prompt)
|
|
|
|
### GitHub Release Format
|
|
```
|
|
Tag: v1.0.0
|
|
Title: Version 1.0.0
|
|
Body: Changelog in markdown
|
|
Asset: ledga-v1.0.0.apk
|
|
```
|
|
|
|
### Endpoint
|
|
```
|
|
GET https://api.github.com/repos/{owner}/{repo}/releases/latest
|
|
```
|
|
|
|
---
|
|
|
|
## Auto-Categorization
|
|
|
|
### Rule Engine (Priority Order)
|
|
1. **User rules** — if user manually categorized a merchant before, remember it
|
|
2. **Category rules table** — pattern matching on recipient name, till number, paybill
|
|
3. **Transaction type fallback** — airtime → Airtime, withdrawal → Withdrawal, etc.
|
|
4. **Default** — "Other" category
|
|
|
|
### Pre-loaded Rules
|
|
| Pattern | Match Type | Category |
|
|
|---------|------------|----------|
|
|
| NAIVAS, QUICKMART, CARREFOUR, CLEANSHELF | Recipient | Groceries |
|
|
| KPLC, KENYA POWER | Recipient | Bills |
|
|
| 888880 (KPLC Prepaid), 888888 (KPLC Postpaid) | Paybill | Bills |
|
|
| NAIROBI WATER, ELDORET WATER | Recipient | Bills |
|
|
| 444400 (Nairobi Water) | Paybill | Bills |
|
|
| DSTV, GOTV, SHOWMAX | Recipient | Bills |
|
|
| UBER, BOLT, LITTLE | Recipient | Transport |
|
|
| JAVA, KFC, CHICKEN INN, PIZZA INN | Recipient | Food & Dining |
|
|
| (any self airtime) | Type | Airtime |
|
|
| (any airtime for others) | Type | Airtime |
|
|
| (any withdrawal) | Type | Withdrawal |
|
|
| (any deposit) | Type | Deposit |
|
|
| (any received) | Type | Received |
|
|
| (any send money) | Type | Send Money |
|
|
| (any M-Pesa Global) | Type | International |
|
|
| (any M-Shwari) | Type | Savings & Loans |
|
|
| (any KCB M-Pesa) | Type | Savings & Loans |
|
|
|
|
Users can add/edit rules from the category settings.
|
|
|
|
---
|
|
|
|
## Permissions Required
|
|
|
|
| Permission | Purpose | When Requested |
|
|
|-----------|---------|----------------|
|
|
| RECEIVE_SMS | Listen for new M-Pesa SMS in real-time | On first launch |
|
|
| READ_SMS | Import existing M-Pesa SMS history | When user taps "Import History" |
|
|
| POST_NOTIFICATIONS | Daily summaries, budget alerts | On first launch (Android 13+) |
|
|
| INTERNET | Google Drive backup, update checks | Automatic |
|
|
| REQUEST_INSTALL_PACKAGES | Self-update APK installation | When user taps "Install Update" |
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
ledga/
|
|
├── app/
|
|
│ ├── src/
|
|
│ │ ├── main/
|
|
│ │ │ ├── java/com/ledga/app/
|
|
│ │ │ │ ├── MainActivity.kt
|
|
│ │ │ │ ├── LedgaApp.kt
|
|
│ │ │ │ ├── di/
|
|
│ │ │ │ │ ├── AppModule.kt
|
|
│ │ │ │ │ └── DatabaseModule.kt
|
|
│ │ │ │ ├── data/
|
|
│ │ │ │ │ ├── db/
|
|
│ │ │ │ │ │ ├── AppDatabase.kt
|
|
│ │ │ │ │ │ ├── Migrations.kt
|
|
│ │ │ │ │ │ ├── dao/
|
|
│ │ │ │ │ │ │ ├── TransactionDao.kt
|
|
│ │ │ │ │ │ │ ├── CategoryDao.kt
|
|
│ │ │ │ │ │ │ └── BudgetDao.kt
|
|
│ │ │ │ │ │ └── entity/
|
|
│ │ │ │ │ │ ├── Transaction.kt
|
|
│ │ │ │ │ │ ├── Category.kt
|
|
│ │ │ │ │ │ ├── Budget.kt
|
|
│ │ │ │ │ │ └── CategoryRule.kt
|
|
│ │ │ │ │ ├── repository/
|
|
│ │ │ │ │ │ ├── TransactionRepository.kt
|
|
│ │ │ │ │ │ ├── CategoryRepository.kt
|
|
│ │ │ │ │ │ └── SettingsRepository.kt
|
|
│ │ │ │ │ └── parser/
|
|
│ │ │ │ │ └── MpesaSmsParser.kt
|
|
│ │ │ │ ├── receiver/
|
|
│ │ │ │ │ └── SmsReceiver.kt
|
|
│ │ │ │ ├── worker/
|
|
│ │ │ │ │ ├── NotificationWorker.kt
|
|
│ │ │ │ │ ├── BackupWorker.kt
|
|
│ │ │ │ │ └── UpdateCheckWorker.kt
|
|
│ │ │ │ ├── ui/
|
|
│ │ │ │ │ ├── theme/
|
|
│ │ │ │ │ │ ├── Theme.kt
|
|
│ │ │ │ │ │ ├── Color.kt
|
|
│ │ │ │ │ │ └── Type.kt
|
|
│ │ │ │ │ ├── navigation/
|
|
│ │ │ │ │ │ └── AppNavigation.kt
|
|
│ │ │ │ │ ├── onboarding/
|
|
│ │ │ │ │ │ ├── OnboardingScreen.kt
|
|
│ │ │ │ │ │ └── OnboardingViewModel.kt
|
|
│ │ │ │ │ ├── home/
|
|
│ │ │ │ │ │ ├── HomeScreen.kt
|
|
│ │ │ │ │ │ └── HomeViewModel.kt
|
|
│ │ │ │ │ ├── transactions/
|
|
│ │ │ │ │ │ ├── TransactionsScreen.kt
|
|
│ │ │ │ │ │ ├── TransactionDetailSheet.kt
|
|
│ │ │ │ │ │ └── TransactionsViewModel.kt
|
|
│ │ │ │ │ ├── trends/
|
|
│ │ │ │ │ │ ├── TrendsScreen.kt
|
|
│ │ │ │ │ │ └── TrendsViewModel.kt
|
|
│ │ │ │ │ ├── settings/
|
|
│ │ │ │ │ │ ├── SettingsScreen.kt
|
|
│ │ │ │ │ │ ├── SettingsViewModel.kt
|
|
│ │ │ │ │ │ └── UnparsedSmsScreen.kt
|
|
│ │ │ │ │ └── components/
|
|
│ │ │ │ │ ├── TransactionCard.kt
|
|
│ │ │ │ │ ├── CategoryChip.kt
|
|
│ │ │ │ │ ├── SpendingChart.kt
|
|
│ │ │ │ │ └── BudgetProgressBar.kt
|
|
│ │ │ │ └── util/
|
|
│ │ │ │ ├── CurrencyFormatter.kt
|
|
│ │ │ │ └── DateUtils.kt
|
|
│ │ │ ├── res/
|
|
│ │ │ └── AndroidManifest.xml
|
|
│ │ └── test/
|
|
│ │ └── java/com/ledga/app/
|
|
│ │ └── parser/
|
|
│ │ └── MpesaSmsParserTest.kt
|
|
│ └── build.gradle.kts
|
|
├── build.gradle.kts
|
|
├── settings.gradle.kts
|
|
└── README.md
|
|
```
|
|
|
|
---
|
|
|
|
## Build Phases
|
|
|
|
### Phase 1 — Core (MVP)
|
|
- [ ] Project setup (Kotlin, Compose, Room, Hilt, Gradle)
|
|
- [ ] SMS parser — handle all M-Pesa transaction types (including M-Pesa Global, M-Shwari, KCB M-Pesa)
|
|
- [ ] SMS parser unit tests — real SMS samples for every transaction type
|
|
- [ ] Room database — transactions, categories, category rules, migrations setup
|
|
- [ ] SMS BroadcastReceiver — real-time capture
|
|
- [ ] SMS history import
|
|
- [ ] Onboarding flow — welcome screen, SMS permission with plain-language explanation, optional history import
|
|
- [ ] Home dashboard — current balance, spending total, fees total, donut chart, recent transactions
|
|
- [ ] Transactions list — grouped by day, search, filter
|
|
- [ ] Basic auto-categorization with default rules (name + paybill/till matching)
|
|
- [ ] Bottom navigation (Home, Transactions, Trends, Settings)
|
|
- [ ] Theme system (System/Light/Dark)
|
|
|
|
### Phase 2 — Polish
|
|
- [ ] Trends screen — bar charts, line charts, top merchants, fees summary, period comparison
|
|
- [ ] Font size settings (System/Small/Medium/Large/Extra Large)
|
|
- [ ] Budget system — set limits, track progress bars
|
|
- [ ] Category management — add/edit/delete custom categories
|
|
- [ ] Transaction detail bottom sheet
|
|
- [ ] Long press to re-categorize
|
|
- [ ] Category rules editor
|
|
- [ ] Unparsed SMS viewer (Settings → Data)
|
|
|
|
### Phase 3 — Notifications & Backup
|
|
- [ ] Daily spending summary notification
|
|
- [ ] Weekly summary notification
|
|
- [ ] Budget alert notifications
|
|
- [ ] Large transaction alerts
|
|
- [ ] Notification settings (toggles, time pickers)
|
|
- [ ] Google Drive auto-backup
|
|
- [ ] Google Drive restore on fresh install
|
|
- [ ] Manual export (CSV + JSON zip)
|
|
- [ ] Manual import
|
|
|
|
### Phase 4 — Distribution
|
|
- [ ] APK signing setup — generate keystore, document secure storage
|
|
- [ ] GitHub Actions CI/CD — build release APK on push to main
|
|
- [ ] GitHub Releases self-update system
|
|
- [ ] Update check on launch
|
|
- [ ] Changelog display
|
|
- [ ] APK download + install flow
|
|
- [ ] App icon and splash screen
|
|
- [ ] Final UI polish and animations
|
|
|
|
---
|
|
|
|
## App Name
|
|
|
|
**Ledga** — a play on "ledger". Short, unique, easy to say.
|
|
- Package name: `com.ledga.app`
|
|
|
|
---
|
|
|
|
## Onboarding Flow
|
|
|
|
First launch experience for non-technical users. Only shown once — a `hasCompletedOnboarding` flag in DataStore skips it on subsequent launches.
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ │
|
|
│ Welcome to Ledga │
|
|
│ │
|
|
│ Track your M-Pesa spending │
|
|
│ automatically. No typing, │
|
|
│ no manual entry. │
|
|
│ │
|
|
│ [Get Started →] │
|
|
│ │
|
|
└─────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────┐
|
|
│ │
|
|
│ Ledga reads your M-Pesa SMS │
|
|
│ to track your spending. │
|
|
│ │
|
|
│ Your messages never leave │
|
|
│ your phone. Everything stays │
|
|
│ private and offline. │
|
|
│ │
|
|
│ [Allow SMS Access →] │
|
|
│ │
|
|
└─────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────┐
|
|
│ │
|
|
│ Import your M-Pesa history? │
|
|
│ │
|
|
│ We can scan your existing │
|
|
│ M-Pesa messages to build │
|
|
│ your spending history. │
|
|
│ │
|
|
│ [Import History] │
|
|
│ [Skip — start fresh] │
|
|
│ │
|
|
│ ┌─────────────────────┐ │
|
|
│ │ ████████░░ 67% │ │
|
|
│ │ Found 342 transactions│ │
|
|
│ └─────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────┘
|
|
↓
|
|
┌─────────────────────────────────┐
|
|
│ │
|
|
│ You're all set! │
|
|
│ │
|
|
│ Ledga will now track your │
|
|
│ M-Pesa spending in the │
|
|
│ background. │
|
|
│ │
|
|
│ [Go to Dashboard →] │
|
|
│ │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests (Critical)
|
|
- **SMS Parser** — test every transaction type with real M-Pesa SMS samples
|
|
- At least 3 variations per type (different amounts, names, edge cases)
|
|
- Malformed messages (truncated SMS, missing fields)
|
|
- Messages from non-M-Pesa senders (should be ignored)
|
|
- Fuliza messages embedded in send/buy goods SMS
|
|
- M-Pesa Global with various country formats
|
|
- M-Shwari/KCB M-Pesa detection
|
|
- Amount parsing edge cases: `Ksh1.00`, `Ksh100,000.00`
|
|
- **Auto-categorization** — rule matching priority, paybill matching, fallback behavior
|
|
- **CurrencyFormatter** — Ksh formatting, large numbers, zero amounts
|
|
|
|
### Integration Tests
|
|
- Room database — DAO queries, migrations, duplicate transaction code handling
|
|
- SMS import — bulk parsing performance, duplicate skipping
|
|
|
|
### Manual Testing
|
|
- Collect real M-Pesa SMS from family members (anonymized) as test fixtures
|
|
- Test on low-end Android devices (target audience likely has budget phones)
|
|
|
|
---
|
|
|
|
## APK Signing & CI/CD
|
|
|
|
### Keystore
|
|
- Generate a release keystore and store it securely
|
|
- **CRITICAL:** Back up the keystore — if lost, existing installs can never receive updates (Android requires consistent signing)
|
|
- Store keystore password and key alias in a secure location (not in the repo)
|
|
|
|
### GitHub Actions
|
|
```yaml
|
|
# .github/workflows/release.yml
|
|
# Triggered on push to main or manual dispatch
|
|
# Steps:
|
|
# 1. Checkout code
|
|
# 2. Set up JDK 17
|
|
# 3. Decode keystore from GitHub secret
|
|
# 4. Build release APK (assembleRelease)
|
|
# 5. Sign APK
|
|
# 6. Create GitHub Release with APK asset
|
|
```
|
|
|
|
### GitHub Secrets Required
|
|
| Secret | Purpose |
|
|
|--------|---------|
|
|
| `KEYSTORE_BASE64` | Base64-encoded release keystore |
|
|
| `KEYSTORE_PASSWORD` | Keystore password |
|
|
| `KEY_ALIAS` | Key alias name |
|
|
| `KEY_PASSWORD` | Key password |
|
|
|
|
---
|
|
|
|
## Database Migrations
|
|
|
|
Schema changes are inevitable as the app evolves. Room requires explicit migrations to preserve user data.
|
|
|
|
### Strategy
|
|
- **Never use `fallbackToDestructiveMigration()`** — this destroys all user data on schema change
|
|
- Define all migrations in `Migrations.kt` as `Migration(oldVersion, newVersion)` objects
|
|
- Test every migration path with Room's `MigrationTestHelper`
|
|
- Keep a version history:
|
|
|
|
| Version | Changes |
|
|
|---------|---------|
|
|
| 1 | Initial schema (transactions, categories, budgets, category_rules) |
|
|
|
|
### Example Migration Pattern
|
|
```kotlin
|
|
val MIGRATION_1_2 = object : Migration(1, 2) {
|
|
override fun migrate(db: SupportSQLiteDatabase) {
|
|
db.execSQL("ALTER TABLE transactions ADD COLUMN newField TEXT")
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Notes
|
|
- All data stays on device — no external server, no analytics, no tracking
|
|
- SMS parsing must be thoroughly tested with real M-Pesa SMS variations
|
|
- Fuliza messages have extra fields — parser must handle gracefully
|
|
- Fuliza has 3 distinct message types: borrow (Fuliza M-PESA amount), repayment (paid to Fuliza), and reversal — each has a different format
|
|
- Some M-Pesa SMS formats may vary slightly — keep raw SMS stored for re-parsing
|
|
- Transaction code uniqueness prevents duplicate imports
|
|
- Multi-SIM: some users have two M-Pesa lines — consider adding a `simSlot` or `phoneNumber` field on transactions (extracted from SMS subscription ID) if multi-line tracking is needed later
|
|
- Google Drive backup uses `appDataFolder` scope — backups are invisible to the user in Drive. This is by design (no extra permissions), but users should know they won't find the file browsing Drive manually
|
|
- The `direction` column (INFLOW/OUTFLOW) is set at parse time based on transaction type, avoiding repeated derivation in every query for totals and charts
|