mirror of
https://github.com/whekin/household-bot.git
synced 2026-04-01 09:24:03 +00:00
feat(architecture): add finance repository adapters
This commit is contained in:
192
packages/application/src/finance-command-service.test.ts
Normal file
192
packages/application/src/finance-command-service.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import type {
|
||||
FinanceCycleRecord,
|
||||
FinanceMemberRecord,
|
||||
FinanceParsedPurchaseRecord,
|
||||
FinanceRentRuleRecord,
|
||||
FinanceRepository,
|
||||
SettlementSnapshotRecord
|
||||
} from '@household/ports'
|
||||
|
||||
import { createFinanceCommandService } from './finance-command-service'
|
||||
|
||||
class FinanceRepositoryStub implements FinanceRepository {
|
||||
member: FinanceMemberRecord | null = null
|
||||
members: readonly FinanceMemberRecord[] = []
|
||||
openCycleRecord: FinanceCycleRecord | null = null
|
||||
cycleByPeriodRecord: FinanceCycleRecord | null = null
|
||||
latestCycleRecord: FinanceCycleRecord | null = null
|
||||
rentRule: FinanceRentRuleRecord | null = null
|
||||
utilityTotal: bigint = 0n
|
||||
purchases: readonly FinanceParsedPurchaseRecord[] = []
|
||||
|
||||
lastSavedRentRule: {
|
||||
period: string
|
||||
amountMinor: bigint
|
||||
currency: 'USD' | 'GEL'
|
||||
} | null = null
|
||||
|
||||
lastUtilityBill: {
|
||||
cycleId: string
|
||||
billName: string
|
||||
amountMinor: bigint
|
||||
currency: 'USD' | 'GEL'
|
||||
createdByMemberId: string
|
||||
} | null = null
|
||||
|
||||
replacedSnapshot: SettlementSnapshotRecord | null = null
|
||||
|
||||
async getMemberByTelegramUserId(): Promise<FinanceMemberRecord | null> {
|
||||
return this.member
|
||||
}
|
||||
|
||||
async listMembers(): Promise<readonly FinanceMemberRecord[]> {
|
||||
return this.members
|
||||
}
|
||||
|
||||
async getOpenCycle(): Promise<FinanceCycleRecord | null> {
|
||||
return this.openCycleRecord
|
||||
}
|
||||
|
||||
async getCycleByPeriod(): Promise<FinanceCycleRecord | null> {
|
||||
return this.cycleByPeriodRecord
|
||||
}
|
||||
|
||||
async getLatestCycle(): Promise<FinanceCycleRecord | null> {
|
||||
return this.latestCycleRecord
|
||||
}
|
||||
|
||||
async openCycle(period: string, currency: 'USD' | 'GEL'): Promise<void> {
|
||||
this.openCycleRecord = {
|
||||
id: 'opened-cycle',
|
||||
period,
|
||||
currency
|
||||
}
|
||||
}
|
||||
|
||||
async closeCycle(): Promise<void> {}
|
||||
|
||||
async saveRentRule(period: string, amountMinor: bigint, currency: 'USD' | 'GEL'): Promise<void> {
|
||||
this.lastSavedRentRule = {
|
||||
period,
|
||||
amountMinor,
|
||||
currency
|
||||
}
|
||||
}
|
||||
|
||||
async addUtilityBill(input: {
|
||||
cycleId: string
|
||||
billName: string
|
||||
amountMinor: bigint
|
||||
currency: 'USD' | 'GEL'
|
||||
createdByMemberId: string
|
||||
}): Promise<void> {
|
||||
this.lastUtilityBill = input
|
||||
}
|
||||
|
||||
async getRentRuleForPeriod(): Promise<FinanceRentRuleRecord | null> {
|
||||
return this.rentRule
|
||||
}
|
||||
|
||||
async getUtilityTotalForCycle(): Promise<bigint> {
|
||||
return this.utilityTotal
|
||||
}
|
||||
|
||||
async listParsedPurchasesForRange(): Promise<readonly FinanceParsedPurchaseRecord[]> {
|
||||
return this.purchases
|
||||
}
|
||||
|
||||
async replaceSettlementSnapshot(snapshot: SettlementSnapshotRecord): Promise<void> {
|
||||
this.replacedSnapshot = snapshot
|
||||
}
|
||||
}
|
||||
|
||||
describe('createFinanceCommandService', () => {
|
||||
test('setRent falls back to the open cycle period when one is active', async () => {
|
||||
const repository = new FinanceRepositoryStub()
|
||||
repository.openCycleRecord = {
|
||||
id: 'cycle-1',
|
||||
period: '2026-03',
|
||||
currency: 'USD'
|
||||
}
|
||||
|
||||
const service = createFinanceCommandService(repository)
|
||||
const result = await service.setRent('700', undefined, undefined)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.period).toBe('2026-03')
|
||||
expect(result?.currency).toBe('USD')
|
||||
expect(result?.amount.amountMinor).toBe(70000n)
|
||||
expect(repository.lastSavedRentRule).toEqual({
|
||||
period: '2026-03',
|
||||
amountMinor: 70000n,
|
||||
currency: 'USD'
|
||||
})
|
||||
})
|
||||
|
||||
test('addUtilityBill returns null when no open cycle exists', async () => {
|
||||
const repository = new FinanceRepositoryStub()
|
||||
const service = createFinanceCommandService(repository)
|
||||
|
||||
const result = await service.addUtilityBill('Electricity', '55.20', 'member-1')
|
||||
|
||||
expect(result).toBeNull()
|
||||
expect(repository.lastUtilityBill).toBeNull()
|
||||
})
|
||||
|
||||
test('generateStatement persists settlement snapshot and returns member lines', async () => {
|
||||
const repository = new FinanceRepositoryStub()
|
||||
repository.latestCycleRecord = {
|
||||
id: 'cycle-2026-03',
|
||||
period: '2026-03',
|
||||
currency: 'USD'
|
||||
}
|
||||
repository.members = [
|
||||
{
|
||||
id: 'alice',
|
||||
telegramUserId: '100',
|
||||
displayName: 'Alice',
|
||||
isAdmin: true
|
||||
},
|
||||
{
|
||||
id: 'bob',
|
||||
telegramUserId: '200',
|
||||
displayName: 'Bob',
|
||||
isAdmin: false
|
||||
}
|
||||
]
|
||||
repository.rentRule = {
|
||||
amountMinor: 70000n,
|
||||
currency: 'USD'
|
||||
}
|
||||
repository.utilityTotal = 12000n
|
||||
repository.purchases = [
|
||||
{
|
||||
id: 'purchase-1',
|
||||
payerMemberId: 'alice',
|
||||
amountMinor: 3000n
|
||||
}
|
||||
]
|
||||
|
||||
const service = createFinanceCommandService(repository)
|
||||
const statement = await service.generateStatement()
|
||||
|
||||
expect(statement).toBe(
|
||||
[
|
||||
'Statement for 2026-03',
|
||||
'- Alice: 395.00 USD',
|
||||
'- Bob: 425.00 USD',
|
||||
'Total: 820.00 USD'
|
||||
].join('\n')
|
||||
)
|
||||
expect(repository.replacedSnapshot).not.toBeNull()
|
||||
expect(repository.replacedSnapshot?.cycleId).toBe('cycle-2026-03')
|
||||
expect(repository.replacedSnapshot?.currency).toBe('USD')
|
||||
expect(repository.replacedSnapshot?.totalDueMinor).toBe(82000n)
|
||||
expect(repository.replacedSnapshot?.lines.map((line) => line.netDueMinor)).toEqual([
|
||||
39500n,
|
||||
42500n
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user