You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Any-currency workspaces (1.12.0) (#121)
* Foundation for any-currency workspaces: one catalogue, one formatter
A workspace keeps its books in exactly one currency, chosen at creation.
Everything about how a figure is written now follows from that currency,
through one catalogue, instead of being assumed Indian in dozens of places.
core/currency.dart CurrencySpec per ISO code: symbol, decimals, digit
grouping (South Asian lakh/crore, or thousands), and
the usual FY start. 32 currencies; an unknown code
still formats sensibly.
formatMoney symbol, decimals and grouping from the spec. A
million dollars is $1,000,000.00, not $10,00,000.00.
formatMoneyCompact K/L/Cr for South Asian currencies, K/M/B otherwise.
roundMoney rounds to the active currency's minor units, read
from MoneyContext, which the workspace controller
keeps in step on every change. Whole yen; three
places for the dinar, which two would have lost.
formatDate the phone's convention, always an English variant.
"Sep 20, 2026" in the US, "20 Sept 2026" as before.
Every default is INR, so every existing workspace and all 277 existing
tests are unaffected — the suite passes untouched, which is the proof
the change is invisible to current users. 20 new tests pin the rest.
This lands first so the parallel work that follows builds against one
API rather than several.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Accounts, ledgers and overview screens follow the workspace currency
Eleven screens and the widget sync in main.dart each worked out the
currency for themselves with `activeWorkspace?.baseCurrency ?? 'INR'`.
They now ask WorkspaceController.currency, the one place that answers.
The account form and detail sheet assumed an Indian bank:
- The credit limit field had a hardcoded rupee prefix. It now shows
the workspace's own symbol ($, AED, and so on).
- IFSC and CIF showed for every bank account. They now show only in an
INR workspace. Elsewhere the form asks for one optional "Routing
code" (sort code, BSB, SWIFT or ABA number). It is kept in the
account's existing `ifsc` field, which needs no change to the model
or the rules. A workspace's currency never changes, so in any one
workspace the field only ever means one thing. Outside India the
form never writes `cif`, so saving cannot clear a stored value
through a field it did not show. The detail sheet labels the rows
the same way the form does.
Other India assumptions fixed along the way:
- The ledger labelled tax lines "Tax / GST" everywhere. That label is
now used only in INR workspaces; elsewhere it reads "Tax".
- Budgets wrote a January-start financial year as a span ("FY
2026-27"). That is wrong for a calendar year, which is the default
for most non-INR currencies. It now reads "FY 2026".
- The calendar wrote dates day-first whatever the phone's locale. It
now follows the phone, so a US phone shows "September 20". Indian,
British and Australian phones show exactly what they did before.
An INR workspace looks exactly as before. The detail rows and the bank
fields of the saved data now come from two small functions, so tests
can check them without Firestore. New tests cover the form at 360dp
(IFSC/CIF in INR, the routing code in USD, the symbol on the credit
limit in INR/USD/AED/JPY), what gets saved, and the detail rows. Five of
the widget tests fail on the previous form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Keep the day-field counter note on the function it describes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Ask for the currency when a workspace is created
A workspace's currency is now permanent, so it can no longer be guessed.
Until now both ways of creating one went through createPersonalWorkspace,
which wrote 'INR' and an April financial year for everyone.
createPersonalWorkspace now takes the currency as a required argument and
builds the document through newWorkspaceFields, a pure function that
writes the chosen code, takes fyStartMonth from that currency's usual
year, and refuses a code the app does not offer.
First sign-in: a person with no workspace is no longer given one
silently. AuthController sets needsWorkspace, and the router holds them
on a new /welcome screen (every other route, deep links included, waits
behind it) until they confirm a currency. The phone's region preselects
one, falling back to INR, and the screen says plainly it can't be changed
later. A failed creation stays on the screen with a retry; back leaves
the app as it would from any root screen, and the person returns here on
next launch; "Use a different account" signs out. People who already
belong to a workspace, including anyone who joined one by invite, never
see the screen. If the membership check answers from an empty offline
cache, onboarding now fails instead of treating that as "no workspace".
Profile's New workspace dialog asks for the currency too, preselected
from the active workspace's, with the same note.
widgets/currency_picker.dart is the shared piece: a searchable sheet
(code, name or symbol) listing kCurrencies with code, name and symbol,
a CurrencyField that opens it, and the lock note.
24 new tests at 360dp phone width cover the preselection, the payload,
the redirect gate, the picker, the first-run screen and the dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Lock the workspace currency; keep shared entries in their own; test the app in CI
A workspace keeps its books in one currency, fixed at creation. This makes
that true at every layer, and stops the shared ledger from mislabelling
amounts between people whose books are in different currencies.
CI. The "Flutter analyze and test" job runs pub get, analyze and the full
test suite in mobile/, using the same subosito/flutter-action and stable
channel as the APK build. flutter analyze fails on infos by default, so it
runs with --no-fatal-infos: warnings and errors still fail the job. The pub
cache is keyed on pubspec.yaml, since no lock file is tracked.
Rules. baseCurrency must be a three-letter upper-case code on create and
cannot change on update, for the owner too. Workspaces from before the field
existed are read as INR everywhere, so the rule compares effective currencies
(missing = INR): those workspaces stay editable and may have 'INR' written in,
but can never become anything else.
Settings. The currency dropdown becomes a read-only field (symbol, code,
name) saying it was set at creation. updateWorkspace no longer takes a
currency at all, so no screen can try.
Shared ledger. New entries (expenses, settlements, re-sends) carry the
creator's workspace currency; an entry without one is INR, which every
existing entry is. Entries show in their own currency, and balances are kept
per partner per currency, since a rupee and a dollar do not net. Accepting,
settling and resolving write into the reader's own books, so they are only
allowed from a workspace in the entry's currency: the mutation refuses with
SharedCurrencyMismatch, and the screen explains and disables the action
instead of posting $50 as 50 rupees. Rejecting stays open everywhere. The
sharedEntries rule validates currency when present but does not require it,
so app versions already installed keep working. Neither party can change it
afterwards.
The rules tests honour FIRESTORE_EMULATOR_HOST, so a second checkout can
run them on another port.
Tests: Flutter 314 passed (17 new). Rules 48 passed (10 new); with the old
rules, 5 of the new ones fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Money, debts, dues and reports speak the workspace's currency
Debts, dues, transactions, reports and the line editor now take the
currency from WorkspaceController.currency instead of assuming India.
Currency
Ten scattered `baseCurrency ?? 'INR'` fallbacks now read
WorkspaceController.currency. The five amount fields that were hardwired
to "₹ " (line amount, TDS, debt repayment, due payment, a debt's opening
amount) now show the workspace's symbol. The trailing space is kept
because Flutter puts no gap after a prefix, and it keeps INR fields
exactly as they were.
India-only tax, INR workspaces only
Line editor: TDS is India's tax deducted at source, so only INR
workspaces are asked for it. Elsewhere the field is hidden but never
cleared: a line that already carries TDS keeps it through any edit. Of
the tax heads, only "Perquisite" is specific to Indian income tax, so
only that one is dropped for other currencies. A head a line already
carries is still offered, so an old line never shows a blank head.
Reports: the tax pack PDF is working papers for an Indian CA, with a TDS
column on every table and a Form 26AS cross-check. Relabelling it would
not change that, so other currencies get no pack and no TDS. Every
currency still gets taxable totals by head, plus a CSV of them to hand
to an accountant.
Recurring titles
{MMM} and {MMMM} follow AppLocale.date, as formatDate() already does,
so a month token and {DATE} in the same title always spell the month
the same way. With the default en_IN locale, output is unchanged
("Sept").
Found on the way
The line sheet disposed its draft as soon as its future resolved, while
the sheet was still sliding away and rebuilding its fields with that
draft's controllers. It now disposes once the sheet has actually gone.
The Type, Head and Purpose pickers use the width they already have
(isExpanded), so a long label stays inside the field on a 360dp phone
with a large system font.
Tests: 26 new, 323 in total, all passing. Widget tests run at 360dp
against light fakes of the two Firestore controllers. The non-INR
expectations fail on the previous code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Tidy what the merged currency work left behind
One financial-year lookup on the workspace controller replaces twenty
scattered 'fyStartMonth ?? 4' fallbacks; the line editor calls a tax line
'Tax' outside INR, as the ledger already did; the login tagline and the
More screen stop assuming rupees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Read world statement formats; draw any currency and script in PDFs
Statement import:
- Amounts had every comma stripped, so a European "1.234,56" imported as
1.23456 and "12,50" as 1250, with no error. ColumnMapping now carries a
decimal separator, detected from the figures in the amount, debit, credit
and balance columns. Each figure votes only when its shape is decisive
(both marks: the last is the decimal; a repeated mark is grouping; a lone
mark with 1, 2 or 4+ digits after it is the decimal); "1,234" and "1.234"
abstain, and ',' wins only outright, so Indian, US and British files read
exactly as before. buildImportRows detects it itself when the mapping has
none, and parseAmountText refuses a figure carrying the other mark's
decimals rather than misreading it.
- Currency signs (any Unicode Sc sign, with prefixes such as S$ and HK$) and
the ISO codes the app knows are stripped from amount cells, as whole words
only. Trailing-minus debits ("1.234,56-"), U+2212 minus and space or
apostrophe digit grouping are read.
- Delimiter detection now picks the delimiter that splits most lines into the
same number of fields, not the one that occurs most often, so a ';' file
whose descriptions and decimal commas hold as many commas still splits on
';'.
- A new mapping breaks date-order ties the way the phone writes dates
(en_US month-first, otherwise day-first); saved profiles keep their order.
- The mapping step shows the detected decimal mark, lets it be changed, and
saves it in the per-account profile. The row editor reads "12,50" as 12.50.
PDFs and messages:
- The khata and tax-pack PDFs are set in an embedded Noto Sans (static
Regular and Bold instances of google/fonts' variable Noto Sans with Noto
Sans Arabic merged in, subset to Latin, Devanagari, Arabic and the
currency signs, no hinting or layout tables; about 139 KB a weight, OFL
licence alongside). Helvetica could draw neither the rupee sign nor any
non-Latin name.
- Money in the PDFs and in the share and reminder messages follows the
workspace currency's symbol, decimals and grouping; the 'Rs' and en_IN
special cases are gone, and pdfSafe no longer turns the rupee sign into
'Rs '.
- Text is laid out with a text direction set, which is what makes Syncfusion
join Arabic letters and order them right to left. Devanagari draws but is
not shaped (conjuncts show the virama); the short-i sign is reordered so
simple syllables read right. Characters outside the font draw as '?'
rather than vanishing. Text bounds no longer have fixed heights, since
Syncfusion silently drops a line taller than its bounds.
- PdfTypeface.load() reads the font; a PDF built before it has loaded falls
back to Helvetica, with ISO codes for the signs Helvetica lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
* Guard against hardcoded rupees, load the PDF font up front, 1.12.0
A source scan fails the suite if any screen falls back to INR, prefixes an
amount with a literal rupee, or assumes an April financial year. The PDF
font is read at startup and awaited before each PDF is built, so the first
PDF in a session carries the currency's own sign.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jkAcgzuuxTfBiAgMLWgC4
---------
Co-authored-by: Claude <noreply@anthropic.com>