feat: finalize lease documents and IRL revisions
This commit is contained in:
1 parent
f127c69a28
commit
1bf6a3449c
196 files changed
+20564
-666
No files matched your search
@@ -1,9 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideNativeDateAdapter } from '@angular/material/core';
|
||||
import { vi } from 'vitest';
|
||||
import { emptyApartmentLegalProfile } from '../../services/apartment.service';
|
||||
import { emptyLeaseLegalTerms } from '../../services/lease.service';
|
||||
import { LeaseForm } from './lease-form';
|
||||
|
||||
describe('LeaseForm', () => {
|
||||
it('serializes selected dates as ISO values', () => {
|
||||
it('derives and serializes the standard lease end date', () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
fixture.componentRef.setInput('apartments', []);
|
||||
@@ -12,12 +15,17 @@ describe('LeaseForm', () => {
|
||||
component.leaseModel.set({
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
startDate: new Date(2026, 8, 1),
|
||||
endDate: new Date(2027, 7, 31),
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-09-01',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: true,
|
||||
legalTerms: emptyLeaseLegalTerms(),
|
||||
});
|
||||
fixture.detectChanges();
|
||||
let submitted: unknown;
|
||||
component.submitted.subscribe((lease) => (submitted = lease));
|
||||
|
||||
@@ -27,12 +35,20 @@ describe('LeaseForm', () => {
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
legalTerms: emptyLeaseLegalTerms(),
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-08-31',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: true,
|
||||
});
|
||||
expect(fixture.nativeElement.querySelector('#endDate')).toBeNull();
|
||||
expect(fixture.nativeElement.textContent).toContain('reconductible tacitement');
|
||||
});
|
||||
|
||||
it('allows overlapping draft dates but leaves active dates unavailable', () => {
|
||||
@@ -46,11 +62,16 @@ describe('LeaseForm', () => {
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: [],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-01-31',
|
||||
monthlyRent: 800,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 800,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'DRAFT',
|
||||
},
|
||||
{
|
||||
@@ -58,11 +79,16 @@ describe('LeaseForm', () => {
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: [],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-02-01',
|
||||
endDate: '2026-02-28',
|
||||
monthlyRent: 800,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 800,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
]);
|
||||
@@ -71,8 +97,8 @@ describe('LeaseForm', () => {
|
||||
apartmentId: 'apartment-1',
|
||||
}));
|
||||
|
||||
expect(fixture.componentInstance.dateFilter(new Date(2026, 0, 15))).toBe(true);
|
||||
expect(fixture.componentInstance.dateFilter(new Date(2026, 1, 15))).toBe(false);
|
||||
expect(fixture.componentInstance.dateFilter('2026-01-15')).toBe(true);
|
||||
expect(fixture.componentInstance.dateFilter('2026-02-15')).toBe(false);
|
||||
});
|
||||
|
||||
it('loads a draft for editing', () => {
|
||||
@@ -85,19 +111,73 @@ describe('LeaseForm', () => {
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
specialClauses: [
|
||||
{ catalogId: null, parameters: {}, customText: 'Animaux admis avec accord.' },
|
||||
],
|
||||
leaseType: 'STUDENT',
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-08-31',
|
||||
endDate: '2099-12-31',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'PROVISION',
|
||||
indexFlatRate: false,
|
||||
status: 'DRAFT',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.leaseModel().apartmentId).toBe('apartment-1');
|
||||
expect(fixture.componentInstance.leaseModel().leaseType).toBe('STUDENT');
|
||||
expect(fixture.componentInstance.leaseDuration().endDate).toBe('2027-05-31');
|
||||
expect(fixture.componentInstance.specialClauses()).toEqual([
|
||||
{ catalogId: null, parameters: {}, customText: 'Animaux admis avec accord.' },
|
||||
]);
|
||||
expect(fixture.nativeElement.textContent).toContain('Modifier le brouillon');
|
||||
});
|
||||
|
||||
it('falls back to a standard duration when editing a legacy draft', () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
fixture.componentRef.setInput('apartments', []);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentRef.setInput('lease', {
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-08-31',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'DRAFT',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.leaseModel().leaseType).toBe('STANDARD');
|
||||
expect(fixture.componentInstance.leaseDuration().endDate).toBe('2027-08-31');
|
||||
expect(fixture.componentInstance.specialClauses()).toEqual([]);
|
||||
});
|
||||
|
||||
it('clamps a student duration at month end and announces that it is not renewable', () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
fixture.componentRef.setInput('apartments', []);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentInstance.leaseModel.update((lease) => ({
|
||||
...lease,
|
||||
leaseType: 'STUDENT',
|
||||
startDate: '2026-05-31',
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.leaseDuration().endDate).toBe('2027-02-27');
|
||||
expect(fixture.nativeElement.textContent).toContain('non reconductible');
|
||||
});
|
||||
|
||||
it('keeps guarantors in the lease form module', () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
@@ -113,4 +193,163 @@ describe('LeaseForm', () => {
|
||||
|
||||
expect(fixture.componentInstance.guarantors()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('uses the lease type context unless several tenants make it a colocation', () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
fixture.componentRef.setInput('apartments', []);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentInstance.leaseModel.update((lease) => ({
|
||||
...lease,
|
||||
leaseType: 'STUDENT',
|
||||
tenantIds: ['tenant-1'],
|
||||
}));
|
||||
|
||||
expect(fixture.componentInstance.clauseContext()).toBe('STUDENT');
|
||||
|
||||
fixture.componentInstance.leaseModel.update((lease) => ({
|
||||
...lease,
|
||||
tenantIds: ['tenant-1', 'tenant-2'],
|
||||
}));
|
||||
expect(fixture.componentInstance.clauseContext()).toBe('COLOCATION');
|
||||
});
|
||||
|
||||
it('blocks submission when a custom clause is empty', () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
fixture.componentRef.setInput('apartments', []);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
const component = fixture.componentInstance;
|
||||
component.leaseModel.set({
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-09-01',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
legalTerms: emptyLeaseLegalTerms(),
|
||||
});
|
||||
component.specialClauses.set([{ catalogId: null, parameters: {}, customText: ' ' }]);
|
||||
const submitted = vi.fn();
|
||||
component.submitted.subscribe(submitted);
|
||||
fixture.detectChanges();
|
||||
|
||||
component.save();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(submitted).not.toHaveBeenCalled();
|
||||
expect(fixture.nativeElement.textContent).toContain('Renseignez le texte');
|
||||
});
|
||||
|
||||
it('shows the finalization warning only when an IRL clause concerns an F or G apartment', () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
const apartment = {
|
||||
id: 'apartment-1',
|
||||
name: 'T2',
|
||||
address: '1 rue de Paris',
|
||||
purchasePrice: 250000,
|
||||
description: '',
|
||||
legalProfile: { ...emptyApartmentLegalProfile(), energyRating: 'F' as const },
|
||||
};
|
||||
fixture.componentRef.setInput('apartments', [apartment]);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentInstance.leaseModel.update((lease) => ({
|
||||
...lease,
|
||||
apartmentId: apartment.id,
|
||||
legalTerms: {
|
||||
...lease.legalTerms,
|
||||
rentRevision: {
|
||||
enabled: true,
|
||||
annualRevisionDate: '2026-09-01',
|
||||
referenceQuarter: 2,
|
||||
referenceYear: 2026,
|
||||
seriesId: '001515333',
|
||||
},
|
||||
},
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain('à confirmer lors de la finalisation');
|
||||
|
||||
fixture.componentRef.setInput('apartments', [
|
||||
{ ...apartment, legalProfile: { ...apartment.legalProfile, energyRating: 'D' as const } },
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).not.toContain('à confirmer lors de la finalisation');
|
||||
});
|
||||
|
||||
it('clears an unfinished rent supplement when the reference-rent scheme is disabled', async () => {
|
||||
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
||||
const fixture = TestBed.createComponent(LeaseForm);
|
||||
fixture.componentRef.setInput('apartments', []);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentRef.setInput('lease', {
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
legalTerms: {
|
||||
...emptyLeaseLegalTerms(),
|
||||
rentControl: {
|
||||
evolutionCapApplicable: false,
|
||||
referenceRentApplicable: true,
|
||||
baseReferenceRent: 20,
|
||||
increasedReferenceRent: 24,
|
||||
supplement: null,
|
||||
supplementJustification: '',
|
||||
},
|
||||
},
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-08-31',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'DRAFT',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const supplementLabel = [...fixture.nativeElement.querySelectorAll('mat-label')].find(
|
||||
(label: Element) => label.textContent?.trim() === 'Complément de loyer',
|
||||
);
|
||||
const supplementInput = supplementLabel?.closest('mat-form-field')?.querySelector('input');
|
||||
if (!supplementInput) throw new Error('Champ complément de loyer introuvable.');
|
||||
supplementInput.value = '-100';
|
||||
supplementInput.dispatchEvent(new Event('input'));
|
||||
await fixture.whenStable();
|
||||
|
||||
const referenceRentCheckbox = [...fixture.nativeElement.querySelectorAll('mat-checkbox')]
|
||||
.find((checkbox: Element) => checkbox.textContent?.includes('loyer de référence'))
|
||||
?.querySelector('input');
|
||||
if (!referenceRentCheckbox) throw new Error('Option loyer de référence introuvable.');
|
||||
referenceRentCheckbox.click();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
fixture.nativeElement.querySelector('button[form=lease-details]').click();
|
||||
|
||||
expect(submitted).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
legalTerms: expect.objectContaining({
|
||||
rentControl: expect.objectContaining({
|
||||
referenceRentApplicable: false,
|
||||
supplement: null,
|
||||
supplementJustification: '',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user