mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 20:54:03 +00:00
feat(miniapp): add cycle-level billing controls
This commit is contained in:
@@ -138,6 +138,62 @@ describe('createFinanceCommandService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('getAdminCycleState prefers the open cycle and returns rent plus utility bills', async () => {
|
||||
const repository = new FinanceRepositoryStub()
|
||||
repository.openCycleRecord = {
|
||||
id: 'cycle-1',
|
||||
period: '2026-03',
|
||||
currency: 'USD'
|
||||
}
|
||||
repository.latestCycleRecord = {
|
||||
id: 'cycle-0',
|
||||
period: '2026-02',
|
||||
currency: 'USD'
|
||||
}
|
||||
repository.rentRule = {
|
||||
amountMinor: 70000n,
|
||||
currency: 'USD'
|
||||
}
|
||||
repository.utilityBills = [
|
||||
{
|
||||
id: 'utility-1',
|
||||
billName: 'Electricity',
|
||||
amountMinor: 12000n,
|
||||
currency: 'USD',
|
||||
createdByMemberId: 'alice',
|
||||
createdAt: instantFromIso('2026-03-12T12:00:00.000Z')
|
||||
}
|
||||
]
|
||||
|
||||
const service = createFinanceCommandService(repository)
|
||||
const result = await service.getAdminCycleState()
|
||||
|
||||
expect(result).toEqual({
|
||||
cycle: {
|
||||
id: 'cycle-1',
|
||||
period: '2026-03',
|
||||
currency: 'USD'
|
||||
},
|
||||
rentRule: {
|
||||
amountMinor: 70000n,
|
||||
currency: 'USD'
|
||||
},
|
||||
utilityBills: [
|
||||
{
|
||||
id: 'utility-1',
|
||||
billName: 'Electricity',
|
||||
amount: expect.objectContaining({
|
||||
amountMinor: 12000n,
|
||||
currency: 'USD'
|
||||
}),
|
||||
currency: 'USD',
|
||||
createdByMemberId: 'alice',
|
||||
createdAt: instantFromIso('2026-03-12T12:00:00.000Z')
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
test('addUtilityBill returns null when no open cycle exists', async () => {
|
||||
const repository = new FinanceRepositoryStub()
|
||||
const service = createFinanceCommandService(repository)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
import type { FinanceCycleRecord, FinanceMemberRecord, FinanceRepository } from '@household/ports'
|
||||
import type {
|
||||
FinanceCycleRecord,
|
||||
FinanceMemberRecord,
|
||||
FinanceRentRuleRecord,
|
||||
FinanceRepository
|
||||
} from '@household/ports'
|
||||
import {
|
||||
BillingCycleId,
|
||||
BillingPeriod,
|
||||
@@ -79,6 +84,19 @@ export interface FinanceDashboard {
|
||||
ledger: readonly FinanceDashboardLedgerEntry[]
|
||||
}
|
||||
|
||||
export interface FinanceAdminCycleState {
|
||||
cycle: FinanceCycleRecord | null
|
||||
rentRule: FinanceRentRuleRecord | null
|
||||
utilityBills: readonly {
|
||||
id: string
|
||||
billName: string
|
||||
amount: Money
|
||||
currency: CurrencyCode
|
||||
createdByMemberId: string | null
|
||||
createdAt: Temporal.Instant
|
||||
}[]
|
||||
}
|
||||
|
||||
async function buildFinanceDashboard(
|
||||
repository: FinanceRepository,
|
||||
periodArg?: string
|
||||
@@ -196,6 +214,7 @@ async function buildFinanceDashboard(
|
||||
export interface FinanceCommandService {
|
||||
getMemberByTelegramUserId(telegramUserId: string): Promise<FinanceMemberRecord | null>
|
||||
getOpenCycle(): Promise<FinanceCycleRecord | null>
|
||||
getAdminCycleState(periodArg?: string): Promise<FinanceAdminCycleState>
|
||||
openCycle(periodArg: string, currencyArg?: string): Promise<FinanceCycleRecord>
|
||||
closeCycle(periodArg?: string): Promise<FinanceCycleRecord | null>
|
||||
setRent(
|
||||
@@ -231,6 +250,38 @@ export function createFinanceCommandService(repository: FinanceRepository): Fina
|
||||
return repository.getOpenCycle()
|
||||
},
|
||||
|
||||
async getAdminCycleState(periodArg) {
|
||||
const cycle = periodArg
|
||||
? await repository.getCycleByPeriod(BillingPeriod.fromString(periodArg).toString())
|
||||
: ((await repository.getOpenCycle()) ?? (await repository.getLatestCycle()))
|
||||
|
||||
if (!cycle) {
|
||||
return {
|
||||
cycle: null,
|
||||
rentRule: null,
|
||||
utilityBills: []
|
||||
}
|
||||
}
|
||||
|
||||
const [rentRule, utilityBills] = await Promise.all([
|
||||
repository.getRentRuleForPeriod(cycle.period),
|
||||
repository.listUtilityBillsForCycle(cycle.id)
|
||||
])
|
||||
|
||||
return {
|
||||
cycle,
|
||||
rentRule,
|
||||
utilityBills: utilityBills.map((bill) => ({
|
||||
id: bill.id,
|
||||
billName: bill.billName,
|
||||
amount: Money.fromMinor(bill.amountMinor, bill.currency),
|
||||
currency: bill.currency,
|
||||
createdByMemberId: bill.createdByMemberId,
|
||||
createdAt: bill.createdAt
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
async openCycle(periodArg, currencyArg) {
|
||||
const period = BillingPeriod.fromString(periodArg).toString()
|
||||
const currency = parseCurrency(currencyArg, 'USD')
|
||||
|
||||
Reference in New Issue
Block a user