Mark any transaction as Fuel or Car Service from the detail sheet (a tag
separate from its category). New 'Car expenses' screen (You tab) shows the
car's lifetime cost plus per-tag This Week / This Month / All Time, summed
across every SIM. DB v4->v5 adds a nullable carTag column (additive, no
rebuild).
Also: notifications now carry a tap intent, so tapping opens the app and
clears them instead of lingering until manually swiped.
SIM detection: history import now attributes per-message sub_id (sim_id
fallback), receiver falls back to the sole active SIM when the OEM omits
the subscription extra, accounts re-link by phone number on SIM/slot
changes, and READ_PHONE_STATE is requested so lines get carrier names.
Accuracy: KCB withdrawals stored the savings-pocket balance as the wallet
balance and INFLOWs as OUTFLOW (goal contributions counted withdrawals).
Wallet-balance regex now wins and 'from your KCB/M-Shwari' parses as
inflow. Combined view sums per-line balances and Fuliza facts instead of
mixing lines; insights run account-agnostic for deterministic output.
Self-healing on launch: SIM sync, incremental catch-up SMS import (covers
receiver kills by OEM battery savers), one-time unassigned-only SIM
backfill, and a versioned re-parse that heals stored rows after parser
fixes while preserving user-set fields.
- New 'People' segment on Activity: browse who you've sent money to or
received from, aggregated by name with totals and counts. Search by name,
filter by a minimum total with a slider, toggle Sent/Received, and tap a
person to see every transaction with them.
- Moved the 'Large' filter chip to the front of the row so it's visible
without scrolling (it was the last, off-screen chip).
- New 'Large' tab on Transactions with a slider (Ksh 1K–100K) to set the
threshold live — drag for bigger/smaller. Shares the large-transaction
alert threshold, so it's now adjustable from the UI for the first time.
- Spending heatmap was blank for non-UTC users: getDailySpending bucketed
by UTC midnight while the grid looked up local-midnight keys (3h off in
EAT → every cell zero). Now buckets by local day. Also: cells size to fit
so the 24-week year view no longer overflows, and the legend shows the
real 5-step alpha ramp.
- Insights 'All clear' empty-state body text was onSurface on a light card
(invisible in dark mode) — now derives ink from the card like the rest.
Insight cards (and the Goals/Update/Backfill tonal cards) have a fixed
light pastel background in both themes, but their content text used the
theme's onSurface — near-white in dark mode — so headlines vanished into
the light card. BentoCard already derived contrasting title/overline ink
via onTonal; the content lambdas now do the same for body, headline, and
actions.
Two display fixes:
1. 'Available to borrow' showed the raw ceiling (e.g. 18K) ignoring what's
already drawn. M-PESA's 'Available Fuliza limit' is the headroom at that
SMS, so ceiling = that reading + its outstanding (0 after a full clear);
borrowable now = ceiling − current outstanding. Card also shows the limit.
2. Balance card had a 'balance > 0' filter that skipped genuine Ksh0.00
readings and surfaced a stale positive balance instead. A real zero (spent
down / running on Fuliza) now shows; only parser-default zeros on
repayment/reversal/companion/unknown rows are skipped.
M-PESA now splits a Fuliza-funded transaction into two SMS sharing one
transaction code: the payment and a Fuliza companion carrying the
outstanding/limit. The unique index on transactionCode silently dropped
the companion (OnConflictStrategy.IGNORE), so outstanding read 0 while
the user actually owed money. insertTransaction now merges a same-code
companion onto the payment row (either arrival order); reparse preserves
the merged Fuliza fields since the payment's own rawSms lacks them.
Also: parse the 'Access Fee charged' phrasing as the Fuliza fee, and add
a running spinner + 'Importing X of Y' progress to Import SMS history
(previously it ran with no feedback).
The Fuliza card showed a bare outstanding figure with no recency, so a
stale reading (e.g. an old full-clear at 0) looked current. It now shows
'as of <date>' and, when the latest Fuliza SMS is over a day old, prompts
re-running Import SMS history — Safaricom moved Fuliza to a dedicated
sender, so older messages need a one-time re-import to be read in.
Marking a recipient as 'my own account' reclassifies its transactions
into a new transfer category excluded from all spending figures —
bank/card top-ups are moved money, not spending.
Fuliza coverage: accept the dedicated FULIZA sender id in the receiver
and both importers (borrows were going untracked while repayments were
captured), capture the available Fuliza limit from any SMS carrying it,
record borrow interest as a fee, and surface an outstanding/available
card on Home.
DB v3->v4: additive ALTER TABLEs only (categories.isTransfer,
transactions.fulizaLimit) + idempotent seed of the My Accounts category.
Spending figures were inflated: Fuliza repayments double-counted the
already-counted borrow-backed purchase, M-Shwari/KCB deposits counted
own-money savings as spending, and reversed transactions never came
back out of the total. All spending queries now exclude these.
Also adds a month-by-month breakdown card to Trends (12 months,
delta vs previous month) and captures the credited amount on
standard-format reversal SMS instead of recording 0.0.
The Fuliza outstanding insight is stateful — it should disappear the
moment the underlying outstanding hits 0. Today it sticks until
manually dismissed, even after data updates.
InsightEngine now tracks a STATEFUL_KEYS set (just "fuliza:outstanding"
for now). If a rule that owns one of those keys returns no candidates
in this run, the row is deleted outright rather than being left around
as a tombstone.
SettingsViewModel.reparseAll() now also calls insightsRepository
.generateAll() once the row update completes. So the moment the user
finishes "Re-parse ALL transactions", any stale Fuliza alert reading
the limit-as-outstanding goes away, and any newly correct alerts show
up immediately.
- Re-parse ALL now shows a spinner + "Re-parsing all transactions…"
while running, so taps don't feel ignored. Button is hidden during
the work; reappears after the result line.
- Empty result (no transactions to re-parse) shows a hint pointing at
"Import SMS history" instead of a confusing "0 of 0".
- Home period selector defaults to Today instead of Month — the spec
showed Month first, but in practice users want to see today's
spending at a glance and switch out only when needed.
Two parser bugs around Fuliza auto-pay that produced wrong balance and
phantom-outstanding alerts:
1. BALANCE_REGEX required the literal "Ksh" before the amount, but the
full-clear auto-pay SMS ends "Your M-PESA balance is 8581.08." with
no Ksh prefix. Balance was lost → stored as 0 → Home's
getLatestTransactionWithBalance filter (balance > 0) fell back to a
stale older transaction. Made the Ksh optional.
2. parseFulizaAutoPay was stuffing the *available Fuliza limit* into
fulizaOutstanding. Those are opposites (available = ceiling - owed).
After a full clear that meant FulizaRule saw the full 14k limit and
raised "You owe Ksh 14,000" — incorrect. Now:
- full clear -> fulizaOutstanding = 0 (rule correctly bails)
- partial -> fulizaOutstanding = null (rule defers to the
previous Fuliza tx;
avoids reporting the
available-limit value)
Reparse all transactions
- New "Re-parse ALL transactions" entry in You → Data. Walks every
stored row through MpesaSmsParser and updates parser-derived fields
in place, preserving user fields (categoryId, accountId, note, id).
- Needed because the broken regex above silently corrupted historical
rows on import; this is the no-data-loss path back to correctness.
DonutChart was hardcoded to a 180dp Canvas. When placed in a 50/50 Row
inside the "Where it went" card on the S24 Ultra, the canvas overflowed
the parent column and clipped into an oval. Switched to
fillMaxWidth + aspectRatio(1f) + fillMaxSize Canvas so the donut always
stays circular and scales to whatever the parent gives it. Stroke width
also scales (18% of the box, capped at 32dp).
Home screen lost the Today / This Week / Month period selector in the
bento rewrite (HomeViewModel still exposed it, the UI just never
rendered it). Restored as a LedgaChip row above the hero card. The
hero overline now also tracks the selection ("SPENT TODAY" /
"SPENT THIS WEEK" / "SPENT THIS MONTH").
MIGRATION_1_2 was using ALTER TABLE to add transactions.accountId, but
SQLite can't add a FOREIGN KEY via ALTER. Room validates schema after
migration and threw IllegalStateException ("Migration didn't properly
handle: transactions") because the FK to mpesa_accounts was missing.
The whole DB stayed at v1, blocking every read/write — which is why
SMS import + SMS receive looked dead too.
Fix: standard SQLite table-swap dance. Create transactions_new with
both FKs declared, INSERT … SELECT old rows in, drop the old table,
rename. All indices recreated. Defensive DROP IF EXISTS in case a
prior failed run left junk.
BentoCard now picks title + overline colors from the tonal background
luminance instead of unconditionally using onSurface. On dark theme
the Insights teaser had near-white text on the (light-theme) pale
green bg — invisible. Now it adapts:
- light tonal bg → dark ink
- dark tonal bg → light ink
HomeScreen Insights teaser switched from hardcoded LedgaAccentSoft to
MaterialTheme.colorScheme.primaryContainer so the bg itself also
adapts to theme.
- LedgaTopBar now applies WindowInsets.statusBars padding. Previously
enableEdgeToEdge was on but the top bar drew at y=0, so the avatar
and trailing icons sat under the system clock / WiFi indicator.
- PillTabBar uses WindowInsets.navigationBars padding instead of a
hardcoded 21dp bottom so the floating pill sits above the system
gesture pill on devices like the S24 Ultra.
- Typography switched from Google-Fonts Inter to FontFamily.SansSerif.
Inter requires a downloadable-fonts round-trip via Google Play
Services that doesn't always succeed; the fallback was hitting
FontFamily.Default, which inherits the user's system font choice —
on Samsung One UI that's frequently a handwritten / script style
and broke the type rhythm everywhere. SansSerif uses the platform
sans (One UI Sans on Galaxy, Roboto elsewhere) regardless of user
preference.
Settings > Data now has "Re-parse Unparsed Messages" button that
re-runs the fixed parser on all UNKNOWN transactions, properly
categorizing previously missed Fuliza auto-deductions, withdrawals,
airtime, and reversals.
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