Awa: Bill Transactions in Awa

By Awa Engineering Team| 31 August 2023

Most of what we’re building in Awa isn’t rocket science and that’s kind of the point. It’s not that complicated… but we’d like you to see how cool it is.

We start with one simple fact: one person covers the cost, and everyone else owes their portion. From there, we keep the process transparent list the participants (minus the payer) and split the total down to two decimals. If tehre's a leftopver cent, we add it to the first share so everything balances perfectly, At creation :

-The total amount is normalized to two decimal places using BigDecimal to eliminate floating-point drift.
-The amount is divided evenly across participants.
-Any rounding remainder is allocated to the first share to guarantee that Σ shares = total.


val total = BigDecimal.valueOf(req.amount).setScale(2, HALF_UP)
val per = total.divide(BigDecimal.valueOf(participants.size.toLong()), 2, HALF_UP)
...

This kinda just keeps aggregates consistent across the UI, repository, and database.

Payment status
Paying people back usually involves a bit of back-and-forth. To keep it simple, we treat each share as being in one of three states: pending, marked as paid, or confirmed.(Thanks Dr Bautista lmao , and Michael Sips - FINITE AUTOMATA mit opencourse ware)

-PENDING → nothing claimed yet
-MARK_PAID → “I sent it”
-CONFIRMED → “Yep, received”

Only the bill creator is authorized to confirm transitions into CONFIRMED. Once all shares reach CONFIRMED, the bill itself resolves to isPaid = true.

Share state transitions (“—” = no-op)
From \ Event markPaid (any debtor) confirm (creator only) reject (creator only)
PENDING MARKED_PAID
MARKED_PAID CONFIRMED PENDING
CONFIRMED


Idempotency by construction
We avoided explicit idempotency keys by structuring operations to be idempotent:

- Re-tapping “Mark as paid” is a noop( no-operation we just wanted to sound smart 😔 ) if the share is already MARKED_PAID or CONFIRMED.
- Confirming a share twice results in no state change.
-Rejecting a share resets it to PENDING and clears its timestamp, ensuring consistent state replay.

This pattern allows the UI to optimistically update without risking double-entries or inconsistent transactions.

Frontend synchronization
On the Flutter side:
-Amount inputs are formatted dynamically using a currency symbol provider. (( this doesn't even work well and we don't even know how to fix it 😔))
-Splits are recalculated client-side before the request is sent, ensuring the user sees per-person amounts immediately.
- The payer’s displayed “amount” reflects their net receivable, not the full bill amount, aligning the UI with what's in the backend

This guarantees that what the user sees is exactly what will be persisted.

Contracts as bill templates
Recurring obligations (e.g., rent, utilities) are treated as contracts: bills flagged with isContract = true. In the current implementation, contracts are just bills filtered into a separate list( so they're easier to find). Future iterations can extend this model with recurrence metadata and automated bill generation.


Aggregates: you owe / you’re owed
Two aggregates should help the dashboard( we didnt later add this , couldnt make it work well):

- “You owe” = sum of the current user’s non-CONFIRMED shares.
- “You’re owed” = sum of all outstanding shares on bills where the user is the creator.
- The latter is computed server-side during bill fetch, so it accounts for shares that have been MARKED_PAID but not yet confirmed.


Well , there goes our simple maths created service for sorting bills

****************************************

States (Q):

Q={PENDING,MARKED_PAID,CONFIRMED}

Alphabet (Σ): Σ={markPaid,confirm,reject}

Transition function (δ):

δ:Q×Σ→Q

Defined as: δ(PENDING,markPaid)=MARKED_PAID
δ(MARKED_PAID,confirm)=CONFIRMED
δ(MARKED_PAID,reject)=PENDING
All other transitions are identity (noop).

Start state (q₀): q0=PENDING Accepting state(s) (F):

F={CONFIRMED}