356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
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('derives and serializes the standard lease end date', () => {
|
|
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: true,
|
|
legalTerms: emptyLeaseLegalTerms(),
|
|
});
|
|
fixture.detectChanges();
|
|
let submitted: unknown;
|
|
component.submitted.subscribe((lease) => (submitted = lease));
|
|
|
|
component.save();
|
|
|
|
expect(submitted).toEqual({
|
|
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', () => {
|
|
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
|
const fixture = TestBed.createComponent(LeaseForm);
|
|
fixture.componentRef.setInput('apartments', []);
|
|
fixture.componentRef.setInput('tenants', []);
|
|
fixture.componentRef.setInput('leases', [
|
|
{
|
|
id: 'draft-1',
|
|
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',
|
|
},
|
|
{
|
|
id: 'active-1',
|
|
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',
|
|
},
|
|
]);
|
|
fixture.componentInstance.leaseModel.update((lease) => ({
|
|
...lease,
|
|
apartmentId: 'apartment-1',
|
|
}));
|
|
|
|
expect(fixture.componentInstance.dateFilter('2026-01-15')).toBe(true);
|
|
expect(fixture.componentInstance.dateFilter('2026-02-15')).toBe(false);
|
|
});
|
|
|
|
it('loads a draft for editing', () => {
|
|
TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] });
|
|
const fixture = TestBed.createComponent(LeaseForm);
|
|
fixture.componentRef.setInput('apartments', []);
|
|
fixture.componentRef.setInput('tenants', []);
|
|
fixture.componentRef.setInput('lease', {
|
|
id: 'draft-1',
|
|
apartmentId: 'apartment-1',
|
|
tenantIds: ['tenant-1'],
|
|
guarantors: [],
|
|
specialClauses: [
|
|
{ catalogId: null, parameters: {}, customText: 'Animaux admis avec accord.' },
|
|
],
|
|
leaseType: 'STUDENT',
|
|
startDate: '2026-09-01',
|
|
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);
|
|
fixture.componentRef.setInput('apartments', []);
|
|
fixture.componentRef.setInput('tenants', []);
|
|
fixture.componentInstance.addGuarantor({
|
|
firstName: 'Alex',
|
|
lastName: 'Durand',
|
|
email: 'alex@example.com',
|
|
phone: '',
|
|
address: '',
|
|
});
|
|
|
|
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: '',
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
});
|