- 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.
375 lines
13 KiB
TypeScript
375 lines
13 KiB
TypeScript
import { Injectable, inject, signal } from '@angular/core';
|
||
import { MatDialog } from '@angular/material/dialog';
|
||
import {
|
||
ConfirmationDialog,
|
||
ConfirmationDialogData,
|
||
} from '../components/confirmation-dialog/confirmation-dialog';
|
||
import { Apartment, ApartmentService } from './apartment.service';
|
||
import { FileDownloadService } from './file-download.service';
|
||
import { finalize } from 'rxjs';
|
||
import { Lease, LeaseInput, LeaseService, RentDue } from './lease.service';
|
||
import { RentPayment, RentPaymentService } from './rent-payment.service';
|
||
import { Tenant, TenantAttachment, TenantService } from './tenant.service';
|
||
import { TenantAttachmentTransferService } from './tenant-attachment-transfer.service';
|
||
import { WorkspaceResource } from './workspace-resource';
|
||
import { httpErrorMessage } from './http-error-message';
|
||
import { withoutRecordKey, withoutSetValue } from './immutable-state';
|
||
|
||
const newResourceKey = 'new';
|
||
|
||
@Injectable({ providedIn: 'root' })
|
||
export class WorkspaceService {
|
||
private readonly dialog = inject(MatDialog);
|
||
private readonly apartmentService = inject(ApartmentService);
|
||
private readonly tenantService = inject(TenantService);
|
||
private readonly leaseService = inject(LeaseService);
|
||
private readonly rentPaymentService = inject(RentPaymentService);
|
||
private readonly downloads = inject(FileDownloadService);
|
||
private readonly attachmentTransfers = inject(TenantAttachmentTransferService);
|
||
|
||
private readonly apartmentResource = new WorkspaceResource(
|
||
() => this.apartmentService.getApartments(),
|
||
{
|
||
load: 'Impossible de charger les appartements.',
|
||
save: 'Impossible d’enregistrer cet appartement.',
|
||
delete: 'Impossible de supprimer cet appartement.',
|
||
},
|
||
);
|
||
private readonly tenantResource = new WorkspaceResource(() => this.tenantService.getTenants(), {
|
||
load: 'Impossible de charger les locataires.',
|
||
save: 'Impossible d’enregistrer ce locataire.',
|
||
delete: 'Impossible de supprimer ce locataire.',
|
||
});
|
||
private readonly leaseResource = new WorkspaceResource(() => this.leaseService.getLeases(), {
|
||
load: 'Impossible de charger les baux.',
|
||
save: 'Impossible d’enregistrer ce bail.',
|
||
delete: 'Impossible de supprimer ce bail.',
|
||
});
|
||
private readonly rentDueResource = new WorkspaceResource(() => this.leaseService.getRentDues(), {
|
||
load: 'Impossible de charger les échéances de loyer.',
|
||
save: '',
|
||
delete: '',
|
||
});
|
||
private readonly rentPaymentResource = new WorkspaceResource(
|
||
() => this.rentPaymentService.getRentPayments(),
|
||
{
|
||
load: 'Impossible de charger les échéances de loyer.',
|
||
save: 'Impossible d’enregistrer ce paiement.',
|
||
delete: '',
|
||
},
|
||
);
|
||
|
||
readonly apartments = this.apartmentResource.items;
|
||
readonly apartmentLoadState = this.apartmentResource.loadState;
|
||
readonly deletingApartmentIds = this.apartmentResource.deletingIds;
|
||
readonly apartmentDeleteErrors = this.apartmentResource.deleteErrors;
|
||
readonly tenants = this.tenantResource.items;
|
||
readonly tenantLoadState = this.tenantResource.loadState;
|
||
readonly deletingTenantIds = this.tenantResource.deletingIds;
|
||
readonly tenantDeleteErrors = this.tenantResource.deleteErrors;
|
||
readonly leases = this.leaseResource.items;
|
||
readonly leaseLoadState = this.leaseResource.loadState;
|
||
readonly deletingLeaseIds = this.leaseResource.deletingIds;
|
||
readonly leaseDeleteErrors = this.leaseResource.deleteErrors;
|
||
readonly rentDues = this.rentDueResource.items;
|
||
readonly rentDueLoadState = this.rentDueResource.loadState;
|
||
private readonly activatingLeaseIds = signal<ReadonlySet<string>>(new Set());
|
||
private readonly leaseActivationErrors = signal<Record<string, string>>({});
|
||
readonly rentPayments = this.rentPaymentResource.items;
|
||
readonly rentPaymentLoadState = this.rentPaymentResource.loadState;
|
||
|
||
readonly tenantAttachmentErrors = this.attachmentTransfers.errors;
|
||
readonly uploadingTenantIds = this.attachmentTransfers.uploadingTenantIds;
|
||
readonly tenantUploadProgress = this.attachmentTransfers.uploadProgress;
|
||
readonly deletingAttachmentIds = this.attachmentTransfers.deletingIds;
|
||
readonly attachmentDeleteErrors = this.attachmentTransfers.deleteErrors;
|
||
readonly downloadingAttachmentIds = this.attachmentTransfers.downloadingIds;
|
||
readonly attachmentDownloadErrors = this.attachmentTransfers.downloadErrors;
|
||
|
||
loadApartments(): void {
|
||
this.apartmentResource.load();
|
||
}
|
||
|
||
saveApartment(apartment: Apartment, id?: string, onSuccess?: () => void): void {
|
||
this.apartmentResource.save(
|
||
this.resourceKey(id),
|
||
id
|
||
? this.apartmentService.updateApartment(id, apartment)
|
||
: this.apartmentService.createApartment(apartment),
|
||
onSuccess,
|
||
);
|
||
}
|
||
|
||
apartmentSaving(id?: string): boolean {
|
||
return this.apartmentResource.savingKeys().has(this.resourceKey(id));
|
||
}
|
||
|
||
apartmentSaveError(id?: string): string {
|
||
return this.apartmentResource.saveErrors()[this.resourceKey(id)] ?? '';
|
||
}
|
||
|
||
clearApartmentSaveError(id?: string): void {
|
||
this.apartmentResource.clearSaveError(this.resourceKey(id));
|
||
}
|
||
|
||
deleteApartment(id: string): void {
|
||
this.requestConfirmation(
|
||
{
|
||
title: 'Supprimer cet appartement ?',
|
||
message: 'Êtes-vous sûr de vouloir supprimer cet appartement ?',
|
||
confirmLabel: 'Supprimer',
|
||
},
|
||
() => this.apartmentResource.delete(id, this.apartmentService.deleteApartment(id)),
|
||
);
|
||
}
|
||
|
||
loadTenants(): void {
|
||
this.tenantResource.load();
|
||
}
|
||
|
||
saveTenant(tenant: Tenant, id?: string, onSuccess?: () => void): void {
|
||
this.tenantResource.save(
|
||
this.resourceKey(id),
|
||
id ? this.tenantService.updateTenant(id, tenant) : this.tenantService.createTenant(tenant),
|
||
onSuccess,
|
||
);
|
||
}
|
||
|
||
tenantSaving(id?: string): boolean {
|
||
return this.tenantResource.savingKeys().has(this.resourceKey(id));
|
||
}
|
||
|
||
tenantSaveError(id?: string): string {
|
||
return this.tenantResource.saveErrors()[this.resourceKey(id)] ?? '';
|
||
}
|
||
|
||
clearTenantSaveError(id?: string): void {
|
||
this.tenantResource.clearSaveError(this.resourceKey(id));
|
||
}
|
||
|
||
deleteTenant(id: string): void {
|
||
this.requestConfirmation(
|
||
{
|
||
title: 'Supprimer ce locataire ?',
|
||
message: 'Êtes-vous sûr de vouloir supprimer ce locataire ?',
|
||
confirmLabel: 'Supprimer',
|
||
},
|
||
() => this.tenantResource.delete(id, this.tenantService.deleteTenant(id)),
|
||
);
|
||
}
|
||
|
||
uploadTenantAttachment(tenantId: string, file: File): void {
|
||
this.attachmentTransfers.upload(tenantId, file, (attachment) => {
|
||
this.tenantResource.update(tenantId, (tenant) => ({
|
||
...tenant,
|
||
attachments: [
|
||
...(tenant.attachments ?? []).filter((item) => item.id !== attachment.id),
|
||
attachment,
|
||
],
|
||
}));
|
||
this.tenantResource.load();
|
||
});
|
||
}
|
||
|
||
downloadTenantAttachment(tenantId: string, attachment: { id: string; fileName: string }): void {
|
||
this.attachmentTransfers.download(tenantId, attachment);
|
||
}
|
||
|
||
attachmentDownloading(attachmentId: string): boolean {
|
||
return this.attachmentTransfers.downloading(attachmentId);
|
||
}
|
||
|
||
attachmentDownloadError(attachmentId: string): string {
|
||
return this.attachmentTransfers.downloadError(attachmentId);
|
||
}
|
||
|
||
deleteTenantAttachment(tenantId: string, attachment: TenantAttachment): void {
|
||
this.requestConfirmation(
|
||
{
|
||
title: 'Retirer cette pièce jointe ?',
|
||
message: `Êtes-vous sûr de vouloir retirer « ${attachment.fileName} » ?`,
|
||
confirmLabel: 'Retirer',
|
||
},
|
||
() =>
|
||
this.attachmentTransfers.delete(tenantId, attachment.id, () => {
|
||
this.tenantResource.update(tenantId, (tenant) => ({
|
||
...tenant,
|
||
attachments: (tenant.attachments ?? []).filter((item) => item.id !== attachment.id),
|
||
}));
|
||
this.tenantResource.load();
|
||
}),
|
||
);
|
||
}
|
||
|
||
loadLeases(): void {
|
||
this.leaseResource.load();
|
||
}
|
||
|
||
loadRentDues(): void {
|
||
this.rentDueResource.load();
|
||
}
|
||
|
||
saveLease(lease: LeaseInput, id?: string, onSuccess?: () => void): void {
|
||
const input: LeaseInput = {
|
||
apartmentId: lease.apartmentId,
|
||
tenantIds: lease.tenantIds,
|
||
guarantors: lease.guarantors,
|
||
startDate: lease.startDate,
|
||
endDate: lease.endDate,
|
||
monthlyRent: lease.monthlyRent,
|
||
monthlyCharges: lease.monthlyCharges,
|
||
securityDeposit: lease.securityDeposit,
|
||
};
|
||
this.leaseResource.save(
|
||
this.resourceKey(id),
|
||
id ? this.leaseService.updateLease(id, input) : this.leaseService.createLease(input),
|
||
onSuccess,
|
||
);
|
||
}
|
||
|
||
leaseSaving(id?: string): boolean {
|
||
return this.leaseResource.savingKeys().has(this.resourceKey(id));
|
||
}
|
||
|
||
leaseSaveError(id?: string): string {
|
||
return this.leaseResource.saveErrors()[this.resourceKey(id)] ?? '';
|
||
}
|
||
|
||
clearLeaseSaveError(id?: string): void {
|
||
this.leaseResource.clearSaveError(this.resourceKey(id));
|
||
}
|
||
|
||
activateLease(id: string): void {
|
||
if (this.activatingLeaseIds().has(id)) return;
|
||
this.activatingLeaseIds.update((ids) => new Set(ids).add(id));
|
||
this.leaseActivationErrors.update((errors) => withoutRecordKey(errors, id));
|
||
this.leaseService
|
||
.activateLease(id)
|
||
.pipe(finalize(() => this.activatingLeaseIds.update((ids) => withoutSetValue(ids, id))))
|
||
.subscribe({
|
||
next: ({ lease, rentDues }) => {
|
||
this.leaseResource.upsert(lease);
|
||
rentDues.forEach((rentDue) => this.rentDueResource.upsert(rentDue));
|
||
this.leaseResource.load();
|
||
this.rentDueResource.load();
|
||
},
|
||
error: (error) =>
|
||
this.leaseActivationErrors.update((errors) => ({
|
||
...errors,
|
||
[id]: httpErrorMessage(error, 'Impossible d’activer ce bail.'),
|
||
})),
|
||
});
|
||
}
|
||
|
||
leaseActivating(id: string): boolean {
|
||
return this.activatingLeaseIds().has(id);
|
||
}
|
||
|
||
leaseActivationError(id: string): string {
|
||
return this.leaseActivationErrors()[id] ?? '';
|
||
}
|
||
|
||
deleteLease(id: string): void {
|
||
this.requestConfirmation(
|
||
{
|
||
title: 'Supprimer ce bail ?',
|
||
message: 'Êtes-vous sûr de vouloir supprimer ce bail ?',
|
||
confirmLabel: 'Supprimer',
|
||
},
|
||
() => this.leaseResource.delete(id, this.leaseService.deleteLease(id)),
|
||
);
|
||
}
|
||
|
||
downloadLeasePdf(lease: Lease): void {
|
||
if (!lease.id) return;
|
||
this.downloads.download(
|
||
this.leaseDownloadKey(lease.id),
|
||
this.leaseService.downloadPdf(lease.id),
|
||
`bail-${lease.id}.pdf`,
|
||
'Impossible de télécharger ce bail.',
|
||
);
|
||
}
|
||
|
||
leaseDownloading(leaseId: string): boolean {
|
||
return this.downloads.isPending(this.leaseDownloadKey(leaseId));
|
||
}
|
||
|
||
leaseDownloadError(leaseId: string): string {
|
||
return this.downloads.error(this.leaseDownloadKey(leaseId));
|
||
}
|
||
|
||
loadRentPayments(): void {
|
||
this.rentPaymentResource.load();
|
||
}
|
||
|
||
acceptRentPayment(payment: RentPayment): void {
|
||
this.rentPaymentResource.upsert(payment);
|
||
this.rentPaymentResource.load();
|
||
this.rentDueResource.load();
|
||
}
|
||
|
||
downloadRentReceipt(rentDue: RentDue): void {
|
||
this.downloads.download(
|
||
this.rentReceiptDownloadKey(rentDue.id),
|
||
this.leaseService.downloadRentReceipt(rentDue.id),
|
||
`quittance-${rentDue.month}.pdf`,
|
||
'Impossible de télécharger cette quittance.',
|
||
);
|
||
}
|
||
|
||
downloadPaymentReceipt(payment: RentPayment): void {
|
||
this.downloads.download(
|
||
this.paymentReceiptDownloadKey(payment.id),
|
||
this.rentPaymentService.downloadReceipt(payment.id),
|
||
`recu-reglement-${payment.id}.pdf`,
|
||
'Impossible de télécharger ce reçu de règlement.',
|
||
);
|
||
}
|
||
|
||
rentReceiptDownloading(paymentId: string): boolean {
|
||
return this.downloads.isPending(this.rentReceiptDownloadKey(paymentId));
|
||
}
|
||
|
||
rentReceiptDownloadError(paymentId: string): string {
|
||
return this.downloads.error(this.rentReceiptDownloadKey(paymentId));
|
||
}
|
||
|
||
paymentReceiptDownloading(paymentId: string): boolean {
|
||
return this.downloads.isPending(this.paymentReceiptDownloadKey(paymentId));
|
||
}
|
||
|
||
paymentReceiptDownloadError(paymentId: string): string {
|
||
return this.downloads.error(this.paymentReceiptDownloadKey(paymentId));
|
||
}
|
||
|
||
private resourceKey(id?: string): string {
|
||
return id ?? newResourceKey;
|
||
}
|
||
|
||
private leaseDownloadKey(leaseId: string): string {
|
||
return `lease:${leaseId}`;
|
||
}
|
||
|
||
private rentReceiptDownloadKey(paymentId: string): string {
|
||
return `rent-receipt:${paymentId}`;
|
||
}
|
||
|
||
private paymentReceiptDownloadKey(paymentId: string): string {
|
||
return `payment-receipt:${paymentId}`;
|
||
}
|
||
|
||
private requestConfirmation(data: ConfirmationDialogData, onConfirmed: () => void): void {
|
||
this.dialog
|
||
.open(ConfirmationDialog, {
|
||
data,
|
||
role: 'alertdialog',
|
||
width: 'min(420px, calc(100vw - 32px))',
|
||
maxWidth: 'calc(100vw - 32px)',
|
||
})
|
||
.afterClosed()
|
||
.subscribe((confirmed) => {
|
||
if (confirmed) onConfirmed();
|
||
});
|
||
}
|
||
}
|