services.gradle.org intermittently returns 504 / times out while the
wrapper fetches the distribution, failing releases and emailing on every
blip. Warm the wrapper with a backoff retry before the real build/test
steps in both jobs.
- 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