mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(trips): deleting a guest keeps the costs adding up instead of dropping their payments
A guest's payer rows and recorded payments hung on the users row by ON DELETE CASCADE, so deleting the guest wiped money that had really moved: expenses they paid were left without a payer, settled payments reopened, and a custom or receipt split lost the guest's share so the balances no longer summed to zero. Deleting a guest who paid for an expense or is part of a recorded payment is now refused with a 409 (REST and the delete_trip_guest MCP tool, same text), and the share dialog says to change the payer or delete the payment first. Otherwise the guest's share is handed on: an equal split re-splits as before, a custom amount goes to the others by largest remainder in the currency's smallest step (members without an amount get one, so the expense stays a custom split), and a receipt drops the guest from its lines and is split again like the cost editor does. A legacy paid_by_user_id is cleared as on account deletion. Account deletion goes through the same re-split. The confirmation text in all languages and the wiki say what really happens. Refs #2604
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-COMP-MEMBERS-001 to FE-COMP-MEMBERS-060
|
||||
// FE-COMP-MEMBERS-001 to FE-COMP-MEMBERS-061
|
||||
import type { Mock } from 'vitest';
|
||||
import { act, render, screen, fireEvent, waitFor } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -989,7 +989,7 @@ describe('TripMembersModal', () => {
|
||||
|
||||
const trash = await screen.findByRole('button', { name: 'Remove access' });
|
||||
await user.click(trash);
|
||||
expect(screen.getByText('Remove this guest? Their assignments and cost shares will be removed too.')).toBeInTheDocument();
|
||||
expect(screen.getByText('Remove this guest? Their assignments are removed and their share of each expense goes to the others in its split.')).toBeInTheDocument();
|
||||
await user.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||
expect(deletedId).toBeNull();
|
||||
|
||||
@@ -1015,6 +1015,23 @@ describe('TripMembersModal', () => {
|
||||
expect(screen.getByText('Grandma')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-061: a guest who paid for something is kept, with the reason (#2604)', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoster([guestRow]);
|
||||
server.use(http.delete('/api/trips/1/guests/:userId', () => HttpResponse.json({ error: 'refused' }, { status: 409 })));
|
||||
render(<TripMembersModal {...defaultProps} />);
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'Remove access' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
||||
|
||||
await waitFor(() => expect(addToast).toHaveBeenCalledWith(
|
||||
'This guest paid for an expense or is part of a recorded payment. Change the payer or delete the payment first.',
|
||||
'error',
|
||||
undefined,
|
||||
));
|
||||
expect(screen.getByText('Grandma')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-COMP-MEMBERS-053: without member_manage a plain member sees no invite or guest controls', async () => {
|
||||
const stranger = buildUser({ id: 99, username: 'stranger' });
|
||||
seedStore(useAuthStore, { user: stranger, isAuthenticated: true });
|
||||
|
||||
@@ -279,8 +279,11 @@ export function useTripMembers({ isOpen, tripId, onClose, onMembersChanged }: {
|
||||
await loadMembers(true)
|
||||
await loadBudgetItems(tripId)
|
||||
toast.success(t('members.guestRemoved'))
|
||||
} catch {
|
||||
toast.error(t('members.removeError'))
|
||||
} catch (err: unknown) {
|
||||
// A guest who paid for something or is part of a recorded payment stays (#2604):
|
||||
// deleting them would take that money off the books.
|
||||
const refused = (err as { response?: { status?: number } } | null)?.response?.status === 409
|
||||
toast.error(t(refused ? 'members.guestHasPayments' : 'members.removeError'))
|
||||
} finally {
|
||||
setRemovingId(null)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user