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:
1 parent
e02ad54bcb
commit
b9e99348f7
76 files changed
+3141
-1531
No files matched your search
@@ -0,0 +1,33 @@
|
||||
.action-dialog-content,
|
||||
.dialog-fieldset {
|
||||
display: grid;
|
||||
gap: var(--space-4, 1rem);
|
||||
}
|
||||
|
||||
.action-dialog-content {
|
||||
width: min(30rem, calc(100vw - 4rem));
|
||||
max-width: 100%;
|
||||
max-height: 75vh;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.dialog-fieldset {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.dialog-error {
|
||||
margin: 0;
|
||||
color: var(--danger, #b42318);
|
||||
}
|
||||
|
||||
mat-dialog-actions button {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
display: inline-block;
|
||||
margin-inline-end: var(--space-2, 0.5rem);
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<h2 mat-dialog-title>Rembourser le surplus</h2>
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<mat-dialog-content class="action-dialog-content">
|
||||
<p>
|
||||
Solde non affecté disponible : <strong>{{ formatEuro(payment.unallocatedBalance) }}</strong>
|
||||
</p>
|
||||
<fieldset class="dialog-fieldset" [disabled]="saving()">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Date du remboursement</mat-label>
|
||||
<input matInput type="date" formControlName="refundedAt" [max]="today" required />
|
||||
@if (form.controls.refundedAt.hasError('futureDate')) {
|
||||
<mat-error>La date du remboursement ne peut pas être dans le futur.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Montant remboursé</mat-label>
|
||||
<input matInput type="number" step="0.01" min="0.01" formControlName="amount" required />
|
||||
<span matTextSuffix>€</span>
|
||||
@if (form.controls.amount.hasError('max')) {
|
||||
<mat-error>Le remboursement dépasse le solde non affecté.</mat-error>
|
||||
} @else if (form.controls.amount.invalid) {
|
||||
<mat-error>Le montant doit être strictement positif.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</fieldset>
|
||||
@if (error()) {
|
||||
<p class="dialog-error" role="alert">{{ error() }}</p>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close type="button" [disabled]="saving()">Annuler</button>
|
||||
<button mat-flat-button type="submit" [disabled]="form.invalid || saving()">
|
||||
@if (saving()) {
|
||||
<mat-spinner diameter="18" aria-label="Remboursement en cours" />
|
||||
}
|
||||
Confirmer le remboursement
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { finalize } from 'rxjs';
|
||||
import { httpErrorMessage } from '../../services/http-error-message';
|
||||
import { formatEuro, utcIsoDate } from '../../services/rent-payment-format';
|
||||
import { RentPayment, RentPaymentService } from '../../services/rent-payment.service';
|
||||
import { isRentPaymentDataStale, RENT_PAYMENT_DATA_STALE } from '../rent-payment-dialog-result';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rent-payment-refund-dialog',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
templateUrl: './rent-payment-refund-dialog.html',
|
||||
styleUrl: './rent-payment-refund-dialog.css',
|
||||
})
|
||||
export class RentPaymentRefundDialog {
|
||||
readonly payment = inject<RentPayment>(MAT_DIALOG_DATA);
|
||||
private readonly dialogRef = inject(MatDialogRef<RentPaymentRefundDialog>);
|
||||
private readonly payments = inject(RentPaymentService);
|
||||
private readonly operationId = crypto.randomUUID();
|
||||
readonly today = utcIsoDate();
|
||||
readonly saving = signal(false);
|
||||
readonly error = signal('');
|
||||
readonly formatEuro = formatEuro;
|
||||
|
||||
readonly form = new FormGroup({
|
||||
refundedAt: new FormControl(this.today, {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, this.notFuture.bind(this)],
|
||||
}),
|
||||
amount: new FormControl(this.payment.unallocatedBalance, {
|
||||
nonNullable: true,
|
||||
validators: [
|
||||
Validators.required,
|
||||
Validators.min(0.01),
|
||||
Validators.max(this.payment.unallocatedBalance),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
submit(): void {
|
||||
this.form.markAllAsTouched();
|
||||
if (this.form.invalid || this.saving()) return;
|
||||
this.saving.set(true);
|
||||
this.error.set('');
|
||||
this.payments
|
||||
.refund(this.payment.id, {
|
||||
operationId: this.operationId,
|
||||
paymentVersion: this.payment.version,
|
||||
...this.form.getRawValue(),
|
||||
})
|
||||
.pipe(finalize(() => this.saving.set(false)))
|
||||
.subscribe({
|
||||
next: (payment) => this.dialogRef.close(payment),
|
||||
error: (error) => {
|
||||
if (isRentPaymentDataStale(error)) {
|
||||
this.dialogRef.close(RENT_PAYMENT_DATA_STALE);
|
||||
return;
|
||||
}
|
||||
this.error.set(httpErrorMessage(error, 'Impossible d’enregistrer ce remboursement.'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private notFuture(control: AbstractControl<string>): ValidationErrors | null {
|
||||
return control.value > this.today ? { futureDate: true } : null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user