mitlist/fe/src/components/__tests__/SettleShareModal.spec.ts
google-labs-jules[bot] f1152c5745 feat: Implement traceable expense splitting and settlement activities
Backend:
- Added `SettlementActivity` model to track payments against specific expense shares.
- Added `status` and `paid_at` to `ExpenseSplit` model.
- Added `overall_settlement_status` to `Expense` model.
- Implemented CRUD for `SettlementActivity`, including logic to update parent expense/split statuses.
- Updated `Expense` CRUD to initialize new status fields.
- Defined Pydantic schemas for `SettlementActivity` and updated `Expense/ExpenseSplit` schemas.
- Exposed API endpoints for creating/listing settlement activities and settling shares.
- Adjusted group balance summary logic to include settlement activities.
- Added comprehensive backend unit and API tests for new functionality.

Frontend (Foundation & TODOs due to my current capabilities):
- Created TypeScript interfaces for all new/updated models.
- Set up `listDetailStore.ts` with an action to handle `settleExpenseSplit` (API call is a placeholder) and refresh data.
- Created `SettleShareModal.vue` component for payment confirmation.
- Added unit tests for the new modal and store logic.
- Updated `ListDetailPage.vue` to display detailed expense/share statuses and settlement activities.
- `mitlist_doc.md` updated to reflect all backend changes and current frontend status.
- A `TODO.md` (implicitly within `mitlist_doc.md`'s new section) outlines necessary manual frontend integrations for `api.ts` and `ListDetailPage.vue` to complete the 'Settle Share' UI flow.

This set of changes provides the core backend infrastructure for precise expense share tracking and settlement, and lays the groundwork for full frontend integration.
2025-05-22 07:05:31 +00:00

135 lines
4.7 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { mount, VueWrapper } from '@vue/test-utils';
import { Decimal } from 'decimal.js';
import SettleShareModal from '../SettleShareModal.vue'; // Adjust path as needed
import type { ExpenseSplitInfo } from '../SettleShareModal.vue'; // Import the interface
// Default props generator
const getDefaultProps = (overrides: Record<string, any> = {}) => ({
show: true,
split: {
id: 1,
user_id: 100,
owed_amount: '50.00',
user: { id: 100, name: 'Test User', email: 'user@example.com' },
} as ExpenseSplitInfo,
paidAmount: 10.00,
isLoading: false,
...overrides,
});
describe('SettleShareModal.vue', () => {
let wrapper: VueWrapper<any>;
const mountComponent = (props: Record<string, any>) => {
wrapper = mount(SettleShareModal, {
props: getDefaultProps(props),
});
};
beforeEach(() => {
// Default mount before each test, can be overridden in specific tests
mountComponent({});
});
it('renders when show is true', () => {
expect(wrapper.find('.modal-backdrop-settle').exists()).toBe(true);
});
it('does not render when show is false', () => {
mountComponent({ show: false });
expect(wrapper.find('.modal-backdrop-settle').exists()).toBe(false);
});
it('displays correct split information', () => {
const props = getDefaultProps({
split: {
id: 2,
user_id: 101,
owed_amount: '75.50',
user: { id: 101, name: 'Jane Doe', email: 'jane@example.com' },
},
paidAmount: 25.00,
});
mountComponent(props);
const html = wrapper.html();
expect(html).toContain('Jane Doe');
expect(html).toContain('$75.50'); // Owed amount
expect(html).toContain('$25.00'); // Paid amount
const expectedRemaining = new Decimal(props.split.owed_amount).minus(new Decimal(props.paidAmount)).toFixed(2);
expect(html).toContain(`$${expectedRemaining}`); // Remaining amount
});
it('calculates and displays correct remaining amount', () => {
const owed = '100.00';
const paid = 30.00;
const remaining = new Decimal(owed).minus(paid).toFixed(2);
mountComponent({ split: { ...getDefaultProps().split, owed_amount: owed }, paidAmount: paid });
const remainingAmountStrong = wrapper.find('.amount-to-settle');
expect(remainingAmountStrong.exists()).toBe(true);
expect(remainingAmountStrong.text()).toBe(`$${remaining}`);
});
it('emits "confirm" with correct amount when Confirm Payment is clicked', async () => {
const owed = '50.00';
const paid = 10.00;
const expectedSettleAmount = new Decimal(owed).minus(paid).toNumber();
mountComponent({
split: { ...getDefaultProps().split, owed_amount: owed },
paidAmount: paid
});
await wrapper.find('.btn-primary-settle').trigger('click');
expect(wrapper.emitted().confirm).toBeTruthy();
expect(wrapper.emitted().confirm[0]).toEqual([expectedSettleAmount]);
});
it('emits "cancel" when Cancel button is clicked', async () => {
await wrapper.find('.btn-neutral-settle').trigger('click');
expect(wrapper.emitted().cancel).toBeTruthy();
});
it('emits "cancel" when backdrop is clicked', async () => {
await wrapper.find('.modal-backdrop-settle').trigger('click.self');
expect(wrapper.emitted().cancel).toBeTruthy();
});
it('disables Confirm Payment button when isLoading is true', () => {
mountComponent({ isLoading: true });
const confirmButton = wrapper.find('.btn-primary-settle');
expect((confirmButton.element as HTMLButtonElement).disabled).toBe(true);
});
it('disables Confirm Payment button when remaining amount is zero or less', () => {
mountComponent({
split: { ...getDefaultProps().split, owed_amount: '20.00' },
paidAmount: 20.00
}); // remaining is 0
const confirmButton = wrapper.find('.btn-primary-settle');
expect((confirmButton.element as HTMLButtonElement).disabled).toBe(true);
mountComponent({
split: { ...getDefaultProps().split, owed_amount: '19.00' },
paidAmount: 20.00
}); // remaining is < 0 (overpaid)
const confirmButtonNegative = wrapper.find('.btn-primary-settle');
expect((confirmButtonNegative.element as HTMLButtonElement).disabled).toBe(true);
});
it('Confirm Payment button is enabled when there is a positive remaining amount and not loading', () => {
mountComponent({
split: { ...getDefaultProps().split, owed_amount: '20.00' },
paidAmount: 10.00,
isLoading: false
});
const confirmButton = wrapper.find('.btn-primary-settle');
expect((confirmButton.element as HTMLButtonElement).disabled).toBe(false);
});
});