Preserve actionable form errors
This commit is contained in:
1 parent
fe36d6cae4
commit
3a8ef0ae0b
17 files changed
+166
-63
No files matched your search
+1
-8
@@ -316,14 +316,7 @@ private Lease activeLease() {
|
||||
|
||||
private Tenant tenant(TenantId id, String firstName, String address) {
|
||||
return new Tenant(
|
||||
id,
|
||||
LANDLORD_ID,
|
||||
firstName,
|
||||
"Martin",
|
||||
firstName.toLowerCase() + "@example.com",
|
||||
"",
|
||||
address,
|
||||
List.of());
|
||||
id, LANDLORD_ID, firstName, "Martin", id.value() + "@example.com", "", address, List.of());
|
||||
}
|
||||
|
||||
private RentDue due(YearMonth month) {
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ describe('LandlordProfileCompletionDialog', () => {
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
'Saisissez un numéro de téléphone valide.',
|
||||
);
|
||||
component.clearSaveFieldError('phone');
|
||||
component.clearSaveFieldError('phoneValid');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).not.toContain(
|
||||
|
||||
@@ -31,6 +31,13 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
display: block;
|
||||
margin-top: -12px;
|
||||
color: var(--danger);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
autocomplete="given-name"
|
||||
/>
|
||||
@if (firstNameError()) {
|
||||
<mat-error>{{ firstNameError() }}</mat-error>
|
||||
<span class="field-error" role="alert">{{ firstNameError() }}</span>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
@@ -34,7 +34,7 @@
|
||||
autocomplete="family-name"
|
||||
/>
|
||||
@if (lastNameError()) {
|
||||
<mat-error>{{ lastNameError() }}</mat-error>
|
||||
<span class="field-error" role="alert">{{ lastNameError() }}</span>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
@@ -48,7 +48,7 @@
|
||||
autocomplete="email"
|
||||
/>
|
||||
@if (emailError()) {
|
||||
<mat-error>{{ emailError() }}</mat-error>
|
||||
<span class="field-error" role="alert">{{ emailError() }}</span>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
@@ -62,7 +62,7 @@
|
||||
autocomplete="tel"
|
||||
/>
|
||||
@if (phoneError()) {
|
||||
<mat-error>{{ phoneError() }}</mat-error>
|
||||
<span class="field-error" role="alert">{{ phoneError() }}</span>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="full-field"
|
||||
@@ -75,7 +75,7 @@
|
||||
autocomplete="street-address"
|
||||
/>
|
||||
@if (addressError()) {
|
||||
<mat-error>{{ addressError() }}</mat-error>
|
||||
<span class="field-error" role="alert">{{ addressError() }}</span>
|
||||
}
|
||||
</mat-form-field>
|
||||
</fieldset>
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('LandlordProfileForm', () => {
|
||||
|
||||
expect(saveRequested).not.toHaveBeenCalled();
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
'Veuillez renseigner tous les champs obligatoires.',
|
||||
'Le numéro de téléphone est obligatoire.',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -94,7 +94,6 @@ describe('LandlordProfileForm', () => {
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
'Saisissez un numéro de téléphone valide.',
|
||||
);
|
||||
@@ -108,6 +107,7 @@ describe('LandlordProfileForm', () => {
|
||||
phone.dispatchEvent(new Event('input'));
|
||||
|
||||
expect(changed).toHaveBeenCalledWith('phone');
|
||||
expect(changed).toHaveBeenCalledWith('phoneValid');
|
||||
fixture.componentRef.setInput('saveFieldErrors', [{ field: 'email', code: 'email' }]);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
@@ -17,6 +17,8 @@ const EMPTY_PROFILE: LandlordProfile = {
|
||||
address: '',
|
||||
};
|
||||
|
||||
type LandlordProfileField = keyof LandlordProfile | 'phoneValid';
|
||||
|
||||
@Component({
|
||||
selector: 'app-landlord-profile-form',
|
||||
imports: [
|
||||
@@ -38,7 +40,7 @@ export class LandlordProfileForm {
|
||||
readonly saveError = input('');
|
||||
readonly saveFieldErrors = input<FieldValidationError[]>([]);
|
||||
readonly saveRequested = output<LandlordProfile>();
|
||||
readonly fieldChanged = output<keyof LandlordProfile>();
|
||||
readonly fieldChanged = output<LandlordProfileField>();
|
||||
private previousServerErrorSignature = '';
|
||||
|
||||
readonly profileModel = signal<LandlordProfile>({ ...this.profile() });
|
||||
@@ -93,6 +95,7 @@ export class LandlordProfileForm {
|
||||
fieldWasChanged(field: keyof LandlordProfile): void {
|
||||
this.validationError.set('');
|
||||
this.fieldChanged.emit(field);
|
||||
if (field === 'phone') this.fieldChanged.emit('phoneValid');
|
||||
}
|
||||
|
||||
showSaveError(): boolean {
|
||||
@@ -189,6 +192,7 @@ export class LandlordProfileForm {
|
||||
: this.addressError()
|
||||
? '#profile-address'
|
||||
: '';
|
||||
if (!selector) return;
|
||||
const control = this.element.nativeElement.querySelector(selector);
|
||||
if (control instanceof HTMLElement) control.focus();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ describe('LeaseAmendmentForm', () => {
|
||||
indexFlatRate: false,
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.amendmentModel.update((model) => ({
|
||||
...model,
|
||||
effectiveDate: '2026-10-01',
|
||||
@@ -43,9 +46,6 @@ describe('LeaseAmendmentForm', () => {
|
||||
monthlyCharges: 0,
|
||||
dueDay: 28,
|
||||
}));
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.save();
|
||||
|
||||
@@ -84,14 +84,14 @@ describe('LeaseAmendmentForm', () => {
|
||||
indexFlatRate: false,
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.amendmentModel.update((model) => ({
|
||||
...model,
|
||||
effectiveDate: '2026-10-01',
|
||||
endDate: '2026-09-30',
|
||||
}));
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.save();
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('LeaseEndPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a skeleton, then a retryable load error', () => {
|
||||
it('shows a skeleton, then a retryable safe load error', () => {
|
||||
const response = new Subject<LeaseEndState>();
|
||||
getState.mockReturnValueOnce(response);
|
||||
const fixture = TestBed.createComponent(LeaseEndPage);
|
||||
@@ -99,7 +99,10 @@ describe('LeaseEndPage', () => {
|
||||
|
||||
response.error({ error: { message: 'Bail introuvable' } });
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain('Bail introuvable');
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
'Impossible de charger la fin de ce bail.',
|
||||
);
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Bail introuvable');
|
||||
|
||||
getState.mockReturnValueOnce(of(state));
|
||||
fixture.nativeElement.querySelector('.load-error button').click();
|
||||
@@ -204,7 +207,7 @@ describe('LeaseEndPage', () => {
|
||||
expect(fixture.nativeElement.textContent).toContain('Enregistrer l’accord');
|
||||
});
|
||||
|
||||
it('keeps notice loading and failures local without publishing false success', () => {
|
||||
it('keeps notice loading and safe failures local without publishing false success', () => {
|
||||
getState.mockReturnValueOnce(of({ ...state, phase: 'ACTIVE', notices: [] }));
|
||||
const response = new Subject<LeaseEndState>();
|
||||
recordNotice.mockReturnValue(response);
|
||||
@@ -228,7 +231,8 @@ describe('LeaseEndPage', () => {
|
||||
|
||||
response.error({ error: { message: 'Congé refusé' } });
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toContain('Congé refusé');
|
||||
expect(fixture.nativeElement.textContent).toContain('Impossible d’enregistrer ce congé.');
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Congé refusé');
|
||||
expect(fixture.componentInstance.state()?.phase).toBe('ACTIVE');
|
||||
});
|
||||
|
||||
@@ -278,7 +282,7 @@ describe('LeaseEndPage', () => {
|
||||
expect(acceptWithdrawal).toHaveBeenCalledWith('lease-1', 'notice-1', request);
|
||||
});
|
||||
|
||||
it('keeps a closure failure actionable', () => {
|
||||
it('keeps a safe closure failure actionable', () => {
|
||||
close.mockReturnValue(throwError(() => ({ error: { message: 'Clôture refusée' } })));
|
||||
getState.mockReturnValueOnce(of({ ...state, phase: 'AWAITING_CLOSURE' }));
|
||||
const fixture = TestBed.createComponent(LeaseEndPage);
|
||||
@@ -290,7 +294,8 @@ describe('LeaseEndPage', () => {
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain('Clôture refusée');
|
||||
expect(fixture.nativeElement.textContent).toContain('Impossible de clôturer ce bail.');
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Clôture refusée');
|
||||
expect(fixture.componentInstance.state()?.phase).toBe('AWAITING_CLOSURE');
|
||||
});
|
||||
});
|
||||
@@ -72,6 +72,11 @@
|
||||
.form-actions {
|
||||
margin-top: 28px;
|
||||
}
|
||||
.core-field .field-error {
|
||||
margin: -14px 0 0;
|
||||
color: var(--danger);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@media (min-width: 700px) {
|
||||
.three-columns {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline"
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Appartement</mat-label
|
||||
><mat-select
|
||||
id="apartmentId"
|
||||
@@ -50,11 +51,13 @@
|
||||
<mat-option [value]="apartment.id">{{ apartment.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (fieldError('apartmentId')) {
|
||||
<mat-error>{{ fieldError('apartmentId') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('apartmentId') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
</div>
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Locataires</mat-label
|
||||
><mat-select
|
||||
id="tenantIds"
|
||||
@@ -68,10 +71,11 @@
|
||||
>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (fieldError('tenantIds')) {
|
||||
<mat-error>{{ fieldError('tenantIds') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('tenantIds') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="form-section">
|
||||
@@ -83,7 +87,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid three-columns">
|
||||
<mat-form-field appearance="outline"
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Type de bail</mat-label
|
||||
><mat-select
|
||||
id="leaseType"
|
||||
@@ -93,11 +98,13 @@
|
||||
<mat-option value="STANDARD">Location meublée classique</mat-option>
|
||||
<mat-option value="STUDENT">Location meublée étudiante</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (fieldError('leaseType')) {
|
||||
<mat-error>{{ fieldError('leaseType') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('leaseType') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
</div>
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Date d'entrée</mat-label
|
||||
><input
|
||||
matInput
|
||||
@@ -106,10 +113,11 @@
|
||||
[formField]="leaseForm.startDate"
|
||||
(change)="fieldWasChanged('startDate')"
|
||||
/>
|
||||
</mat-form-field>
|
||||
@if (fieldError('startDate')) {
|
||||
<mat-error>{{ fieldError('startDate') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('startDate') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="lease-duration" aria-live="polite">
|
||||
<span id="lease-end-date-label">Date de fin calculée</span>
|
||||
<output aria-labelledby="lease-end-date-label">
|
||||
@@ -120,7 +128,8 @@
|
||||
@if (overlapWarning()) {
|
||||
<p class="lease-warning" role="note">{{ overlapWarning() }}</p>
|
||||
}
|
||||
<mat-form-field appearance="outline"
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Loyer mensuel</mat-label
|
||||
><input
|
||||
matInput
|
||||
@@ -130,11 +139,13 @@
|
||||
step="0.01"
|
||||
(change)="fieldWasChanged('monthlyRent')"
|
||||
/><span matTextSuffix>€</span>
|
||||
</mat-form-field>
|
||||
@if (fieldError('monthlyRent')) {
|
||||
<mat-error>{{ fieldError('monthlyRent') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('monthlyRent') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
</div>
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Charges mensuelles</mat-label
|
||||
><input
|
||||
matInput
|
||||
@@ -144,11 +155,13 @@
|
||||
step="0.01"
|
||||
(change)="fieldWasChanged('monthlyCharges')"
|
||||
/><span matTextSuffix>€</span>
|
||||
</mat-form-field>
|
||||
@if (fieldError('monthlyCharges')) {
|
||||
<mat-error>{{ fieldError('monthlyCharges') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('monthlyCharges') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
</div>
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Dépôt de garantie</mat-label
|
||||
><input
|
||||
matInput
|
||||
@@ -158,11 +171,13 @@
|
||||
step="0.01"
|
||||
(change)="fieldWasChanged('securityDeposit')"
|
||||
/><span matTextSuffix>€</span>
|
||||
</mat-form-field>
|
||||
@if (fieldError('securityDeposit')) {
|
||||
<mat-error>{{ fieldError('securityDeposit') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('securityDeposit') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
</div>
|
||||
<div class="core-field">
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Mode de récupération des charges</mat-label
|
||||
><mat-select
|
||||
id="chargeMethod"
|
||||
@@ -185,10 +200,11 @@
|
||||
step="1"
|
||||
(change)="fieldWasChanged('dueDay')"
|
||||
/><mat-hint>Entre le 1er et le 28 du mois</mat-hint>
|
||||
</mat-form-field>
|
||||
@if (fieldError('dueDay')) {
|
||||
<mat-error>{{ fieldError('dueDay') }}</mat-error>
|
||||
<p class="field-error" role="alert">{{ fieldError('dueDay') }}</p>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@if (leaseModel().chargeMethod === 'FLAT_RATE') {
|
||||
<mat-checkbox id="indexFlatRate" [formField]="leaseForm.indexFlatRate">
|
||||
Indexer le forfait de charges avec le loyer
|
||||
|
||||
@@ -174,6 +174,7 @@ describe('LeaseForm', () => {
|
||||
]);
|
||||
fixture.detectChanges();
|
||||
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.leaseModel().monthlyCharges).toBe(50);
|
||||
expect(fixture.nativeElement.textContent).toContain('charges mensuelles doivent être');
|
||||
@@ -182,6 +183,7 @@ describe('LeaseForm', () => {
|
||||
fixture.componentRef.setInput('saveFieldErrors', [{ field: 'dueDay', code: 'max' }]);
|
||||
fixture.detectChanges();
|
||||
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
||||
fixture.detectChanges();
|
||||
expect(document.activeElement?.id).toBe('dueDay');
|
||||
});
|
||||
|
||||
|
||||
+13
-2
@@ -19,7 +19,12 @@ import { RentDue } from '../../services/lease.service';
|
||||
import { saveFailure } from '../../services/http-error-message';
|
||||
import { formatEuro } from '../../services/rent-payment-format';
|
||||
import { RentPayment, RentPaymentService } from '../../services/rent-payment.service';
|
||||
import { isRentPaymentDataStale, RENT_PAYMENT_DATA_STALE } from '../rent-payment-dialog-result';
|
||||
import {
|
||||
isRentPaymentConflict,
|
||||
isRentPaymentDataStale,
|
||||
RENT_PAYMENT_CONFLICT_MESSAGE,
|
||||
RENT_PAYMENT_DATA_STALE,
|
||||
} from '../rent-payment-dialog-result';
|
||||
|
||||
export interface EligibleRentDue {
|
||||
rentDue: RentDue;
|
||||
@@ -136,7 +141,13 @@ export class RentPaymentAllocationDialog {
|
||||
const knownErrors = failure.fieldErrors.filter((fieldError) =>
|
||||
this.isKnownFieldError(fieldError.field, fieldError.code),
|
||||
);
|
||||
this.error.set(knownErrors.length === failure.fieldErrors.length ? '' : failure.message);
|
||||
this.error.set(
|
||||
knownErrors.length > 0 && knownErrors.length === failure.fieldErrors.length
|
||||
? ''
|
||||
: isRentPaymentConflict(error)
|
||||
? RENT_PAYMENT_CONFLICT_MESSAGE
|
||||
: failure.message,
|
||||
);
|
||||
this.fieldErrorFields.set(new Set(knownErrors.map((fieldError) => fieldError.field)));
|
||||
for (const fieldError of knownErrors) {
|
||||
const control = this.form.get(fieldError.field);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { RentPayment } from '../services/rent-payment.service';
|
||||
|
||||
export const RENT_PAYMENT_DATA_STALE = 'RENT_PAYMENT_DATA_STALE';
|
||||
export const RENT_PAYMENT_CONFLICT_MESSAGE =
|
||||
'Cette action n’est plus possible avec les données actuelles. Actualisez-les puis réessayez.';
|
||||
const CONCURRENT_CHANGE = 'concurrent_change';
|
||||
|
||||
export type RentPaymentDialogResult = RentPayment | typeof RENT_PAYMENT_DATA_STALE;
|
||||
@@ -18,3 +20,7 @@ export function isRentPaymentDataStale(error: unknown): boolean {
|
||||
error.error.code === CONCURRENT_CHANGE
|
||||
);
|
||||
}
|
||||
|
||||
export function isRentPaymentConflict(error: unknown): boolean {
|
||||
return typeof error === 'object' && error !== null && 'status' in error && error.status === 409;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Type } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { RENT_PAYMENT_DATA_STALE } from './rent-payment-dialog-result';
|
||||
import { RENT_PAYMENT_CONFLICT_MESSAGE, RENT_PAYMENT_DATA_STALE } from './rent-payment-dialog-result';
|
||||
import { RentPaymentAllocationDialog } from './rent-payment-allocation-dialog/rent-payment-allocation-dialog';
|
||||
import { RentPaymentRefundDialog } from './rent-payment-refund-dialog/rent-payment-refund-dialog';
|
||||
import { RentPaymentReversalDialog } from './rent-payment-reversal-dialog/rent-payment-reversal-dialog';
|
||||
@@ -80,10 +80,31 @@ describe('rent payment dialogs', () => {
|
||||
component.submit();
|
||||
|
||||
expect(component.form.controls.amount.value).toBe(0.001);
|
||||
expect(component.error()).toBe('Échéance déjà réglée');
|
||||
expect(component.error()).toBe(RENT_PAYMENT_CONFLICT_MESSAGE);
|
||||
expect(dialogRef.close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps non-concurrent conflicts visible for every payment action', () => {
|
||||
payments.refund.mockReturnValue(throwError(() => ({ status: 409, error: { message: 'detail' } })));
|
||||
const refund = create(RentPaymentRefundDialog, payment({ unallocatedBalance: 100 }));
|
||||
refund.submit();
|
||||
expect(refund.error()).toBe(RENT_PAYMENT_CONFLICT_MESSAGE);
|
||||
|
||||
payments.reverse.mockReturnValue(throwError(() => ({ status: 409, error: { message: 'detail' } })));
|
||||
const reversal = create(RentPaymentReversalDialog, payment());
|
||||
reversal.form.controls.reason.setValue('Erreur de saisie');
|
||||
reversal.submit();
|
||||
expect(reversal.error()).toBe(RENT_PAYMENT_CONFLICT_MESSAGE);
|
||||
|
||||
payments.allocate.mockReturnValue(throwError(() => ({ status: 409, error: { message: 'detail' } })));
|
||||
const allocation = create(RentPaymentAllocationDialog, {
|
||||
payment: payment({ unallocatedBalance: 100 }),
|
||||
eligibleRentDues: [{ rentDue: due(), label: 'Juillet' }],
|
||||
});
|
||||
allocation.submit();
|
||||
expect(allocation.error()).toBe(RENT_PAYMENT_CONFLICT_MESSAGE);
|
||||
});
|
||||
|
||||
it('maps a known server field error and clears it when that field changes', () => {
|
||||
payments.refund.mockReturnValue(
|
||||
throwError(() => ({
|
||||
|
||||
+13
-2
@@ -16,7 +16,12 @@ import { finalize } from 'rxjs';
|
||||
import { saveFailure } from '../../services/http-error-message';
|
||||
import { browserTimeZone, formatEuro, localIsoDate } from '../../services/rent-payment-format';
|
||||
import { RentPayment, RentPaymentService } from '../../services/rent-payment.service';
|
||||
import { isRentPaymentDataStale, RENT_PAYMENT_DATA_STALE } from '../rent-payment-dialog-result';
|
||||
import {
|
||||
isRentPaymentConflict,
|
||||
isRentPaymentDataStale,
|
||||
RENT_PAYMENT_CONFLICT_MESSAGE,
|
||||
RENT_PAYMENT_DATA_STALE,
|
||||
} from '../rent-payment-dialog-result';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rent-payment-refund-dialog',
|
||||
@@ -92,7 +97,13 @@ export class RentPaymentRefundDialog {
|
||||
const knownErrors = failure.fieldErrors.filter((fieldError) =>
|
||||
this.isKnownFieldError(fieldError.field, fieldError.code),
|
||||
);
|
||||
this.error.set(knownErrors.length === failure.fieldErrors.length ? '' : failure.message);
|
||||
this.error.set(
|
||||
knownErrors.length > 0 && knownErrors.length === failure.fieldErrors.length
|
||||
? ''
|
||||
: isRentPaymentConflict(error)
|
||||
? RENT_PAYMENT_CONFLICT_MESSAGE
|
||||
: failure.message,
|
||||
);
|
||||
this.fieldErrorFields.set(new Set(knownErrors.map((fieldError) => fieldError.field)));
|
||||
for (const fieldError of knownErrors) {
|
||||
const control = this.form.get(fieldError.field);
|
||||
|
||||
+13
-2
@@ -15,7 +15,12 @@ import { finalize } from 'rxjs';
|
||||
import { saveFailure } from '../../services/http-error-message';
|
||||
import { browserTimeZone, formatEuro } from '../../services/rent-payment-format';
|
||||
import { RentPayment, RentPaymentService } from '../../services/rent-payment.service';
|
||||
import { isRentPaymentDataStale, RENT_PAYMENT_DATA_STALE } from '../rent-payment-dialog-result';
|
||||
import {
|
||||
isRentPaymentConflict,
|
||||
isRentPaymentDataStale,
|
||||
RENT_PAYMENT_CONFLICT_MESSAGE,
|
||||
RENT_PAYMENT_DATA_STALE,
|
||||
} from '../rent-payment-dialog-result';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rent-payment-reversal-dialog',
|
||||
@@ -87,7 +92,13 @@ export class RentPaymentReversalDialog {
|
||||
const knownErrors = failure.fieldErrors.filter((fieldError) =>
|
||||
this.isKnownFieldError(fieldError.field, fieldError.code),
|
||||
);
|
||||
this.error.set(knownErrors.length === failure.fieldErrors.length ? '' : failure.message);
|
||||
this.error.set(
|
||||
knownErrors.length > 0 && knownErrors.length === failure.fieldErrors.length
|
||||
? ''
|
||||
: isRentPaymentConflict(error)
|
||||
? RENT_PAYMENT_CONFLICT_MESSAGE
|
||||
: failure.message,
|
||||
);
|
||||
this.fieldErrorFields.set(new Set(knownErrors.map((fieldError) => fieldError.field)));
|
||||
for (const fieldError of knownErrors) {
|
||||
const control = this.form.get(fieldError.field);
|
||||
|
||||
+13
-2
@@ -18,7 +18,12 @@ import { RentDue } from '../../services/lease.service';
|
||||
import { saveFailure } from '../../services/http-error-message';
|
||||
import { browserTimeZone, formatEuro, localIsoDate } from '../../services/rent-payment-format';
|
||||
import { RentPaymentService } from '../../services/rent-payment.service';
|
||||
import { isRentPaymentDataStale, RENT_PAYMENT_DATA_STALE } from '../rent-payment-dialog-result';
|
||||
import {
|
||||
isRentPaymentConflict,
|
||||
isRentPaymentDataStale,
|
||||
RENT_PAYMENT_CONFLICT_MESSAGE,
|
||||
RENT_PAYMENT_DATA_STALE,
|
||||
} from '../rent-payment-dialog-result';
|
||||
|
||||
export interface RentPaymentValidationDialogData {
|
||||
rentDue: RentDue;
|
||||
@@ -109,7 +114,13 @@ export class RentPaymentValidationDialog {
|
||||
const knownErrors = failure.fieldErrors.filter((fieldError) =>
|
||||
this.isKnownFieldError(fieldError.field, fieldError.code),
|
||||
);
|
||||
this.error.set(knownErrors.length === failure.fieldErrors.length ? '' : failure.message);
|
||||
this.error.set(
|
||||
knownErrors.length > 0 && knownErrors.length === failure.fieldErrors.length
|
||||
? ''
|
||||
: isRentPaymentConflict(error)
|
||||
? RENT_PAYMENT_CONFLICT_MESSAGE
|
||||
: failure.message,
|
||||
);
|
||||
this.fieldErrorFields.set(new Set(knownErrors.map((fieldError) => fieldError.field)));
|
||||
for (const fieldError of knownErrors) {
|
||||
const control = this.form.get(fieldError.field);
|
||||
|
||||
Reference in new issue
Block a user