fix(reconcile): prevent split/tag double-counting on reconciled transactions

Move splits, tags and overrides from manual to statement side on reconcile
(delete from manual after copying) instead of just copying. Add read-time
filter to exclude reconciled manual transactions from balance and shared
transaction queries. Also adds participant filter to shared expenses page.
This commit is contained in:
2026-05-11 19:15:41 +10:00
parent ce67e38d77
commit b8cd1b0f89
5 changed files with 41 additions and 10 deletions
+7 -4
View File
@@ -210,12 +210,15 @@ export function useParticipantBalances(tagIds?: string[]) {
});
}
export function useSharedTransactions(tagIds?: string[]) {
export function useSharedTransactions(tagIds?: string[], participantId?: number) {
return useQuery({
queryKey: ["shared-transactions", tagIds],
queryKey: ["shared-transactions", tagIds, participantId],
queryFn: async () => {
const params = tagIds?.length ? `?tag_ids=${tagIds.join(",")}` : "";
const res = await fetch(`/api/shared-transactions${params}`);
const sp = new URLSearchParams();
if (tagIds?.length) sp.set("tag_ids", tagIds.join(","));
if (participantId) sp.set("participant_id", String(participantId));
const query = sp.toString() ? `?${sp.toString()}` : "";
const res = await fetch(`/api/shared-transactions${query}`);
return res.json();
},
});
+11 -1
View File
@@ -329,6 +329,7 @@ export async function getParticipantBalances(ownerId: number, tagIds?: number[])
JOIN transactions t ON t.id = ts.transaction_id
LEFT JOIN statements s ON s.id = t.statement_id
WHERE COALESCE(t.owner_id, s.owner_id) = $1 AND ts.participant_id != $1
AND NOT (t.statement_id IS NULL AND t.reconciled_with_id IS NOT NULL)
${tagFilter}
UNION ALL
@@ -341,6 +342,7 @@ export async function getParticipantBalances(ownerId: number, tagIds?: number[])
JOIN transactions t ON t.id = ts.transaction_id
LEFT JOIN statements s ON s.id = t.statement_id
WHERE ts.participant_id = $1 AND COALESCE(t.owner_id, s.owner_id) != $1
AND NOT (t.statement_id IS NULL AND t.reconciled_with_id IS NOT NULL)
${tagFilter}
) splits ON splits.pid = p.id
${paymentsJoin}
@@ -542,7 +544,7 @@ export async function getTags() {
`);
}
export async function getSharedTransactions(ownerId: number, tagIds?: number[], noTags?: boolean) {
export async function getSharedTransactions(ownerId: number, tagIds?: number[], noTags?: boolean, participantId?: number) {
const params: unknown[] = [ownerId];
let tagClause = "";
if (noTags) {
@@ -552,6 +554,12 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
tagClause = `AND EXISTS (SELECT 1 FROM transaction_tags tt WHERE tt.transaction_id = t.id AND tt.tag_id = ANY($2::int[]))`;
}
let participantClause = "";
if (participantId) {
params.push(participantId);
participantClause = `AND EXISTS (SELECT 1 FROM transaction_splits ts_p WHERE ts_p.transaction_id = t.id AND ts_p.participant_id = $${params.length})`;
}
const rows = await queryRaw<TransactionRow & { split_data: string }>(`
SELECT t.*,
o.category_override, o.merchant_normalized as merchant_override, o.notes,
@@ -584,7 +592,9 @@ export async function getSharedTransactions(ownerId: number, tagIds?: number[],
AND EXISTS (SELECT 1 FROM transaction_splits ts_me WHERE ts_me.transaction_id = t.id AND ts_me.participant_id = $1)
)
)
AND NOT (t.statement_id IS NULL AND t.reconciled_with_id IS NOT NULL)
${tagClause}
${participantClause}
GROUP BY t.id, o.category_override, o.merchant_normalized, o.notes, s.bank_name, s.owner_id, p_owner.name, src.created_at
ORDER BY t.transaction_date DESC
`, params);