mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 13:54:02 +00:00
Stabilize purchase functionality: fix ID prefix, uniqueness, and split participant inclusion
This commit is contained in:
@@ -61,6 +61,7 @@ class FinanceRepositoryStub implements FinanceRepository {
|
||||
replacedSnapshot: SettlementSnapshotRecord | null = null
|
||||
cycleExchangeRates = new Map<string, FinanceCycleExchangeRateRecord>()
|
||||
lastUpdatedPurchaseInput: Parameters<FinanceRepository['updateParsedPurchase']>[0] | null = null
|
||||
lastAddedPurchaseInput: Parameters<FinanceRepository['addParsedPurchase']>[0] | null = null
|
||||
|
||||
async getMemberByTelegramUserId(): Promise<FinanceMemberRecord | null> {
|
||||
return this.member
|
||||
@@ -131,6 +132,24 @@ class FinanceRepositoryStub implements FinanceRepository {
|
||||
this.lastUtilityBill = input
|
||||
}
|
||||
|
||||
async addParsedPurchase(input: Parameters<FinanceRepository['addParsedPurchase']>[0]) {
|
||||
this.lastAddedPurchaseInput = input
|
||||
return {
|
||||
id: 'purchase-1',
|
||||
payerMemberId: input.payerMemberId,
|
||||
amountMinor: input.amountMinor,
|
||||
currency: input.currency,
|
||||
description: input.description,
|
||||
occurredAt: input.occurredAt,
|
||||
splitMode: input.splitMode ?? 'equal',
|
||||
participants: (input.participants ?? []).map((p) => ({
|
||||
memberId: p.memberId,
|
||||
included: p.included ?? true,
|
||||
shareAmountMinor: p.shareAmountMinor
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async updateUtilityBill() {
|
||||
return null
|
||||
}
|
||||
@@ -655,10 +674,12 @@ describe('createFinanceCommandService', () => {
|
||||
participants: [
|
||||
{
|
||||
memberId: 'alice',
|
||||
included: true,
|
||||
shareAmountMinor: 2000n
|
||||
},
|
||||
{
|
||||
memberId: 'bob',
|
||||
included: true,
|
||||
shareAmountMinor: 1000n
|
||||
}
|
||||
]
|
||||
|
||||
@@ -644,6 +644,7 @@ export interface FinanceCommandService {
|
||||
mode: 'equal' | 'custom_amounts'
|
||||
participants: readonly {
|
||||
memberId: string
|
||||
included?: boolean
|
||||
shareAmountMajor?: string
|
||||
}[]
|
||||
}
|
||||
@@ -652,6 +653,24 @@ export interface FinanceCommandService {
|
||||
amount: Money
|
||||
currency: CurrencyCode
|
||||
} | null>
|
||||
addPurchase(
|
||||
description: string,
|
||||
amountArg: string,
|
||||
payerMemberId: string,
|
||||
currencyArg?: string,
|
||||
split?: {
|
||||
mode: 'equal' | 'custom_amounts'
|
||||
participants: readonly {
|
||||
memberId: string
|
||||
included?: boolean
|
||||
shareAmountMajor?: string
|
||||
}[]
|
||||
}
|
||||
): Promise<{
|
||||
purchaseId: string
|
||||
amount: Money
|
||||
currency: CurrencyCode
|
||||
}>
|
||||
deletePurchase(purchaseId: string): Promise<boolean>
|
||||
addPayment(
|
||||
memberId: string,
|
||||
@@ -900,6 +919,7 @@ export function createFinanceCommandService(
|
||||
splitMode: split.mode,
|
||||
participants: split.participants.map((participant) => ({
|
||||
memberId: participant.memberId,
|
||||
included: participant.included ?? true,
|
||||
shareAmountMinor:
|
||||
participant.shareAmountMajor !== undefined
|
||||
? Money.fromMajor(participant.shareAmountMajor, currency).amountMinor
|
||||
@@ -920,6 +940,67 @@ export function createFinanceCommandService(
|
||||
}
|
||||
},
|
||||
|
||||
async addPurchase(description, amountArg, payerMemberId, currencyArg, split) {
|
||||
const settings = await householdConfigurationRepository.getHouseholdBillingSettings(
|
||||
dependencies.householdId
|
||||
)
|
||||
const currency = parseCurrency(currencyArg, settings.settlementCurrency)
|
||||
const amount = Money.fromMajor(amountArg, currency)
|
||||
|
||||
const openCycle = await repository.getOpenCycle()
|
||||
if (!openCycle) {
|
||||
throw new DomainError(DOMAIN_ERROR_CODE.INVALID_SETTLEMENT_INPUT, 'No open billing cycle')
|
||||
}
|
||||
|
||||
if (split?.mode === 'custom_amounts') {
|
||||
if (split.participants.some((p) => p.shareAmountMajor === undefined)) {
|
||||
throw new DomainError(
|
||||
DOMAIN_ERROR_CODE.INVALID_SETTLEMENT_INPUT,
|
||||
'Purchase custom split must include explicit share amounts for every participant'
|
||||
)
|
||||
}
|
||||
|
||||
const totalMinor = split.participants.reduce(
|
||||
(sum, p) => sum + Money.fromMajor(p.shareAmountMajor!, currency).amountMinor,
|
||||
0n
|
||||
)
|
||||
if (totalMinor !== amount.amountMinor) {
|
||||
throw new DomainError(
|
||||
DOMAIN_ERROR_CODE.INVALID_SETTLEMENT_INPUT,
|
||||
'Purchase custom split must add up to the full amount'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const created = await repository.addParsedPurchase({
|
||||
cycleId: openCycle.id,
|
||||
payerMemberId,
|
||||
amountMinor: amount.amountMinor,
|
||||
currency,
|
||||
description: description.trim().length > 0 ? description.trim() : null,
|
||||
occurredAt: nowInstant(),
|
||||
...(split
|
||||
? {
|
||||
splitMode: split.mode,
|
||||
participants: split.participants.map((participant) => ({
|
||||
memberId: participant.memberId,
|
||||
included: participant.included ?? true,
|
||||
shareAmountMinor:
|
||||
participant.shareAmountMajor !== undefined
|
||||
? Money.fromMajor(participant.shareAmountMajor, currency).amountMinor
|
||||
: null
|
||||
}))
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
return {
|
||||
purchaseId: created.id,
|
||||
amount,
|
||||
currency
|
||||
}
|
||||
},
|
||||
|
||||
deletePurchase(purchaseId) {
|
||||
return repository.deleteParsedPurchase(purchaseId)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user