Asset Externalization Owner management for Loans (#1771)

Co-authored-by: Jose Alberto Hernandez <alberto@blue-daemon.local>
This commit is contained in:
José Alberto Hernández
2023-06-21 12:17:47 -06:00
committed by GitHub
co-authored by Jose Alberto Hernandez
parent 1b60a8f43c
commit bddf46cccd
36 changed files with 704 additions and 126 deletions
@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { ExternalAssetOwnerService } from '../external-asset-owner.service';
@Injectable({
providedIn: 'root'
})
export class ExternalAssetOwnerActiveTransferResolver implements Resolve<boolean> {
/**
* @param {ExternalAssetOwnerService} externalAssetOwnerService External Asset Owner service.
*/
constructor(private externalAssetOwnerService: ExternalAssetOwnerService) { }
/**
* Returns the Loans with Association data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
return this.externalAssetOwnerService.retrieveExternalAssetOwnerActiveTransfer(loanId);
}
}
@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { ExternalAssetOwnerService } from '../external-asset-owner.service';
@Injectable({
providedIn: 'root'
})
export class ExternalAssetOwnerJournalEntryResolver implements Resolve<boolean> {
/**
* @param {ExternalAssetOwnerService} externalAssetOwnerService External Asset Owner service.
*/
constructor(private externalAssetOwnerService: ExternalAssetOwnerService) { }
/**
* Returns the Loans with Association data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const transferId = route.paramMap.get('transferId') || route.parent.paramMap.get('transferId');
return this.externalAssetOwnerService.retrieveExternalAssetOwnerTransferJournalEntries(transferId);
}
}
@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { ExternalAssetOwnerService } from '../external-asset-owner.service';
@Injectable({
providedIn: 'root'
})
export class ExternalAssetOwnerResolver implements Resolve<boolean> {
/**
* @param {ExternalAssetOwnerService} externalAssetOwnerService External Asset Owner service.
*/
constructor(private externalAssetOwnerService: ExternalAssetOwnerService) { }
/**
* Returns the Loans with Association data.
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const loanId = route.paramMap.get('loanId') || route.parent.paramMap.get('loanId');
return this.externalAssetOwnerService.retrieveExternalAssetOwnerTransfers(loanId);
}
}
@@ -0,0 +1,49 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ExternalAssetOwnerService {
constructor(private http: HttpClient) { }
/**
* @param {string} loanId Loan Id
* @param {string} command Command
* @param {any} data Data
* @returns {Observable<any>}
*/
executeExternalAssetOwnerLoanCommand(loanId: string, data: any, command: string): Observable<any> {
const httpParams = new HttpParams().set('command', command);
return this.http.post(`/external-asset-owners/transfers/loans/${loanId}`, data, { params: httpParams });
}
/**
* @param {string} loanId Loan Id
* @returns {Observable<any>}
*/
retrieveExternalAssetOwnerTransfers(loanId: string): Observable<any> {
const httpParams = new HttpParams().set('loanId', loanId);
return this.http.get(`/external-asset-owners/transfers`, { params: httpParams });
}
/**
* @param {string} loanId Loan Id
* @returns {Observable<any>}
*/
retrieveExternalAssetOwnerActiveTransfer(loanId: string): Observable<any> {
const httpParams = new HttpParams().set('loanId', loanId);
return this.http.get(`/external-asset-owners/transfers/active-transfer`, { params: httpParams });
}
/**
* @param {string} transferId Transfer Id
* @returns {Observable<any>}
*/
retrieveExternalAssetOwnerTransferJournalEntries(transferId: string): Observable<any> {
return this.http.get(`/external-asset-owners/transfers/${transferId}/journal-entries`);
}
}
+15 -1
View File
@@ -57,6 +57,10 @@ import { LoanReschedulesResolver } from './common-resolvers/loan-reschedules.res
import { RescheduleLoanTabComponent } from './loans-view/reschedule-loan-tab/reschedule-loan-tab.component';
import { AdjustLoanChargeComponent } from './loans-view/loan-account-actions/adjust-loan-charge/adjust-loan-charge.component';
import { LoanArrearDelinquencyResolver } from './common-resolvers/loan-arrear-delinquency.resolver';
import { ExternalAssetOwnerTabComponent } from './loans-view/external-asset-owner-tab/external-asset-owner-tab.component';
import { ExternalAssetOwnerResolver } from './common-resolvers/external-asset-owner.resolver';
import { ExternalAssetOwnerActiveTransferResolver } from './common-resolvers/external-asset-owner-active-transfer.resolver';
import { ExternalAssetOwnerJournalEntryResolver } from './common-resolvers/external-asset-owner-journal-entry.resolver';
/** Loans Route. */
const routes: Routes = [
@@ -198,6 +202,15 @@ const routes: Routes = [
component: StandingInstructionsTabComponent,
data: { title: extract('Standing Instructions'), breadcrumb: 'Standing Instructions', routeParamBreadcrumb: false }
},
{
path: 'external-asset-owner',
component: ExternalAssetOwnerTabComponent,
data: { title: extract('External Asset Owner'), breadcrumb: 'External Asset Owner', routeParamBreadcrumb: false },
resolve: {
activeTransferData: ExternalAssetOwnerActiveTransferResolver,
loanTransfersData: ExternalAssetOwnerResolver
}
},
{
path: 'datatables',
children: [{
@@ -349,7 +362,8 @@ const routes: Routes = [
GroupAccountsResolver,
GLIMViewResolver,
GSIMAccountsResolver,
GLIMLoanTemplateResolver
GLIMLoanTemplateResolver,
ExternalAssetOwnerResolver
]
})
@@ -0,0 +1,81 @@
<div class="tab-container mat-typography">
<div fxLayout="row" fxLayoutAlign="start center">
<h3 fxFlex="90%">External Asset Owner</h3>
<div fxFlex="10%" *ngIf="canBeSaled()" >
<button mat-raised-button class="f-right" color="primary" (click)="saleLoan()" *mifosxHasPermission="'SALE_LOAN'">
<fa-icon icon="dollar-sign" class="m-r-10"></fa-icon>Sale Loan
</button>
</div>
<div fxFlex="10%" *ngIf="canBeCancelled()" >
<button mat-raised-button class="f-right" color="warn" (click)="cancelSaleLoan()" *mifosxHasPermission="'SALE_LOAN'">
<fa-icon icon="trash" class="m-r-10"></fa-icon>Cancel Sale Loan
</button>
</div>
<div fxFlex="10%" *ngIf="canBeBuyed()" >
<button mat-raised-button class="f-right" color="primary" (click)="buyBackLoan()" *mifosxHasPermission="'SALE_LOAN'">
<fa-icon icon="dollar-sign" class="m-r-10"></fa-icon>Buy Back Loan
</button>
</div>
</div>
<table mat-table mat-sort [dataSource]="loanTransfersData" *ngIf="loanTransfersData.length>0">
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef> Status </th>
<td mat-cell *matCellDef="let item">
<div [ngClass]="itemStatus(item.status)">
<fa-icon icon="stop"></fa-icon>
<span class="m-l-10 status">
{{ item.status | translate }}
</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="effectiveFrom">
<th mat-header-cell *matHeaderCellDef> Effective From </th>
<td mat-cell *matCellDef="let item"> {{ item.effectiveFrom | dateFormat }}</td>
</ng-container>
<ng-container matColumnDef="ownerExternalId">
<th mat-header-cell *matHeaderCellDef> Owner External Id </th>
<td mat-cell *matCellDef="let item">
<mifosx-external-identifier externalId="{{item.owner.externalId}}"></mifosx-external-identifier>
</td>
</ng-container>
<ng-container matColumnDef="transferExternalId">
<th mat-header-cell *matHeaderCellDef> Transfer External Id </th>
<td mat-cell *matCellDef="let item">
<mifosx-external-identifier externalId="{{item.transferExternalId}}"></mifosx-external-identifier>
</td>
</ng-container>
<ng-container matColumnDef="settlementDate">
<th mat-header-cell *matHeaderCellDef> Settlement Date </th>
<td mat-cell *matCellDef="let item"> {{ item.settlementDate | dateFormat }}</td>
</ng-container>
<ng-container matColumnDef="purchasePriceRatio">
<th mat-header-cell *matHeaderCellDef> Purchase Price Ratio </th>
<td mat-cell *matCellDef="let item"> {{ item.purchasePriceRatio | number }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell class="center" *matHeaderCellDef> Actions </th>
<td mat-cell *matCellDef="let item" class="center td-min-space">
<button class="account-action-button" *ngIf="!isPending(item)" mat-raised-button color="primary" matTooltip="View Journal Entries"
matTooltipPosition="left" (click)="routeJournalEntry($event)" [routerLink]="['/', 'journal-entry', 'view-transfer', item.transferId]">
<i class="fa fa-arrow-circle-right"></i>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="loanTransferColumns"></tr>
<tr mat-row *matRowDef="let row; columns: loanTransferColumns;"></tr>
</table>
</div>
@@ -0,0 +1,35 @@
.container {
padding-bottom: 2%;
.transaction-buttons {
padding-bottom: 1rem;
padding-top: 1rem;
.accruals {
padding-top: 1%;
}
}
table {
width: 100%;
.select-row:hover {
cursor: pointer;
}
}
}
.tab-container {
padding: 1%;
margin: 1%;
}
.status {
color: black;
}
.account-action-button{
min-width: 26px;
padding: 0 0 3px 0;
margin: 0 2%;
line-height: 25px;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExternalAssetOwnerTabComponent } from './external-asset-owner-tab.component';
describe('ExternalAssetOwnerTabComponent', () => {
let component: ExternalAssetOwnerTabComponent;
let fixture: ComponentFixture<ExternalAssetOwnerTabComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ExternalAssetOwnerTabComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ExternalAssetOwnerTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,97 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { ExternalAssetOwnerService } from 'app/loans/external-asset-owner.service';
import { CancelDialogComponent } from 'app/shared/cancel-dialog/cancel-dialog.component';
@Component({
selector: 'mifosx-external-asset-owner-tab',
templateUrl: './external-asset-owner-tab.component.html',
styleUrls: ['./external-asset-owner-tab.component.scss']
})
export class ExternalAssetOwnerTabComponent implements OnInit {
loanTransfersData: any[] = [];
activeTransferData: any;
loanTransferColumns: string[] = ['status', 'effectiveFrom', 'ownerExternalId', 'transferExternalId', 'settlementDate', 'purchasePriceRatio', 'actions'];
currentItem: any;
constructor(private route: ActivatedRoute,
private router: Router,
private dialog: MatDialog,
private externalAssetOwnerService: ExternalAssetOwnerService
) {
this.route.data.subscribe((data: { loanTransfersData: any, activeTransferData: any }) => {
this.loanTransfersData = data.loanTransfersData.empty ? [] : data.loanTransfersData.content;
this.activeTransferData = data.activeTransferData || {};
});
}
ngOnInit(): void {
this.currentItem = null;
if (this.loanTransfersData.length > 0) {
this.currentItem = this.loanTransfersData[(this.loanTransfersData.length - 1)];
}
}
itemStatus(status: string): string {
return 'status-' + status.toLowerCase();
}
isPending(item: any): boolean {
return (item.status === 'PENDING');
}
canBeCancelled(): boolean {
return this.validateStatus('PENDING');
}
canBeSaled(): boolean {
if (this.currentItem == null) {
return true;
}
return ['', 'BUYBACK'].includes(this.currentItem.status);
}
canBeBuyed(): boolean {
return this.validateStatus('ACTIVE');
}
private validateStatus(status: string): boolean {
if (this.currentItem != null) {
return (this.currentItem.status === status);
}
return false;
}
saleLoan(): void {
this.router.navigate(['../actions/Sale Loan'], { relativeTo: this.route });
}
cancelSaleLoan(): void {
const deleteDataTableDialogRef = this.dialog.open(CancelDialogComponent, {
data: { cancelContext: `the Asset Transfer with the Owner External Id ${this.currentItem.owner.externalId} ` }
});
deleteDataTableDialogRef.afterClosed().subscribe((response: any) => {
if (response.cancel) {
const payload: any = {
transferExternalId: this.currentItem.transferExternalId
};
this.externalAssetOwnerService.executeExternalAssetOwnerLoanCommand(this.currentItem.loan.loanId, payload, 'cancel')
.subscribe((result: any) => {
console.log(result);
});
}
});
}
buyBackLoan(): void {
this.router.navigate(['../actions/Buy Back Loan'], { relativeTo: this.route });
}
routeJournalEntry(ev: MouseEvent): void {
ev.stopPropagation();
}
}
@@ -0,0 +1,57 @@
<div class="container mat-elevation-z8">
<mat-card>
<form [formGroup]="saleLoanForm" (ngSubmit)="submit()">
<mat-card-content>
<div fxLayout="column">
<mat-form-field (click)="settlementDatePicker.open()">
<mat-label>Settlement Date</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="settlementDatePicker" required
formControlName="settlementDate">
<mat-datepicker-toggle matSuffix [for]="settlementDatePicker"></mat-datepicker-toggle>
<mat-datepicker #settlementDatePicker></mat-datepicker>
<mat-error *ngIf="saleLoanForm.controls.settlementDate.hasError('required')">
Settlement Date <strong>is required</strong>
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="!isBuyBack()">
<mat-label>Purchase Price Ratio</mat-label>
<input type="number" matInput required formControlName="purchasePriceRatio">
<mat-error *ngIf="saleLoanForm.controls.purchasePriceRatio.hasError('required')">
Purchase Price Ratio <strong>is required</strong>
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="!isBuyBack()">
<mat-label>Owner External Id</mat-label>
<input matInput formControlName="ownerExternalId">
<mat-error *ngIf="saleLoanForm.controls.ownerExternalId.hasError('required')">
Owner External Id <strong>is required</strong>
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Transfer External Id</mat-label>
<input matInput formControlName="transferExternalId">
</mat-form-field>
</div>
<mat-card-actions fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" fxLayoutGap="5px">
<button type="button" mat-raised-button [routerLink]="['../../general']">Cancel</button>
<button mat-raised-button color="primary" [disabled]="!saleLoanForm.valid"
*mifosxHasPermission="'SALE_LOAN'">Submit</button>
</mat-card-actions>
</mat-card-content>
</form>
</mat-card>
</div>
@@ -0,0 +1,3 @@
.container {
max-width: 37rem;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AssetTransferLoanComponent } from './asset-transfer-loan.component';
describe('AssetTransferLoanComponent', () => {
let component: AssetTransferLoanComponent;
let fixture: ComponentFixture<AssetTransferLoanComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AssetTransferLoanComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AssetTransferLoanComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,90 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
import { ExternalAssetOwnerService } from 'app/loans/external-asset-owner.service';
import { SettingsService } from 'app/settings/settings.service';
@Component({
selector: 'mifosx-asset-transfer-loan',
templateUrl: './asset-transfer-loan.component.html',
styleUrls: ['./asset-transfer-loan.component.scss']
})
export class AssetTransferLoanComponent implements OnInit {
BUYBACK_COMMAND = 'buyback';
SALE_COMMAND = 'sale';
command: string;
/** Loan Id */
loanId: string;
/** Minimum Date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum Date allowed. */
maxDate = new Date();
/** Sale Loan Form */
saleLoanForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private externalAssetOwnerService: ExternalAssetOwnerService,
private route: ActivatedRoute,
private router: Router,
private dateUtils: Dates,
private settingsService: SettingsService) {
this.loanId = this.route.snapshot.params['loanId'];
const actionName = this.route.snapshot.params['action'];
this.command = (actionName === 'Sale Loan') ? this.SALE_COMMAND : this.BUYBACK_COMMAND;
}
/**
* Creates the sale loan form
* and initialize with the required values
*/
ngOnInit() {
this.minDate = this.settingsService.businessDate;
this.maxDate = this.settingsService.maxAllowedDate;
this.createSaleLoanForm();
}
isBuyBack(): boolean {
return (this.command === this.BUYBACK_COMMAND);
}
/**
* Creates the create close form.
*/
createSaleLoanForm() {
this.saleLoanForm = this.formBuilder.group({
'settlementDate': [this.settingsService.businessDate, Validators.required],
'purchasePriceRatio': ['', Validators.required],
'transferExternalId': '',
'ownerExternalId': ['', Validators.required]
});
if (this.isBuyBack()) {
this.saleLoanForm.removeControl('purchasePriceRatio');
this.saleLoanForm.removeControl('ownerExternalId');
}
}
submit() {
const saleLoanFormData = this.saleLoanForm.value;
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const prevsettlementDate: Date = this.saleLoanForm.value.settlementDate;
if (saleLoanFormData.settlementDate instanceof Date) {
saleLoanFormData.settlementDate = this.dateUtils.formatDate(prevsettlementDate, dateFormat);
}
const data = {
...saleLoanFormData,
dateFormat,
locale
};
this.externalAssetOwnerService.executeExternalAssetOwnerLoanCommand(this.loanId, data, this.command)
.subscribe((response: any) => {
this.router.navigate(['../../external-asset-owner'], { relativeTo: this.route });
});
}
}
@@ -27,3 +27,4 @@
<mifosx-edit-repayment-schedule *ngIf="actions['Edit Repayment Schedule']"></mifosx-edit-repayment-schedule>
<mifosx-charge-off [dataObject]="actionButtonData" *ngIf="actions['Charge-Off']"></mifosx-charge-off>
<mifosx-undo-command [actionName]="actionName" *ngIf="actions['Undo Charge-Off']"></mifosx-undo-command>
<mifosx-asset-transfer-loan *ngIf="actions['Sale Loan'] || actions['Buy Back Loan']"></mifosx-asset-transfer-loan>
@@ -42,7 +42,9 @@ export class LoanAccountActionsComponent {
'Credit Balance Refund': boolean,
'Edit Repayment Schedule': boolean,
'Charge-Off': boolean,
'Undo Charge-Off': boolean
'Undo Charge-Off': boolean,
'Sale Loan': boolean,
'Buy Back Loan': boolean
} = {
'Close': false,
'Undo Approval': false,
@@ -73,7 +75,9 @@ export class LoanAccountActionsComponent {
'Credit Balance Refund': false,
'Edit Repayment Schedule': false,
'Charge-Off': false,
'Undo Charge-Off': false
'Undo Charge-Off': false,
'Sale Loan': false,
'Buy Back Loan': false
};
actionButtonData: any;
@@ -189,6 +189,10 @@ export class LoansAccountButtonConfiguration {
{
name: 'Recover From Guarantor',
taskPermissionName: 'RECOVERGUARANTEES_LOAN',
},
{
name: 'Sale Loan',
taskPermissionName: 'SALE_LOAN',
}
];
this.optionPaymentArray = [
@@ -135,7 +135,7 @@
Transactions
</a>
</ng-container>
<ng-container>
<ng-container *ngIf="loanDetailsData.status.active">
<a mat-tab-link [routerLink]="['./delinquencytags']" routerLinkActive #delinquencytags="routerLinkActive"
[active]="delinquencytags.isActive">
Delinquency Tags
@@ -171,10 +171,12 @@
Charges
</a>
</ng-container>
<a mat-tab-link [routerLink]="['./loan-reschedules']" routerLinkActive #loanReschedules="routerLinkActive"
[active]="loanReschedules.isActive">
Loan Reschedules
</a>
<ng-container *ngIf="loanDetailsData.status.active">
<a mat-tab-link [routerLink]="['./loan-reschedules']" routerLinkActive #loanReschedules="routerLinkActive"
[active]="loanReschedules.isActive">
Loan Reschedules
</a>
</ng-container>
<a mat-tab-link [routerLink]="['./loan-documents']" routerLinkActive #loanDocuments="routerLinkActive"
[active]="loanDocuments.isActive">
Loan Documents
@@ -189,6 +191,12 @@
Standing Instruction
</a>
</ng-container>
<ng-container *ngIf="loanDetailsData.status.active">
<a mat-tab-link [routerLink]="['./external-asset-owner']" routerLinkActive #externalAssetOwner="routerLinkActive"
[active]="externalAssetOwner.isActive">
External Asset Owner
</a>
</ng-container>
<ng-container *ngFor="let loanDatatable of loanDatatables">
<a mat-tab-link *mifosxHasPermission="'READ_' + loanDatatable.registeredTableName"
[routerLink]="['./datatables',loanDatatable.registeredTableName]" routerLinkActive
+5 -1
View File
@@ -75,6 +75,8 @@ import { LoansAccountDatatableStepComponent } from './loans-account-stepper/loan
import { RescheduleLoanTabComponent } from './loans-view/reschedule-loan-tab/reschedule-loan-tab.component';
import { AdjustLoanChargeComponent } from './loans-view/loan-account-actions/adjust-loan-charge/adjust-loan-charge.component';
import { ChargeOffComponent } from './loans-view/loan-account-actions/charge-off/charge-off.component';
import { AssetTransferLoanComponent } from './loans-view/loan-account-actions/asset-transfer-loan/asset-transfer-loan.component';
import { ExternalAssetOwnerTabComponent } from './loans-view/external-asset-owner-tab/external-asset-owner-tab.component';
/**
* Loans Module
@@ -148,7 +150,9 @@ import { ChargeOffComponent } from './loans-view/loan-account-actions/charge-off
LoansAccountDatatableStepComponent,
RescheduleLoanTabComponent,
AdjustLoanChargeComponent,
ChargeOffComponent
ChargeOffComponent,
AssetTransferLoanComponent,
ExternalAssetOwnerTabComponent
],
providers: [ ],
})