feat(miniapp): show payment activity in dashboard

This commit is contained in:
2026-03-10 17:16:16 +04:00
parent 1b490fa4a5
commit c292518760
7 changed files with 135 additions and 15 deletions

View File

@@ -32,6 +32,14 @@ class FinanceRepositoryStub implements FinanceRepository {
createdByMemberId: string | null
createdAt: Instant
}[] = []
paymentRecords: readonly {
id: string
memberId: string
kind: 'rent' | 'utilities'
amountMinor: bigint
currency: 'USD' | 'GEL'
recordedAt: Instant
}[] = []
lastSavedRentRule: {
period: string
amountMinor: bigint
@@ -126,7 +134,7 @@ class FinanceRepositoryStub implements FinanceRepository {
}
async listPaymentRecordsForCycle() {
return []
return this.paymentRecords
}
async listParsedPurchasesForRange(): Promise<readonly FinanceParsedPurchaseRecord[]> {
@@ -345,6 +353,16 @@ describe('createFinanceCommandService', () => {
occurredAt: instantFromIso('2026-03-12T11:00:00.000Z')
}
]
repository.paymentRecords = [
{
id: 'payment-1',
memberId: 'alice',
kind: 'rent',
amountMinor: 50000n,
currency: 'GEL',
recordedAt: instantFromIso('2026-03-18T12:00:00.000Z')
}
]
const service = createService(repository)
const dashboard = await service.generateDashboard()
@@ -355,18 +373,20 @@ describe('createFinanceCommandService', () => {
expect(dashboard?.rentSourceAmount.toMajorString()).toBe('700.00')
expect(dashboard?.rentDisplayAmount.toMajorString()).toBe('1890.00')
expect(dashboard?.members.map((line) => line.netDue.amountMinor)).toEqual([99000n, 102000n])
expect(dashboard?.ledger.map((entry) => entry.title)).toEqual(['Soap', 'Electricity'])
expect(dashboard?.ledger.map((entry) => entry.currency)).toEqual(['GEL', 'GEL'])
expect(dashboard?.ledger.map((entry) => entry.displayCurrency)).toEqual(['GEL', 'GEL'])
expect(dashboard?.ledger.map((entry) => entry.title)).toEqual(['Soap', 'Electricity', 'rent'])
expect(dashboard?.ledger.map((entry) => entry.kind)).toEqual(['purchase', 'utility', 'payment'])
expect(dashboard?.ledger.map((entry) => entry.currency)).toEqual(['GEL', 'GEL', 'GEL'])
expect(dashboard?.ledger.map((entry) => entry.displayCurrency)).toEqual(['GEL', 'GEL', 'GEL'])
expect(dashboard?.ledger.map((entry) => entry.paymentKind)).toEqual([null, null, 'rent'])
expect(statement).toBe(
[
'Statement for 2026-03',
'Rent: 700.00 USD (~1890.00 GEL)',
'- Alice: due 990.00 GEL, paid 0.00 GEL, remaining 990.00 GEL',
'- Alice: due 990.00 GEL, paid 500.00 GEL, remaining 490.00 GEL',
'- Bob: due 1020.00 GEL, paid 0.00 GEL, remaining 1020.00 GEL',
'Total due: 2010.00 GEL',
'Total paid: 0.00 GEL',
'Total remaining: 2010.00 GEL'
'Total paid: 500.00 GEL',
'Total remaining: 1510.00 GEL'
].join('\n')
)
expect(repository.replacedSnapshot).not.toBeNull()

View File

@@ -4,6 +4,7 @@ import type {
ExchangeRateProvider,
FinanceCycleRecord,
FinanceMemberRecord,
FinancePaymentKind,
FinanceRentRuleRecord,
FinanceRepository,
HouseholdConfigurationRepository
@@ -93,7 +94,7 @@ export interface FinanceDashboardMemberLine {
export interface FinanceDashboardLedgerEntry {
id: string
kind: 'purchase' | 'utility'
kind: 'purchase' | 'utility' | 'payment'
title: string
amount: Money
currency: CurrencyCode
@@ -103,6 +104,7 @@ export interface FinanceDashboardLedgerEntry {
fxEffectiveDate: string | null
actorDisplayName: string | null
occurredAt: string | null
paymentKind: FinancePaymentKind | null
}
export interface FinanceDashboard {
@@ -379,7 +381,8 @@ async function buildFinanceDashboard(
actorDisplayName: bill.createdByMemberId
? (memberNameById.get(bill.createdByMemberId) ?? null)
: null,
occurredAt: bill.createdAt.toString()
occurredAt: bill.createdAt.toString(),
paymentKind: null
})),
...convertedPurchases.map(({ purchase, converted }) => ({
id: purchase.id,
@@ -392,7 +395,22 @@ async function buildFinanceDashboard(
fxRateMicros: converted.fxRateMicros,
fxEffectiveDate: converted.fxEffectiveDate,
actorDisplayName: memberNameById.get(purchase.payerMemberId) ?? null,
occurredAt: purchase.occurredAt?.toString() ?? null
occurredAt: purchase.occurredAt?.toString() ?? null,
paymentKind: null
})),
...paymentRecords.map((payment) => ({
id: payment.id,
kind: 'payment' as const,
title: payment.kind,
amount: Money.fromMinor(payment.amountMinor, payment.currency),
currency: payment.currency,
displayAmount: Money.fromMinor(payment.amountMinor, payment.currency),
displayCurrency: payment.currency,
fxRateMicros: null,
fxEffectiveDate: null,
actorDisplayName: memberNameById.get(payment.memberId) ?? null,
occurredAt: payment.recordedAt.toString(),
paymentKind: payment.kind
}))
].sort((left, right) => {
if (left.occurredAt === right.occurredAt) {