feat: add rent payment history and reversal dialogs

- Implemented RentPaymentHistoryServiceTest to validate payment retrieval with refunds and reversals.
- Created ReverseRentPaymentServiceTest to ensure correct handling of payment reversals and allocation releases.
- Developed rent-payment-allocation-dialog component with form for surplus allocation.
- Added rent-payment-refund-dialog component for processing refunds with validation.
- Introduced rent-payment-reversal-dialog component for reversing payments with reason input.
- Implemented utility functions for formatting Euro values and handling date formatting.
- Enhanced rent-payment-dialogs.spec.ts to cover new dialog functionalities and error handling.
This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent e02ad54bcb
commit b9e99348f7
76 files changed
+3141 -1531

No files matched your search

+130 -1
View File
@@ -275,6 +275,9 @@ test('creates a tenant with an attachment and a lease with a guarantor', async (
}
await route.fulfill({ contentType: 'application/json', json: leases });
});
await page.route('**/api/rent-dues', (route) =>
route.fulfill({ contentType: 'application/json', json: [] }),
);
await page.goto('/');
await page.getByTestId('primary-navigation').getByRole('button', { name: 'Locataires' }).click();
@@ -312,7 +315,7 @@ test('creates a tenant with an attachment and a lease with a guarantor', async (
await page.getByLabel('Nom du garant', { exact: true }).fill('Durand');
await page.getByLabel('E-mail du garant').fill('alex@example.com');
await page.getByRole('button', { name: 'Ajouter le garant' }).click();
await page.getByRole('button', { name: 'Enregistrer le bail' }).click();
await page.getByRole('button', { name: 'Enregistrer le brouillon' }).click();
await expect(page.getByRole('heading', { name: 'Studio République' })).toBeVisible();
await expect(page.getByText('Alex Durand')).toBeVisible();
@@ -322,3 +325,129 @@ test('creates a tenant with an attachment and a lease with a guarantor', async (
expect(download.suggestedFilename()).toBe('bail-lease-1.pdf');
await expectNoHorizontalOverflow(page);
});
test('records a received rent payment and keeps its surplus visible', async ({ page }) => {
const apartment = {
id: 'apartment-1',
name: 'Studio République',
address: '12 rue de la République, Lyon',
purchasePrice: 150000,
description: '',
};
const lease = {
id: 'lease-1',
apartmentId: apartment.id,
tenantIds: ['tenant-1'],
guarantors: [],
startDate: '2026-07-01',
endDate: '2027-06-30',
monthlyRent: 850,
monthlyCharges: 50,
securityDeposit: 850,
status: 'ACTIVE',
};
let rentDues = [
{
id: 'due-1',
leaseId: lease.id,
month: '2026-07',
dueDate: '2026-07-05',
rent: 850,
charges: 50,
total: 900,
allocatedAmount: 0,
remainingAmount: 900,
settlement: 'UNPAID',
timing: 'OVERDUE',
version: 1,
},
];
let payments: Record<string, unknown>[] = [];
let submittedPayment: Record<string, unknown> | undefined;
await mockUser(page, true);
await page.route('**/api/apartments', (route) =>
route.fulfill({ contentType: 'application/json', json: [apartment] }),
);
await page.route('**/api/leases', (route) =>
route.fulfill({ contentType: 'application/json', json: [lease] }),
);
await page.route('**/api/rent-dues', (route) =>
route.fulfill({ contentType: 'application/json', json: rentDues }),
);
await page.route('**/api/rent-payments', async (route) => {
if (route.request().method() === 'POST') {
submittedPayment = route.request().postDataJSON() as Record<string, unknown>;
const receivedAt = submittedPayment['receivedAt'] as string;
payments = [
{
id: 'payment-1',
leaseId: lease.id,
receivedAt,
amount: 950,
status: 'CONFIRMED',
allocations: [
{
id: 'allocation-1',
rentDueId: 'due-1',
amount: 900,
allocatedAt: `${receivedAt}T12:00:00Z`,
},
],
allocatedAmount: 900,
refundedAmount: 0,
unallocatedBalance: 50,
confirmedAt: `${receivedAt}T12:00:00Z`,
version: 1,
refunds: [],
reversal: null,
},
];
rentDues = [
{
...rentDues[0],
allocatedAmount: 900,
remainingAmount: 0,
settlement: 'PAID',
version: 2,
},
];
await route.fulfill({ contentType: 'application/json', json: payments[0] });
return;
}
await route.fulfill({ contentType: 'application/json', json: payments });
});
await page.goto('/');
await page.getByTestId('primary-navigation').getByRole('button', { name: 'Loyers' }).click();
await expect(page.getByRole('heading', { name: 'Studio République' })).toBeVisible();
await page.getByRole('button', { name: 'Enregistrer un règlement' }).click();
const dialog = page.getByRole('dialog');
await expect(dialog.getByRole('heading', { name: 'Enregistrer un règlement' })).toBeVisible();
await dialog.getByLabel('Montant reçu').fill('950');
await expect(dialog.getByText(/Surplus non affecté/)).toContainText('50,00');
await dialog.getByRole('button', { name: 'Confirmer le règlement' }).click();
await expect(page.getByRole('status')).toHaveText('Le règlement a été enregistré.');
expect(submittedPayment).toMatchObject({
rentDueId: 'due-1',
rentDueVersion: 1,
amount: 950,
});
expect(submittedPayment?.['operationId']).toEqual(expect.any(String));
const dueCard = page
.getByRole('article')
.filter({ has: page.getByRole('heading', { name: 'Studio République' }) });
await expect(dueCard.getByText('Réglée', { exact: true })).toBeVisible();
await expect(dueCard.getByRole('button', { name: 'Télécharger la quittance' })).toBeVisible();
const paymentCard = page
.getByRole('article')
.filter({ has: page.getByText('Confirmé', { exact: true }) });
await expect(paymentCard.getByText(/^950,00\s€$/)).toBeVisible();
await expect(paymentCard.getByText(/^50,00\s€$/)).toBeVisible();
await expect(paymentCard.getByRole('button', { name: 'Rembourser' })).toBeVisible();
await expectNoHorizontalOverflow(page);
});