mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-14 11:36:13 +01:00
* do balance rounding only on full balances rather than on every expense * use "public balances" calculated from reimbursements to show on balance page * fixes for totals that did not work as expected * prettier
117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
import { getGroupExpenses } from '@/lib/api'
|
|
import { Participant } from '@prisma/client'
|
|
import { match } from 'ts-pattern'
|
|
|
|
export type Balances = Record<
|
|
Participant['id'],
|
|
{ paid: number; paidFor: number; total: number }
|
|
>
|
|
|
|
export type Reimbursement = {
|
|
from: Participant['id']
|
|
to: Participant['id']
|
|
amount: number
|
|
}
|
|
|
|
export function getBalances(
|
|
expenses: NonNullable<Awaited<ReturnType<typeof getGroupExpenses>>>,
|
|
): Balances {
|
|
const balances: Balances = {}
|
|
|
|
for (const expense of expenses) {
|
|
const paidBy = expense.paidById
|
|
const paidFors = expense.paidFor
|
|
|
|
if (!balances[paidBy]) balances[paidBy] = { paid: 0, paidFor: 0, total: 0 }
|
|
balances[paidBy].paid += expense.amount
|
|
|
|
const totalPaidForShares = paidFors.reduce(
|
|
(sum, paidFor) => sum + paidFor.shares,
|
|
0,
|
|
)
|
|
let remaining = expense.amount
|
|
paidFors.forEach((paidFor, index) => {
|
|
if (!balances[paidFor.participantId])
|
|
balances[paidFor.participantId] = { paid: 0, paidFor: 0, total: 0 }
|
|
|
|
const isLast = index === paidFors.length - 1
|
|
|
|
const [shares, totalShares] = match(expense.splitMode)
|
|
.with('EVENLY', () => [1, paidFors.length])
|
|
.with('BY_SHARES', () => [paidFor.shares, totalPaidForShares])
|
|
.with('BY_PERCENTAGE', () => [paidFor.shares, totalPaidForShares])
|
|
.with('BY_AMOUNT', () => [paidFor.shares, totalPaidForShares])
|
|
.exhaustive()
|
|
|
|
const dividedAmount = isLast
|
|
? remaining
|
|
: (expense.amount * shares) / totalShares
|
|
remaining -= dividedAmount
|
|
balances[paidFor.participantId].paidFor += dividedAmount
|
|
})
|
|
}
|
|
|
|
// rounding and add total
|
|
for (const participantId in balances) {
|
|
// add +0 to avoid negative zeros
|
|
balances[participantId].paidFor =
|
|
Math.round(balances[participantId].paidFor) + 0
|
|
balances[participantId].paid = Math.round(balances[participantId].paid) + 0
|
|
|
|
balances[participantId].total =
|
|
balances[participantId].paid - balances[participantId].paidFor
|
|
}
|
|
return balances
|
|
}
|
|
|
|
export function getPublicBalances(reimbursements: Reimbursement[]): Balances {
|
|
const balances: Balances = {}
|
|
reimbursements.forEach((reimbursement) => {
|
|
if (!balances[reimbursement.from])
|
|
balances[reimbursement.from] = { paid: 0, paidFor: 0, total: 0 }
|
|
|
|
if (!balances[reimbursement.to])
|
|
balances[reimbursement.to] = { paid: 0, paidFor: 0, total: 0 }
|
|
|
|
balances[reimbursement.from].paidFor += reimbursement.amount
|
|
balances[reimbursement.from].total -= reimbursement.amount
|
|
|
|
balances[reimbursement.to].paid += reimbursement.amount
|
|
balances[reimbursement.to].total += reimbursement.amount
|
|
})
|
|
return balances
|
|
}
|
|
|
|
export function getSuggestedReimbursements(
|
|
balances: Balances,
|
|
): Reimbursement[] {
|
|
const balancesArray = Object.entries(balances)
|
|
.map(([participantId, { total }]) => ({ participantId, total }))
|
|
.filter((b) => b.total !== 0)
|
|
balancesArray.sort((b1, b2) => b2.total - b1.total)
|
|
const reimbursements: Reimbursement[] = []
|
|
while (balancesArray.length > 1) {
|
|
const first = balancesArray[0]
|
|
const last = balancesArray[balancesArray.length - 1]
|
|
const amount = first.total + last.total
|
|
if (first.total > -last.total) {
|
|
reimbursements.push({
|
|
from: last.participantId,
|
|
to: first.participantId,
|
|
amount: -last.total,
|
|
})
|
|
first.total = amount
|
|
balancesArray.pop()
|
|
} else {
|
|
reimbursements.push({
|
|
from: last.participantId,
|
|
to: first.participantId,
|
|
amount: first.total,
|
|
})
|
|
last.total = amount
|
|
balancesArray.shift()
|
|
}
|
|
}
|
|
return reimbursements.filter(({ amount }) => Math.round(amount) + 0 !== 0)
|
|
}
|