fix(splits): editing a split must not resurrect a settled debt
ci / lint-test (push) Successful in 47s
ci / lint-test (push) Successful in 47s
The route replaces every split for a transaction rather than editing in place, so the recreated rows took the column default settled=false. Opening the split modal on a historical transaction and saving it therefore converted a discharged obligation into a live one, with nothing on screen saying so. That is not theoretical. 657 pre-2026 transactions now carry settled splits imported from SplitMyExpenses -- $37,233.28 of balance that the carryover (transaction 2348) already accounts for. Editing one would double-count its share against a debt that was paid years ago. Now carries settled and settled_at across the rewrite, per participant, the same way the rules revert route already does. Changing someone's percentage does not re-open the obligation: it was settled outside this app and stays settled. A participant who was not on the transaction before is a genuinely new obligation and correctly starts unsettled. rule-actions.ts was already safe here -- it upserts ON CONFLICT DO UPDATE SET share_percent, so it never touches the flag.
This commit is contained in:
@@ -66,18 +66,47 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carry `settled` across the rewrite.
|
||||||
|
//
|
||||||
|
// This replaces every split rather than editing in place, so without this the
|
||||||
|
// recreated rows take the column default of false — silently converting a
|
||||||
|
// discharged historical obligation into a live debt. That is not theoretical:
|
||||||
|
// 657 pre-2026 transactions carry settled splits imported from
|
||||||
|
// SplitMyExpenses, $37,233.28 of balance that the carryover (transaction
|
||||||
|
// 2348) already accounts for. Editing one would double-count its share, and
|
||||||
|
// nothing on screen would say so.
|
||||||
|
//
|
||||||
|
// Changing someone's percentage does not re-open the obligation — it was
|
||||||
|
// settled outside this app and stays settled. A participant added who was not
|
||||||
|
// there before is a genuinely new obligation and correctly starts unsettled.
|
||||||
|
const previous = await queryRaw<{
|
||||||
|
participant_id: number;
|
||||||
|
settled: boolean;
|
||||||
|
settled_at: string | null;
|
||||||
|
}>(
|
||||||
|
`SELECT participant_id, settled, settled_at
|
||||||
|
FROM transaction_splits WHERE transaction_id = $1`,
|
||||||
|
[transactionId]
|
||||||
|
);
|
||||||
|
const settledBefore = new Map(
|
||||||
|
previous.map((p) => [p.participant_id, { settled: p.settled, settled_at: p.settled_at }])
|
||||||
|
);
|
||||||
|
|
||||||
// Replace all splits for this transaction atomically
|
// Replace all splits for this transaction atomically
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
prisma.transaction_splits.deleteMany({ where: { transaction_id: transactionId } }),
|
prisma.transaction_splits.deleteMany({ where: { transaction_id: transactionId } }),
|
||||||
...splits.map((s) =>
|
...splits.map((s) => {
|
||||||
prisma.transaction_splits.create({
|
const before = settledBefore.get(s.participant_id);
|
||||||
|
return prisma.transaction_splits.create({
|
||||||
data: {
|
data: {
|
||||||
transaction_id: transactionId,
|
transaction_id: transactionId,
|
||||||
participant_id: s.participant_id,
|
participant_id: s.participant_id,
|
||||||
share_percent: s.share_percent,
|
share_percent: s.share_percent,
|
||||||
|
settled: before?.settled ?? false,
|
||||||
|
settled_at: before?.settled_at ? new Date(before.settled_at) : null,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await queryRaw<SplitRow>(
|
const result = await queryRaw<SplitRow>(
|
||||||
|
|||||||
Reference in New Issue
Block a user