diff --git a/apps/bot/src/anonymous-feedback.test.ts b/apps/bot/src/anonymous-feedback.test.ts
index d93ed27..698392d 100644
--- a/apps/bot/src/anonymous-feedback.test.ts
+++ b/apps/bot/src/anonymous-feedback.test.ts
@@ -198,6 +198,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
}),
getHouseholdBillingSettings: async (householdId) => ({
householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: null,
rentCurrency: 'USD',
rentDueDay: 20,
@@ -208,6 +209,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
}),
updateHouseholdBillingSettings: async (input) => ({
householdId: input.householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: input.rentAmountMinor ?? null,
rentCurrency: input.rentCurrency ?? 'USD',
rentDueDay: input.rentDueDay ?? 20,
diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts
index 8c2d971..5c1620d 100644
--- a/apps/bot/src/index.ts
+++ b/apps/bot/src/index.ts
@@ -51,6 +51,7 @@ import {
createMiniAppRentUpdateHandler
} from './miniapp-billing'
import { createMiniAppLocalePreferenceHandler } from './miniapp-locale'
+import { createNbgExchangeRateProvider } from './nbg-exchange-rates'
const runtime = getBotRuntimeConfig()
configureLogger({
@@ -71,6 +72,9 @@ const bot = createTelegramBot(
const webhookHandler = webhookCallback(bot, 'std/http')
const financeRepositoryClients = new Map>()
const financeServices = new Map>()
+const exchangeRateProvider = createNbgExchangeRateProvider({
+ logger: getLogger('fx')
+})
const householdOnboardingService = householdConfigurationRepositoryClient
? createHouseholdOnboardingService({
repository: householdConfigurationRepositoryClient.repository
@@ -105,7 +109,12 @@ function financeServiceForHousehold(householdId: string) {
financeRepositoryClients.set(householdId, repositoryClient)
shutdownTasks.push(repositoryClient.close)
- const service = createFinanceCommandService(repositoryClient.repository)
+ const service = createFinanceCommandService({
+ householdId,
+ repository: repositoryClient.repository,
+ householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
+ exchangeRateProvider
+ })
financeServices.set(householdId, service)
return service
}
diff --git a/apps/bot/src/miniapp-admin.test.ts b/apps/bot/src/miniapp-admin.test.ts
index b20418f..7d004d0 100644
--- a/apps/bot/src/miniapp-admin.test.ts
+++ b/apps/bot/src/miniapp-admin.test.ts
@@ -118,6 +118,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
: null,
getHouseholdBillingSettings: async (householdId) => ({
householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: 70000n,
rentCurrency: 'USD',
rentDueDay: 20,
@@ -128,6 +129,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}),
updateHouseholdBillingSettings: async (input) => ({
householdId: input.householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: input.rentAmountMinor ?? 70000n,
rentCurrency: input.rentCurrency ?? 'USD',
rentDueDay: input.rentDueDay ?? 20,
@@ -370,6 +372,7 @@ describe('createMiniAppSettingsHandler', () => {
authorized: true,
settings: {
householdId: 'household-1',
+ settlementCurrency: 'GEL',
rentAmountMinor: '70000',
rentCurrency: 'USD',
rentDueDay: 20,
@@ -452,6 +455,7 @@ describe('createMiniAppUpdateSettingsHandler', () => {
authorized: true,
settings: {
householdId: 'household-1',
+ settlementCurrency: 'GEL',
rentAmountMinor: '75000',
rentCurrency: 'USD',
rentDueDay: 22,
diff --git a/apps/bot/src/miniapp-admin.ts b/apps/bot/src/miniapp-admin.ts
index 2dbe2ed..6cbba4f 100644
--- a/apps/bot/src/miniapp-admin.ts
+++ b/apps/bot/src/miniapp-admin.ts
@@ -42,6 +42,7 @@ async function readApprovalPayload(request: Request): Promise<{
async function readSettingsUpdatePayload(request: Request): Promise<{
initData: string
+ settlementCurrency?: string
rentAmountMajor?: string
rentCurrency?: string
rentDueDay: number
@@ -58,6 +59,7 @@ async function readSettingsUpdatePayload(request: Request): Promise<{
const text = await clonedRequest.text()
let parsed: {
+ settlementCurrency?: string
rentAmountMajor?: string
rentCurrency?: string
rentDueDay?: number
@@ -89,6 +91,11 @@ async function readSettingsUpdatePayload(request: Request): Promise<{
rentAmountMajor: parsed.rentAmountMajor
}
: {}),
+ ...(typeof parsed.settlementCurrency === 'string'
+ ? {
+ settlementCurrency: parsed.settlementCurrency
+ }
+ : {}),
...(typeof parsed.rentCurrency === 'string'
? {
rentCurrency: parsed.rentCurrency
@@ -212,6 +219,7 @@ async function readRentWeightPayload(request: Request): Promise<{
function serializeBillingSettings(settings: HouseholdBillingSettingsRecord) {
return {
householdId: settings.householdId,
+ settlementCurrency: settings.settlementCurrency,
rentAmountMinor: settings.rentAmountMinor?.toString() ?? null,
rentCurrency: settings.rentCurrency,
rentDueDay: settings.rentDueDay,
diff --git a/apps/bot/src/miniapp-auth.test.ts b/apps/bot/src/miniapp-auth.test.ts
index b4d7713..535d79b 100644
--- a/apps/bot/src/miniapp-auth.test.ts
+++ b/apps/bot/src/miniapp-auth.test.ts
@@ -155,6 +155,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
},
getHouseholdBillingSettings: async (householdId) => ({
householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: null,
rentCurrency: 'USD',
rentDueDay: 20,
@@ -165,6 +166,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}),
updateHouseholdBillingSettings: async (input) => ({
householdId: input.householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: input.rentAmountMinor ?? null,
rentCurrency: input.rentCurrency ?? 'USD',
rentDueDay: input.rentDueDay ?? 20,
diff --git a/apps/bot/src/miniapp-billing.test.ts b/apps/bot/src/miniapp-billing.test.ts
index 241e32d..7b280a7 100644
--- a/apps/bot/src/miniapp-billing.test.ts
+++ b/apps/bot/src/miniapp-billing.test.ts
@@ -77,6 +77,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
listHouseholdMembers: async () => [],
getHouseholdBillingSettings: async (householdId) => ({
householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: 70000n,
rentCurrency: 'USD',
rentDueDay: 20,
@@ -87,6 +88,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}),
updateHouseholdBillingSettings: async (input) => ({
householdId: input.householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: input.rentAmountMinor ?? 70000n,
rentCurrency: input.rentCurrency ?? 'USD',
rentDueDay: input.rentDueDay ?? 20,
diff --git a/apps/bot/src/miniapp-dashboard.test.ts b/apps/bot/src/miniapp-dashboard.test.ts
index 8f0a264..797cd64 100644
--- a/apps/bot/src/miniapp-dashboard.test.ts
+++ b/apps/bot/src/miniapp-dashboard.test.ts
@@ -6,6 +6,7 @@ import {
} from '@household/application'
import { instantFromIso } from '@household/domain'
import type {
+ ExchangeRateProvider,
FinanceRepository,
HouseholdConfigurationRepository,
HouseholdTopicBindingRecord
@@ -31,17 +32,19 @@ function repository(
getOpenCycle: async () => ({
id: 'cycle-1',
period: '2026-03',
- currency: 'USD'
+ currency: 'GEL'
}),
getCycleByPeriod: async () => null,
getLatestCycle: async () => ({
id: 'cycle-1',
period: '2026-03',
- currency: 'USD'
+ currency: 'GEL'
}),
openCycle: async () => {},
closeCycle: async () => {},
saveRentRule: async () => {},
+ getCycleExchangeRate: async () => null,
+ saveCycleExchangeRate: async (input) => input,
addUtilityBill: async () => {},
getRentRuleForPeriod: async () => ({
amountMinor: 70000n,
@@ -53,7 +56,7 @@ function repository(
id: 'utility-1',
billName: 'Electricity',
amountMinor: 12000n,
- currency: 'USD',
+ currency: 'GEL',
createdByMemberId: member?.id ?? 'member-1',
createdAt: instantFromIso('2026-03-12T12:00:00.000Z')
}
@@ -72,6 +75,28 @@ function repository(
}
}
+const exchangeRateProvider: ExchangeRateProvider = {
+ async getRate(input) {
+ if (input.baseCurrency === input.quoteCurrency) {
+ return {
+ baseCurrency: input.baseCurrency,
+ quoteCurrency: input.quoteCurrency,
+ rateMicros: 1_000_000n,
+ effectiveDate: input.effectiveDate,
+ source: 'nbg'
+ }
+ }
+
+ return {
+ baseCurrency: input.baseCurrency,
+ quoteCurrency: input.quoteCurrency,
+ rateMicros: 2_700_000n,
+ effectiveDate: input.effectiveDate,
+ source: 'nbg'
+ }
+ }
+}
+
function onboardingRepository(): HouseholdConfigurationRepository {
const household = {
householdId: 'household-1',
@@ -141,6 +166,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
updateMemberPreferredLocale: async () => null,
getHouseholdBillingSettings: async (householdId) => ({
householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: null,
rentCurrency: 'USD',
rentDueDay: 20,
@@ -151,6 +177,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}),
updateHouseholdBillingSettings: async (input) => ({
householdId: input.householdId,
+ settlementCurrency: input.settlementCurrency ?? 'GEL',
rentAmountMinor: input.rentAmountMinor ?? null,
rentCurrency: input.rentCurrency ?? 'USD',
rentDueDay: input.rentDueDay ?? 20,
@@ -176,16 +203,20 @@ function onboardingRepository(): HouseholdConfigurationRepository {
describe('createMiniAppDashboardHandler', () => {
test('returns a dashboard for an authenticated household member', async () => {
const authDate = Math.floor(Date.now() / 1000)
- const financeService = createFinanceCommandService(
- repository({
+ const householdRepository = onboardingRepository()
+ const financeService = createFinanceCommandService({
+ householdId: 'household-1',
+ repository: repository({
id: 'member-1',
telegramUserId: '123456',
displayName: 'Stan',
rentShareWeight: 1,
isAdmin: true
- })
- )
- const householdRepository = onboardingRepository()
+ }),
+ householdConfigurationRepository: householdRepository,
+ exchangeRateProvider
+ })
+
householdRepository.listHouseholdMembersByTelegramUserId = async () => [
{
id: 'member-1',
@@ -232,13 +263,16 @@ describe('createMiniAppDashboardHandler', () => {
authorized: true,
dashboard: {
period: '2026-03',
- currency: 'USD',
- totalDueMajor: '820.00',
+ currency: 'GEL',
+ totalDueMajor: '2010.00',
+ rentSourceAmountMajor: '700.00',
+ rentSourceCurrency: 'USD',
+ rentDisplayAmountMajor: '1890.00',
members: [
{
displayName: 'Stan',
- netDueMajor: '820.00',
- rentShareMajor: '700.00',
+ netDueMajor: '2010.00',
+ rentShareMajor: '1890.00',
utilityShareMajor: '120.00',
purchaseOffsetMajor: '0.00'
}
@@ -246,11 +280,13 @@ describe('createMiniAppDashboardHandler', () => {
ledger: [
{
title: 'Soap',
- currency: 'GEL'
+ currency: 'GEL',
+ displayCurrency: 'GEL'
},
{
title: 'Electricity',
- currency: 'USD'
+ currency: 'GEL',
+ displayCurrency: 'GEL'
}
]
}
@@ -258,16 +294,20 @@ describe('createMiniAppDashboardHandler', () => {
})
test('returns 400 for malformed JSON bodies', async () => {
- const financeService = createFinanceCommandService(
- repository({
+ const householdRepository = onboardingRepository()
+ const financeService = createFinanceCommandService({
+ householdId: 'household-1',
+ repository: repository({
id: 'member-1',
telegramUserId: '123456',
displayName: 'Stan',
rentShareWeight: 1,
isAdmin: true
- })
- )
- const householdRepository = onboardingRepository()
+ }),
+ householdConfigurationRepository: householdRepository,
+ exchangeRateProvider
+ })
+
householdRepository.listHouseholdMembersByTelegramUserId = async () => [
{
id: 'member-1',
diff --git a/apps/bot/src/miniapp-dashboard.ts b/apps/bot/src/miniapp-dashboard.ts
index fc13291..8469ab7 100644
--- a/apps/bot/src/miniapp-dashboard.ts
+++ b/apps/bot/src/miniapp-dashboard.ts
@@ -89,6 +89,11 @@ export function createMiniAppDashboardHandler(options: {
period: dashboard.period,
currency: dashboard.currency,
totalDueMajor: dashboard.totalDue.toMajorString(),
+ rentSourceAmountMajor: dashboard.rentSourceAmount.toMajorString(),
+ rentSourceCurrency: dashboard.rentSourceAmount.currency,
+ rentDisplayAmountMajor: dashboard.rentDisplayAmount.toMajorString(),
+ rentFxRateMicros: dashboard.rentFxRateMicros?.toString() ?? null,
+ rentFxEffectiveDate: dashboard.rentFxEffectiveDate,
members: dashboard.members.map((line) => ({
memberId: line.memberId,
displayName: line.displayName,
@@ -104,6 +109,10 @@ export function createMiniAppDashboardHandler(options: {
title: entry.title,
amountMajor: entry.amount.toMajorString(),
currency: entry.currency,
+ displayAmountMajor: entry.displayAmount.toMajorString(),
+ displayCurrency: entry.displayCurrency,
+ fxRateMicros: entry.fxRateMicros?.toString() ?? null,
+ fxEffectiveDate: entry.fxEffectiveDate,
actorDisplayName: entry.actorDisplayName,
occurredAt: entry.occurredAt
}))
diff --git a/apps/bot/src/miniapp-locale.test.ts b/apps/bot/src/miniapp-locale.test.ts
index 90b677e..9dca72f 100644
--- a/apps/bot/src/miniapp-locale.test.ts
+++ b/apps/bot/src/miniapp-locale.test.ts
@@ -130,6 +130,7 @@ function repository(): HouseholdConfigurationRepository {
},
getHouseholdBillingSettings: async (householdId) => ({
householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: null,
rentCurrency: 'USD',
rentDueDay: 20,
@@ -140,6 +141,7 @@ function repository(): HouseholdConfigurationRepository {
}),
updateHouseholdBillingSettings: async (input) => ({
householdId: input.householdId,
+ settlementCurrency: 'GEL',
rentAmountMinor: input.rentAmountMinor ?? null,
rentCurrency: input.rentCurrency ?? 'USD',
rentDueDay: input.rentDueDay ?? 20,
diff --git a/apps/bot/src/nbg-exchange-rates.ts b/apps/bot/src/nbg-exchange-rates.ts
new file mode 100644
index 0000000..7514ef8
--- /dev/null
+++ b/apps/bot/src/nbg-exchange-rates.ts
@@ -0,0 +1,157 @@
+import { FX_RATE_SCALE_MICROS, type CurrencyCode } from '@household/domain'
+import type { ExchangeRateProvider } from '@household/ports'
+import type { Logger } from '@household/observability'
+
+interface NbgCurrencyPayload {
+ code: string
+ quantity: number
+ rateFormated?: string
+ rate?: number
+ validFromDate?: string
+}
+
+interface NbgDayPayload {
+ date?: string
+ currencies?: NbgCurrencyPayload[]
+}
+
+function parseScaledDecimal(value: string, scale: number): bigint {
+ const trimmed = value.trim()
+ const match = /^([+-]?)(\d+)(?:\.(\d+))?$/.exec(trimmed)
+ if (!match) {
+ throw new Error(`Invalid decimal value: ${value}`)
+ }
+
+ const [, sign, whole, fraction = ''] = match
+ const normalizedFraction = fraction.padEnd(scale, '0').slice(0, scale)
+ const digits = `${whole}${normalizedFraction}`
+ const parsed = BigInt(digits)
+
+ return sign === '-' ? -parsed : parsed
+}
+
+function divideRoundedHalfUp(dividend: bigint, divisor: bigint): bigint {
+ if (divisor === 0n) {
+ throw new Error('Division by zero')
+ }
+
+ const quotient = dividend / divisor
+ const remainder = dividend % divisor
+ if (remainder * 2n >= divisor) {
+ return quotient + 1n
+ }
+
+ return quotient
+}
+
+export function createNbgExchangeRateProvider(
+ options: {
+ fetchImpl?: typeof fetch
+ logger?: Logger
+ } = {}
+): ExchangeRateProvider {
+ const fetchImpl = options.fetchImpl ?? fetch
+ const cache = new Map>()
+
+ async function getGelRate(currency: CurrencyCode, effectiveDate: string) {
+ if (currency === 'GEL') {
+ return {
+ gelRateMicros: FX_RATE_SCALE_MICROS,
+ effectiveDate
+ }
+ }
+
+ const cacheKey = `${currency}:${effectiveDate}`
+ const existing = cache.get(cacheKey)
+ if (existing) {
+ return existing
+ }
+
+ const request = (async () => {
+ const url = new URL('https://nbg.gov.ge/gw/api/ct/monetarypolicy/currencies/en/json/')
+ url.searchParams.set('currencies', currency)
+ url.searchParams.set('date', effectiveDate)
+
+ const response = await fetchImpl(url)
+ if (!response.ok) {
+ throw new Error(`NBG request failed: ${response.status}`)
+ }
+
+ const payload = (await response.json()) as NbgDayPayload[]
+ const day = payload[0]
+ const currencyPayload = day?.currencies?.find((entry) => entry.code === currency)
+ if (!currencyPayload) {
+ throw new Error(`NBG rate missing for ${currency} on ${effectiveDate}`)
+ }
+
+ const quantity = Number(currencyPayload.quantity)
+ if (!Number.isFinite(quantity) || quantity <= 0) {
+ throw new Error(`Invalid NBG quantity for ${currency}: ${currencyPayload.quantity}`)
+ }
+
+ const rateString =
+ currencyPayload.rateFormated ??
+ (typeof currencyPayload.rate === 'number' ? currencyPayload.rate.toFixed(6) : null)
+ if (!rateString) {
+ throw new Error(`Invalid NBG rate for ${currency} on ${effectiveDate}`)
+ }
+
+ const effective =
+ currencyPayload.validFromDate?.slice(0, 10) ?? day?.date?.slice(0, 10) ?? effectiveDate
+ const gelRateMicros = divideRoundedHalfUp(parseScaledDecimal(rateString, 6), BigInt(quantity))
+
+ options.logger?.debug(
+ {
+ event: 'fx.nbg_fetched',
+ currency,
+ requestedDate: effectiveDate,
+ effectiveDate: effective,
+ gelRateMicros: gelRateMicros.toString()
+ },
+ 'Fetched NBG exchange rate'
+ )
+
+ return {
+ gelRateMicros,
+ effectiveDate: effective
+ }
+ })()
+
+ cache.set(cacheKey, request)
+ return request
+ }
+
+ return {
+ async getRate(input) {
+ if (input.baseCurrency === input.quoteCurrency) {
+ return {
+ baseCurrency: input.baseCurrency,
+ quoteCurrency: input.quoteCurrency,
+ rateMicros: FX_RATE_SCALE_MICROS,
+ effectiveDate: input.effectiveDate,
+ source: 'nbg'
+ }
+ }
+
+ const [base, quote] = await Promise.all([
+ getGelRate(input.baseCurrency, input.effectiveDate),
+ getGelRate(input.quoteCurrency, input.effectiveDate)
+ ])
+
+ const rateMicros = divideRoundedHalfUp(
+ base.gelRateMicros * FX_RATE_SCALE_MICROS,
+ quote.gelRateMicros
+ )
+ const effectiveDate =
+ base.effectiveDate > quote.effectiveDate ? base.effectiveDate : quote.effectiveDate
+
+ return {
+ baseCurrency: input.baseCurrency,
+ quoteCurrency: input.quoteCurrency,
+ rateMicros,
+ effectiveDate,
+ source: 'nbg'
+ }
+ }
+ }
+}
diff --git a/apps/bot/src/purchase-topic-ingestion.ts b/apps/bot/src/purchase-topic-ingestion.ts
index 9d97a79..5c1d06a 100644
--- a/apps/bot/src/purchase-topic-ingestion.ts
+++ b/apps/bot/src/purchase-topic-ingestion.ts
@@ -51,7 +51,8 @@ export type PurchaseMessageIngestionResult =
export interface PurchaseMessageIngestionRepository {
save(
record: PurchaseTopicRecord,
- llmFallback?: PurchaseParserLlmFallback
+ llmFallback?: PurchaseParserLlmFallback,
+ defaultCurrency?: 'GEL' | 'USD'
): Promise
}
@@ -121,7 +122,7 @@ export function createPurchaseMessageRepository(databaseUrl: string): {
})
const repository: PurchaseMessageIngestionRepository = {
- async save(record, llmFallback) {
+ async save(record, llmFallback, defaultCurrency) {
const matchedMember = await db
.select({ id: schema.members.id })
.from(schema.members)
@@ -140,11 +141,18 @@ export function createPurchaseMessageRepository(databaseUrl: string): {
{
rawText: record.rawText
},
- llmFallback
- ? {
- llmFallback
- }
- : {}
+ {
+ ...(llmFallback
+ ? {
+ llmFallback
+ }
+ : {}),
+ ...(defaultCurrency
+ ? {
+ defaultCurrency
+ }
+ : {})
+ }
).catch((error) => {
parserError = error instanceof Error ? error.message : 'Unknown parser error'
return null
@@ -324,7 +332,7 @@ export function registerPurchaseTopicIngestion(
}
try {
- const status = await repository.save(record, options.llmFallback)
+ const status = await repository.save(record, options.llmFallback, 'GEL')
const acknowledgement = buildPurchaseAcknowledgement(status, 'en')
if (status.status === 'created') {
@@ -394,7 +402,14 @@ export function registerConfiguredPurchaseTopicIngestion(
}
try {
- const status = await repository.save(record, options.llmFallback)
+ const billingSettings = await householdConfigurationRepository.getHouseholdBillingSettings(
+ record.householdId
+ )
+ const status = await repository.save(
+ record,
+ options.llmFallback,
+ billingSettings.settlementCurrency
+ )
const householdChat = await householdConfigurationRepository.getHouseholdChatByHouseholdId(
record.householdId
)
diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx
index b5a2c60..8799afa 100644
--- a/apps/miniapp/src/App.tsx
+++ b/apps/miniapp/src/App.tsx
@@ -1,4 +1,4 @@
-import { Match, Switch, createMemo, createSignal, onMount, type JSX } from 'solid-js'
+import { Match, Show, Switch, createMemo, createSignal, onMount, type JSX } from 'solid-js'
import { dictionary, type Locale } from './i18n'
import {
@@ -160,6 +160,18 @@ function memberBaseDueMajor(member: MiniAppDashboard['members'][number]): string
)
}
+function ledgerPrimaryAmount(entry: MiniAppDashboard['ledger'][number]): string {
+ return `${entry.displayAmountMajor} ${entry.displayCurrency}`
+}
+
+function ledgerSecondaryAmount(entry: MiniAppDashboard['ledger'][number]): string | null {
+ if (entry.currency === entry.displayCurrency && entry.amountMajor === entry.displayAmountMajor) {
+ return null
+ }
+
+ return `${entry.amountMajor} ${entry.currency}`
+}
+
function App() {
const [locale, setLocale] = createSignal('en')
const [session, setSession] = createSignal({
@@ -184,6 +196,7 @@ function App() {
const [savingCycleRent, setSavingCycleRent] = createSignal(false)
const [savingUtilityBill, setSavingUtilityBill] = createSignal(false)
const [billingForm, setBillingForm] = createSignal({
+ settlementCurrency: 'GEL' as 'USD' | 'GEL',
rentAmountMajor: '',
rentCurrency: 'USD' as 'USD' | 'GEL',
rentDueDay: 20,
@@ -195,7 +208,7 @@ function App() {
const [newCategoryName, setNewCategoryName] = createSignal('')
const [cycleForm, setCycleForm] = createSignal({
period: defaultCyclePeriod(),
- currency: 'USD' as 'USD' | 'GEL',
+ currency: 'GEL' as 'USD' | 'GEL',
rentAmountMajor: '',
utilityCategorySlug: '',
utilityAmountMajor: ''
@@ -267,12 +280,14 @@ function App() {
)
setCycleForm((current) => ({
...current,
+ currency: current.currency || payload.settings.settlementCurrency,
utilityCategorySlug:
current.utilityCategorySlug ||
payload.categories.find((category) => category.isActive)?.slug ||
''
}))
setBillingForm({
+ settlementCurrency: payload.settings.settlementCurrency,
rentAmountMajor: payload.settings.rentAmountMinor
? (Number(payload.settings.rentAmountMinor) / 100).toFixed(2)
: '',
@@ -299,7 +314,10 @@ function App() {
setCycleForm((current) => ({
...current,
period: payload.cycle?.period ?? current.period,
- currency: payload.cycle?.currency ?? payload.rentRule?.currency ?? current.currency,
+ currency:
+ payload.cycle?.currency ??
+ adminSettings()?.settings.settlementCurrency ??
+ current.currency,
rentAmountMajor: payload.rentRule
? (Number(payload.rentRule.amountMinor) / 100).toFixed(2)
: '',
@@ -385,25 +403,30 @@ function App() {
setSession(demoSession)
setDashboard({
period: '2026-03',
- currency: 'USD',
- totalDueMajor: '414.00',
+ currency: 'GEL',
+ totalDueMajor: '1030.00',
+ rentSourceAmountMajor: '700.00',
+ rentSourceCurrency: 'USD',
+ rentDisplayAmountMajor: '1932.00',
+ rentFxRateMicros: '2760000',
+ rentFxEffectiveDate: '2026-03-17',
members: [
{
memberId: 'demo-member',
displayName: 'Demo Resident',
- rentShareMajor: '175.00',
+ rentShareMajor: '483.00',
utilityShareMajor: '32.00',
purchaseOffsetMajor: '-14.00',
- netDueMajor: '193.00',
+ netDueMajor: '501.00',
explanations: ['Equal utility split', 'Shared purchase offset']
},
{
memberId: 'member-2',
displayName: 'Alice',
- rentShareMajor: '175.00',
+ rentShareMajor: '483.00',
utilityShareMajor: '32.00',
purchaseOffsetMajor: '14.00',
- netDueMajor: '221.00',
+ netDueMajor: '529.00',
explanations: ['Equal utility split']
}
],
@@ -414,6 +437,10 @@ function App() {
title: 'Soap',
amountMajor: '30.00',
currency: 'GEL',
+ displayAmountMajor: '30.00',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
actorDisplayName: 'Alice',
occurredAt: '2026-03-12T11:00:00.000Z'
},
@@ -423,6 +450,10 @@ function App() {
title: 'Electricity',
amountMajor: '120.00',
currency: 'GEL',
+ displayAmountMajor: '120.00',
+ displayCurrency: 'GEL',
+ fxRateMicros: null,
+ fxEffectiveDate: null,
actorDisplayName: 'Alice',
occurredAt: '2026-03-12T12:00:00.000Z'
}
@@ -612,6 +643,10 @@ function App() {
}
: current
)
+ setCycleForm((current) => ({
+ ...current,
+ currency: cycleState()?.cycle?.currency ?? settings.settlementCurrency
+ }))
} finally {
setSavingBillingSettings(false)
}
@@ -914,10 +949,11 @@ function App() {
{entry.title}
-
- {entry.amountMajor} {entry.currency}
-
+ {ledgerPrimaryAmount(entry)}
+
+ {(secondary) => {secondary()}
}
+
{entry.actorDisplayName ?? copy().ledgerActorFallback}
))}
@@ -936,10 +972,11 @@ function App() {
{entry.title}
-
- {entry.amountMajor} {entry.currency}
-
+ {ledgerPrimaryAmount(entry)}
+
+ {(secondary) => {secondary()}
}
+
{entry.actorDisplayName ?? copy().ledgerActorFallback}
))}
@@ -973,6 +1010,17 @@ function App() {
cycleState()?.cycle?.currency ?? cycleForm().currency
)}
+
+ {(data) => (
+
+ {copy().shareRent}: {data().rentSourceAmountMajor}{' '}
+ {data().rentSourceCurrency}
+ {data().rentSourceCurrency !== data().currency
+ ? ` -> ${data().rentDisplayAmountMajor} ${data().currency}`
+ : ''}
+
+ )}
+