3 Commits

Author SHA1 Message Date
Peter Smit
fc0feee736 Use decimal.js to validate uneven amounts
All checks were successful
CI / checks (push) Successful in 56s
2025-11-08 11:11:43 +01:00
Derek
548a8dc5ee Add cascading delete behavior to activity.group. (#448) 2025-11-08 09:49:40 +01:00
Derek
157ed4fd96 bugfix: Fix share values being incorrectly divided by 100 in expense form (#453)
* Update expense-form.tsx to handle shares as strings

Proposing fix to #424

The issue is in the data flow between the form and the schema transform function:

When editing existing expenses: Form loads shares by dividing database values by 100 (e.g., 200 / 100 = 2), but loads them as numbers
When users change values: Input fields return strings via enforceCurrencyPattern
Schema transform: Only multiplies by 100 for string values, not number values
Result: Modified shares (strings) get multiplied by 100, unmodified shares (numbers) stay as-is

Proposed fix: handle all shares consistently as strings throughout the form

* Add type assertions to fix TypeScript errors in expense form

Fix formatting.

---------

Co-authored-by: yllar <yllar.pajus@gmail.com>
2025-11-08 09:34:57 +01:00
4 changed files with 30 additions and 24 deletions

View File

@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "Activity" DROP CONSTRAINT "Activity_groupId_fkey";
-- AddForeignKey
ALTER TABLE "Activity" ADD CONSTRAINT "Activity_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -116,7 +116,7 @@ model ExpensePaidFor {
model Activity {
id String @id
group Group @relation(fields: [groupId], references: [id])
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
groupId String
time DateTime @default(now())
activityType ActivityType

View File

@@ -81,7 +81,7 @@ const getDefaultSplittingOptions = (
splitMode: 'EVENLY' as const,
paidFor: group.participants.map(({ id }) => ({
participant: id,
shares: 1,
shares: '1' as any, // Use string to ensure consistent schema handling
})),
}
@@ -113,7 +113,7 @@ const getDefaultSplittingOptions = (
splitMode: parsedDefaultSplitMode.splitMode,
paidFor: parsedDefaultSplitMode.paidFor.map((paidFor) => ({
participant: paidFor.participant,
shares: paidFor.shares / 100,
shares: (paidFor.shares / 100).toString() as any, // Convert to string for consistent schema handling
})),
}
}
@@ -197,10 +197,9 @@ export function ExpenseForm({
paidBy: expense.paidById,
paidFor: expense.paidFor.map(({ participantId, shares }) => ({
participant: participantId,
shares:
expense.splitMode === 'BY_AMOUNT'
? amountAsDecimal(shares, groupCurrency)
: shares / 100,
shares: (expense.splitMode === 'BY_AMOUNT'
? amountAsDecimal(shares, groupCurrency)
: (shares / 100).toString()) as any, // Convert to string to ensure consistent handling
})),
splitMode: expense.splitMode,
saveDefaultSplittingOptions: false,
@@ -226,7 +225,7 @@ export function ExpenseForm({
searchParams.get('to')
? {
participant: searchParams.get('to')!,
shares: 1,
shares: '1' as any, // String for consistent form handling
}
: undefined,
],
@@ -359,9 +358,9 @@ export function ExpenseForm({
if (!editedParticipants.includes(participant.participant)) {
return {
...participant,
shares: Number(
amountPerRemaining.toFixed(groupCurrency.decimal_digits),
),
shares: amountPerRemaining.toFixed(
groupCurrency.decimal_digits,
) as any, // Keep as string for consistent schema handling
}
}
return participant
@@ -825,11 +824,11 @@ export function ExpenseForm({
? []
: group.participants.map((p) => ({
participant: p.id,
shares:
paidFor.find((pfor) => pfor.participant === p.id)
?.shares ?? 1,
shares: (paidFor.find(
(pfor) => pfor.participant === p.id,
)?.shares ?? '1') as any, // Use string to ensure consistent schema handling
}))
form.setValue('paidFor', newPaidFor, {
form.setValue('paidFor', newPaidFor as any, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
@@ -886,9 +885,9 @@ export function ExpenseForm({
...field.value,
{
participant: id,
shares: 1,
shares: '1', // Use string to ensure consistent schema handling
},
],
] as any,
options,
)
: form.setValue(

View File

@@ -1,4 +1,6 @@
import { RecurrenceRule, SplitMode } from '@prisma/client'
import Decimal from 'decimal.js'
import * as z from 'zod'
export const groupFormSchema = z
@@ -148,14 +150,14 @@ export const expenseFormSchema = z
break // noop
case 'BY_AMOUNT': {
const sum = expense.paidFor.reduce(
(sum, { shares }) => sum + Number(shares),
0,
(sum, { shares }) => new Decimal(shares).add(sum),
new Decimal(0),
)
if (sum !== expense.amount) {
const detail =
sum < expense.amount
? `${((expense.amount - sum) / 100).toFixed(2)} missing`
: `${((sum - expense.amount) / 100).toFixed(2)} surplus`
if (!sum.equals(new Decimal(expense.amount))) {
// const detail =
// sum < expense.amount
// ? `${((expense.amount - sum) / 100).toFixed(2)} missing`
// : `${((sum - expense.amount) / 100).toFixed(2)} surplus`
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'amountSum',