@cbm-common/credit-request-repository
v0.0.1
Published
Repositorio Angular para la gestión completa de solicitudes de crédito en sistemas financieros. Implementa operaciones CRUD, timeline de seguimiento, cambios de estado, validaciones de cliente y envío de notificaciones por email con integración completa a
Downloads
4
Readme
Credit Request Repository
Repositorio Angular para la gestión completa de solicitudes de crédito en sistemas financieros. Implementa operaciones CRUD, timeline de seguimiento, cambios de estado, validaciones de cliente y envío de notificaciones por email con integración completa al sistema CBM.
📦 Instalación
npm install @cbm-common/credit-request-repository⚙️ Configuración
Configuración del Módulo
El módulo debe configurarse en el módulo raíz de la aplicación:
import { CbmCreditRequestModule } from '@cbm-common/credit-request-repository';
@NgModule({
imports: [
CbmCreditRequestModule.forRoot({
baseUrl: 'https://api.cbm.com/credit-requests'
})
]
})
export class AppModule {}Configuración Standalone
Para aplicaciones standalone, configura el módulo en el bootstrap:
import { CbmCreditRequestModule } from '@cbm-common/credit-request-repository';
bootstrapApplication(AppComponent, {
providers: [
CbmCreditRequestModule.forRoot({
baseUrl: 'https://api.cbm.com/credit-requests'
})
]
});🎯 Inyección de Dependencias
Inyección del Servicio
import { CbmCreditRequestService } from '@cbm-common/credit-request-repository';
@Component({
selector: 'app-credit-request-manager',
standalone: true,
imports: [CbmCreditRequestService]
})
export class CreditRequestManagerComponent {
constructor(private creditRequestService: CbmCreditRequestService) {}
}Inyección del Repositorio
import { CbmCreditRequestRepository } from '@cbm-common/credit-request-repository';
@Component({
selector: 'app-credit-request-list',
standalone: true,
imports: [CbmCreditRequestRepository]
})
export class CreditRequestListComponent {
constructor(private creditRequestRepo: CbmCreditRequestRepository) {}
}🏗️ Arquitectura del Repositorio
Patrón de Diseño
El repositorio sigue el patrón Repository Pattern con Dependency Injection:
CbmCreditRequestModule
├── ICbmCreditRequestModuleConfig (configuración)
├── CbmCreditRequestService (implementa ICbmCreditRequestRepository)
├── CbmCreditRequestRepository (wrapper del service)
├── CbmCreditRequestModel (modelos de datos)
└── HttpClient (cliente HTTP)Interfaz del Repositorio
export interface ICbmCreditRequestRepository {
list(params: CbmCreditRequestModel.ListParams): Observable<CbmCreditRequestModel.ListResponse>;
getOne(id: string): Observable<CbmCreditRequestModel.GetOneResponse>;
timeline(id: string): Observable<CbmCreditRequestModel.TimelineResponse[]>;
save(data: CbmCreditRequestModel.SaveBody): Observable<CbmCreditRequestModel.ConfirmResponse>;
changeStatus(id: string, data: CbmCreditRequestModel.ChangeStatusBody): Observable<CbmCreditRequestModel.ConfirmResponse>;
validationOperationClient(request: CbmCreditRequestModel.ValidationOperationClientParams): Observable<CbmCreditRequestModel.ValidationOperationClientResponse>;
sendEmail(id: string, params: CbmCreditRequestModel.SendEmailParams): Observable<CbmCreditRequestModel.ConfirmResponse>;
}📊 Operaciones Disponibles
Listado de Solicitudes de Crédito
// Listado paginado con filtros avanzados
list(params: {
page: number; // Página actual
size: number; // Tamaño de página
date_begin?: number; // Fecha inicio (timestamp)
date_end?: number; // Fecha fin (timestamp)
client_id?: string; // ID del cliente
new_credit_number?: string; // Número de nueva solicitud
document_state?: string; // Estado del documento
type?: string; // Tipo de solicitud
}): Observable<ListResponse>Obtener Solicitud Individual
// Obtener una solicitud específica por ID
getOne(id: string): Observable<GetOneResponse>Timeline de Solicitud
// Obtener el historial/timeline de una solicitud
timeline(id: string): Observable<TimelineResponse[]>Crear Nueva Solicitud
// Crear una nueva solicitud de crédito
save(data: SaveBody): Observable<ConfirmResponse>Cambiar Estado
// Cambiar el estado de una solicitud (aprobar, rechazar, etc.)
changeStatus(id: string, data: ChangeStatusBody): Observable<ConfirmResponse>Validar Operación de Cliente
// Validar si un cliente puede realizar operaciones
validationOperationClient(request: {
client_id: string; // ID del cliente a validar
}): Observable<ValidationOperationClientResponse>Enviar Email
// Enviar notificación por email
sendEmail(id: string, params: {
timezone: string; // Zona horaria
locale: string; // Idioma/localización
emails?: string; // Emails adicionales (opcional)
}): Observable<ConfirmResponse>📋 Modelos de Datos
ListResponse.Item
Información completa de una solicitud de crédito:
interface Item {
_id: string;
company_id?: string;
company_branch_id?: string;
// Información de la empresa
company_NIF?: string;
company_address?: string;
company_trade_name?: string;
company_business_name?: string;
// Información de la sucursal
company_branch_identification_number?: string;
company_branch_trade_name?: string;
company_branch_logo?: string;
company_branch_address?: string;
company_branch_email?: string;
company_branch_cellphone?: string;
company_branch_phone?: string;
// Información del usuario
user_id?: string;
// Información del cliente
client_id?: string;
client_branch_id?: string;
client_business_name?: string;
client_document_number?: string;
client_credit_limit?: number;
client_trade_name?: string;
client_email?: string;
client_address?: string;
client_phone_code?: string;
client_cellphone?: string;
// Ubicación del cliente
client_province_id?: string;
client_canton_id?: string;
client_parish_id?: string;
client_province_code?: string;
client_province_name?: string;
client_canton_code?: string;
client_canton_name?: string;
client_parish_code?: string;
client_parish_name?: string;
// Información del documento
document_nomenclature?: string;
document_number?: string;
document_state?: string;
// Valores financieros
request_value?: number; // Valor solicitado
decrease_value?: number; // Valor de disminución
increase_value?: number; // Valor de aumento
approved_value?: number; // Valor aprobado
// Información general
type?: string; // Tipo de solicitud
reason?: string; // Razón/motivo
created_user?: string; // Usuario creador
created_at?: number; // Fecha de creación
// Información de sucursal del cliente
client_branch_code?: string;
client_branch_name?: string;
client_branch_address?: string;
client_branch_email?: string;
client_branch_cellphone?: string;
client_branch_phone?: string;
client_branch_phone_code?: string;
client_payment_deadline?: number; // Plazo de pago del cliente
// Información del vendedor
seller_id?: string;
seller_identification_number?: string;
seller_full_name?: string;
seller_document_number?: string;
seller_address?: string;
seller_cellphone?: string;
seller_email?: string[]; // Array de emails
// Número de nueva solicitud
new_credit_number?: string;
}SaveBody
Datos para crear una nueva solicitud de crédito:
interface SaveBody {
// Información de la empresa
company_NIF?: string;
company_address?: string;
company_trade_name?: string;
company_business_name?: string;
// Información de sucursal
company_branch_identification_number?: string;
company_branch_trade_name?: string;
company_branch_logo?: string;
company_branch_address?: string;
company_branch_email?: string;
company_branch_cellphone?: string;
company_branch_phone?: string;
// Información del cliente
client_id?: string;
client_business_name?: string;
client_document_number?: string;
client_credit_limit?: number;
client_trade_name?: string;
client_email?: string;
client_address?: string;
client_phone_code?: string;
client_cellphone?: string;
// Ubicación del cliente
client_province_id?: string;
client_canton_id?: string;
client_parish_id?: string;
client_province_code?: string;
client_province_name?: string;
client_canton_code?: string;
client_canton_name?: string;
client_parish_code?: string;
client_parish_name?: string;
// Razón y tipo
reason?: string;
type?: string;
// Valores financieros
request_value?: number; // Valor solicitado
increase_value?: number; // Valor de aumento
decrease_value?: number; // Valor de disminución
revoke_value?: number; // Valor de revocación
// Sucursal del cliente
client_branch_id?: string;
client_payment_deadline?: number;
client_branch_code?: string;
client_branch_name?: string;
client_branch_address?: string;
client_branch_email?: string;
client_branch_cellphone?: string;
client_branch_phone?: string;
client_branch_phone_code?: string;
// Información del vendedor
seller_id?: string;
seller_identification_number?: string;
seller_document_number?: string;
seller_full_name?: string;
seller_address?: string;
seller_email?: string[];
seller_cellphone?: string;
// Razones de país
reason_country_id?: string;
reason_country_code?: string;
reason_country_description?: string;
reason_country_description_process?: string;
}TimelineResponse
Información del historial de una solicitud:
interface TimelineResponse {
_id: string;
document_nomenclature: string; // Nomenclatura del documento
document_number: string; // Número del documento
document_state: string; // Estado del documento
type: string; // Tipo de cambio
date: number; // Fecha del cambio
user: string; // Usuario que realizó el cambio
credit_limit: number; // Límite de crédito
request_value: number; // Valor solicitado
approved_value: number; // Valor aprobado
commentary?: string; // Comentario opcional
payment_deadline: number; // Plazo de pago
previous_payment_deadline: number; // Plazo anterior
}ChangeStatusBody
Datos para cambiar el estado de una solicitud:
interface ChangeStatusBody {
document_state?: string; // Nuevo estado del documento
approved_value?: number; // Valor aprobado
commentary?: string; // Comentario del cambio
payment_deadline?: number; // Nuevo plazo de pago
previous_payment_deadline?: number; // Plazo anterior
quota_number?: number; // Número de cuota
grace_days?: number; // Días de gracia
// Razones de aprobación
reason_country_id?: string;
reason_country_code?: string;
reason_country_description?: string;
reason_country_description_process?: string;
// Razones de denegación
deny_reason_country_id?: string;
deny_reason_country_code?: string;
deny_reason_country_description?: string;
deny_reason_country_description_process?: string;
}🚀 Ejemplos de Uso
Ejemplo Básico: Listado de Solicitudes
import { CbmCreditRequestService } from '@cbm-common/credit-request-repository';
import { CbmCreditRequestModel } from '@cbm-common/credit-request-repository';
@Component({
selector: 'app-credit-request-list',
standalone: true,
template: `
<div class="credit-request-list">
<h2>Solicitudes de Crédito</h2>
<div class="filters">
<input [(ngModel)]="filters.document_number" placeholder="Número de documento">
<input [(ngModel)]="filters.client_document_number" placeholder="Cédula/RUC del cliente">
<select [(ngModel)]="filters.document_state">
<option value="">Todos los estados</option>
<option value="PENDING">Pendiente</option>
<option value="APPROVED">Aprobado</option>
<option value="REJECTED">Rechazado</option>
</select>
<select [(ngModel)]="filters.type">
<option value="">Todos los tipos</option>
<option value="INCREASE">Aumento</option>
<option value="DECREASE">Disminución</option>
<option value="NEW">Nueva solicitud</option>
</select>
<input [(ngModel)]="filters.date_begin" type="date" placeholder="Fecha inicio">
<input [(ngModel)]="filters.date_end" type="date" placeholder="Fecha fin">
<button (click)="loadRequests()">Buscar</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Número Documento</th>
<th>Cliente</th>
<th>Vendedor</th>
<th>Valor Solicitado</th>
<th>Valor Aprobado</th>
<th>Estado</th>
<th>Tipo</th>
<th>Fecha</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let request of requests">
<td>{{ request.document_number }}</td>
<td>
<div class="client-info">
<div>{{ request.client_business_name }}</div>
<small>{{ request.client_document_number }}</small>
</div>
</td>
<td>{{ request.seller_full_name }}</td>
<td>{{ request.request_value | currency }}</td>
<td>{{ request.approved_value | currency }}</td>
<td>
<span [class]="'status ' + request.document_state?.toLowerCase()">
{{ getStatusLabel(request.document_state) }}
</span>
</td>
<td>{{ getTypeLabel(request.type) }}</td>
<td>{{ request.created_at | date:'dd/MM/yyyy' }}</td>
<td>
<button (click)="viewRequest(request._id)">Ver</button>
<button (click)="viewTimeline(request._id)">Timeline</button>
<button (click)="changeStatus(request._id)">Cambiar Estado</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination" *ngIf="totalPages > 1">
<button [disabled]="currentPage === 1" (click)="changePage(currentPage - 1)">
Anterior
</button>
<span>Página {{ currentPage }} de {{ totalPages }}</span>
<button [disabled]="currentPage === totalPages" (click)="changePage(currentPage + 1)">
Siguiente
</button>
</div>
</div>
`,
styles: [`
.credit-request-list { padding: 20px; }
.filters { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
.table-container { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
.client-info { line-height: 1.2; }
.client-info small { color: #666; }
.status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.status.pending { background: #fff3cd; color: #856404; }
.status.approved { background: #d4edda; color: #155724; }
.status.rejected { background: #f8d7da; color: #721c24; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 20px; }
`]
})
export class CreditRequestListComponent implements OnInit {
requests: CbmCreditRequestModel.ListResponse.Item[] = [];
currentPage = 1;
pageSize = 10;
totalPages = 0;
filters: Partial<CbmCreditRequestModel.ListParams> = {
document_number: '',
client_document_number: '',
document_state: '',
type: '',
date_begin: undefined,
date_end: undefined
};
constructor(private creditRequestService: CbmCreditRequestService) {}
ngOnInit() {
this.loadRequests();
}
loadRequests() {
// Convertir fechas a timestamps
const params: CbmCreditRequestModel.ListParams = {
page: this.currentPage,
size: this.pageSize,
...this.filters
};
if (this.filters.date_begin) {
params.date_begin = new Date(this.filters.date_begin).getTime();
}
if (this.filters.date_end) {
params.date_end = new Date(this.filters.date_end).getTime();
}
this.creditRequestService.list(params).subscribe({
next: (response) => {
this.requests = response.items;
this.totalPages = response.pages;
},
error: (error) => {
console.error('Error al cargar solicitudes:', error);
}
});
}
changePage(page: number) {
this.currentPage = page;
this.loadRequests();
}
getStatusLabel(state?: string): string {
const labels: { [key: string]: string } = {
'PENDING': 'Pendiente',
'APPROVED': 'Aprobado',
'REJECTED': 'Rechazado',
'IN_REVIEW': 'En Revisión'
};
return labels[state || ''] || state || 'Desconocido';
}
getTypeLabel(type?: string): string {
const labels: { [key: string]: string } = {
'INCREASE': 'Aumento',
'DECREASE': 'Disminución',
'NEW': 'Nueva Solicitud',
'REVOCATION': 'Revocación'
};
return labels[type || ''] || type || 'Desconocido';
}
viewRequest(id: string) {
this.creditRequestService.getOne(id).subscribe({
next: (request) => {
console.log('Solicitud:', request);
// Mostrar modal o navegar a detalle
}
});
}
viewTimeline(id: string) {
this.creditRequestService.timeline(id).subscribe({
next: (timeline) => {
console.log('Timeline:', timeline);
// Mostrar timeline en modal
}
});
}
changeStatus(id: string) {
// Implementar cambio de estado
console.log('Cambiar estado de solicitud:', id);
}
}Ejemplo con Formulario de Nueva Solicitud
import { ReactiveFormsModule } from '@angular/forms';
import { CbmCreditRequestService } from '@cbm-common/credit-request-repository';
@Component({
selector: 'app-credit-request-form',
standalone: true,
imports: [ReactiveFormsModule, CbmCreditRequestService],
template: `
<form [formGroup]="requestForm" (ngSubmit)="onSubmit()">
<h2>Nueva Solicitud de Crédito</h2>
<div class="form-section">
<h3>Información del Cliente</h3>
<div class="form-row">
<div class="form-group">
<label for="client">Cliente</label>
<select id="client" formControlName="client_id">
<option *ngFor="let client of clients" [value]="client.id">
{{ client.business_name }} - {{ client.document_number }}
</option>
</select>
</div>
<div class="form-group">
<label for="clientBranch">Sucursal del Cliente</label>
<select id="clientBranch" formControlName="client_branch_id">
<option *ngFor="let branch of clientBranches" [value]="branch.id">
{{ branch.name }} - {{ branch.code }}
</option>
</select>
</div>
</div>
</div>
<div class="form-section">
<h3>Información de la Solicitud</h3>
<div class="form-row">
<div class="form-group">
<label for="type">Tipo de Solicitud</label>
<select id="type" formControlName="type">
<option value="NEW">Nueva Solicitud</option>
<option value="INCREASE">Aumento de Crédito</option>
<option value="DECREASE">Disminución de Crédito</option>
<option value="REVOCATION">Revocación</option>
</select>
</div>
<div class="form-group">
<label for="requestValue">Valor Solicitado</label>
<input id="requestValue" type="number" formControlName="request_value" step="0.01">
</div>
</div>
<div class="form-group">
<label for="reason">Razón de la Solicitud</label>
<textarea id="reason" formControlName="reason" rows="3" placeholder="Explique detalladamente la razón de esta solicitud"></textarea>
</div>
</div>
<div class="form-section">
<h3>Valores Adicionales</h3>
<div class="form-row">
<div class="form-group">
<label for="increaseValue">Valor de Aumento</label>
<input id="increaseValue" type="number" formControlName="increase_value" step="0.01">
</div>
<div class="form-group">
<label for="decreaseValue">Valor de Disminución</label>
<input id="decreaseValue" type="number" formControlName="decrease_value" step="0.01">
</div>
</div>
</div>
<div class="form-section">
<h3>Información del Vendedor</h3>
<div class="form-group">
<label for="seller">Vendedor Asignado</label>
<select id="seller" formControlName="seller_id">
<option *ngFor="let seller of sellers" [value]="seller.id">
{{ seller.full_name }} - {{ seller.identification_number }}
</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="button" (click)="onCancel()">Cancelar</button>
<button type="submit" [disabled]="requestForm.invalid">Crear Solicitud</button>
</div>
</form>
`,
styles: [`
form { max-width: 800px; margin: 0 auto; }
.form-section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
.form-section h3 { margin-top: 0; color: #333; }
.form-row { display: flex; gap: 15px; }
.form-group { margin-bottom: 15px; flex: 1; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.form-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
button[type="submit"] { background: #007bff; color: white; }
button[type="button"] { background: #6c757d; color: white; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
`]
})
export class CreditRequestFormComponent implements OnInit {
requestForm!: FormGroup;
clients: any[] = [];
clientBranches: any[] = [];
sellers: any[] = [];
constructor(
private fb: FormBuilder,
private creditRequestService: CbmCreditRequestService
) {}
ngOnInit() {
this.initForm();
this.loadReferenceData();
}
initForm() {
this.requestForm = this.fb.group({
client_id: ['', Validators.required],
client_branch_id: ['', Validators.required],
type: ['NEW', Validators.required],
request_value: ['', [Validators.required, Validators.min(0)]],
reason: ['', [Validators.required, Validators.minLength(10)]],
increase_value: [0, Validators.min(0)],
decrease_value: [0, Validators.min(0)],
seller_id: ['', Validators.required]
});
}
loadReferenceData() {
// Cargar datos de referencia
this.clients = [
{ id: '1', business_name: 'Cliente Ejemplo S.A.', document_number: '1790012345001' },
{ id: '2', business_name: 'Empresa ABC Cía. Ltda.', document_number: '1790023456001' }
];
this.clientBranches = [
{ id: '1', name: 'Sucursal Principal', code: '001' },
{ id: '2', name: 'Sucursal Norte', code: '002' }
];
this.sellers = [
{ id: '1', full_name: 'Juan Pérez', identification_number: '1700000001' },
{ id: '2', full_name: 'María García', identification_number: '1700000002' }
];
}
onSubmit() {
if (this.requestForm.valid) {
const formData = this.requestForm.value;
this.creditRequestService.save(formData).subscribe({
next: (response) => {
console.log('Solicitud creada:', response);
this.requestForm.reset();
this.requestForm.patchValue({ type: 'NEW' });
},
error: (error) => {
console.error('Error al crear solicitud:', error);
}
});
}
}
onCancel() {
this.requestForm.reset();
this.requestForm.patchValue({ type: 'NEW' });
}
}Ejemplo con Gestión de Estados y Timeline
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-credit-request-detail',
standalone: true,
template: `
<div class="request-detail">
<div class="header">
<h2>Solicitud #{{ request?.document_number }}</h2>
<span [class]="'status ' + request?.document_state?.toLowerCase()">
{{ getStatusLabel(request?.document_state) }}
</span>
</div>
<div class="content">
<div class="info-section">
<h3>Información General</h3>
<div class="info-grid">
<div class="info-item">
<label>Cliente:</label>
<span>{{ request?.client_business_name }}</span>
</div>
<div class="info-item">
<label>Documento:</label>
<span>{{ request?.client_document_number }}</span>
</div>
<div class="info-item">
<label>Valor Solicitado:</label>
<span>{{ request?.request_value | currency }}</span>
</div>
<div class="info-item">
<label>Valor Aprobado:</label>
<span>{{ request?.approved_value | currency }}</span>
</div>
<div class="info-item">
<label>Vendedor:</label>
<span>{{ request?.seller_full_name }}</span>
</div>
<div class="info-item">
<label>Fecha de Creación:</label>
<span>{{ request?.created_at | date:'dd/MM/yyyy HH:mm' }}</span>
</div>
</div>
</div>
<div class="timeline-section">
<h3>Timeline de Cambios</h3>
<div class="timeline">
<div *ngFor="let event of timeline" class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-content">
<div class="timeline-header">
<span class="timeline-state">{{ getStatusLabel(event.document_state) }}</span>
<span class="timeline-date">{{ event.date | date:'dd/MM/yyyy HH:mm' }}</span>
</div>
<div class="timeline-body">
<p><strong>Usuario:</strong> {{ event.user }}</p>
<p><strong>Valor Solicitado:</strong> {{ event.request_value | currency }}</p>
<p><strong>Valor Aprobado:</strong> {{ event.approved_value | currency }}</p>
<p *ngIf="event.commentary"><strong>Comentario:</strong> {{ event.commentary }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="actions-section" *ngIf="canChangeStatus()">
<h3>Cambiar Estado</h3>
<form [formGroup]="statusForm" (ngSubmit)="onChangeStatus()">
<div class="form-group">
<label for="newState">Nuevo Estado</label>
<select id="newState" formControlName="document_state">
<option value="APPROVED">Aprobar</option>
<option value="REJECTED">Rechazar</option>
<option value="IN_REVIEW">En Revisión</option>
</select>
</div>
<div class="form-group">
<label for="approvedValue">Valor Aprobado</label>
<input id="approvedValue" type="number" formControlName="approved_value" step="0.01">
</div>
<div class="form-group">
<label for="commentary">Comentario</label>
<textarea id="commentary" formControlName="commentary" rows="3"></textarea>
</div>
<div class="form-actions">
<button type="submit" [disabled]="statusForm.invalid">Cambiar Estado</button>
<button type="button" (click)="sendNotification()">Enviar Notificación</button>
</div>
</form>
</div>
</div>
</div>
`,
styles: [`
.request-detail { padding: 20px; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
.status { padding: 8px 16px; border-radius: 20px; font-weight: bold; }
.status.pending { background: #fff3cd; color: #856404; }
.status.approved { background: #d4edda; color: #155724; }
.status.rejected { background: #f8d7da; color: #721c24; }
.status.in_review { background: #cce7ff; color: #004085; }
.content { display: grid; gap: 30px; }
.info-section, .timeline-section, .actions-section { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; }
.info-item { display: flex; flex-direction: column; }
.info-item label { font-weight: bold; margin-bottom: 5px; color: #666; }
.info-item span { font-size: 16px; }
.timeline { position: relative; }
.timeline-item { display: flex; margin-bottom: 20px; }
.timeline-marker { width: 12px; height: 12px; background: #007bff; border-radius: 50%; margin-right: 15px; margin-top: 6px; }
.timeline-content { flex: 1; }
.timeline-header { display: flex; justify-content: space-between; margin-bottom: 10px; }
.timeline-state { font-weight: bold; color: #007bff; }
.timeline-date { color: #666; font-size: 14px; }
.timeline-body p { margin: 5px 0; font-size: 14px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
input, select, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.form-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
button[type="submit"] { background: #28a745; color: white; }
button[type="button"] { background: #17a2b8; color: white; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
`]
})
export class CreditRequestDetailComponent implements OnInit {
request: CbmCreditRequestModel.GetOneResponse.Data | null = null;
timeline: CbmCreditRequestModel.TimelineResponse[] = [];
statusForm!: FormGroup;
constructor(
private fb: FormBuilder,
private creditRequestService: CbmCreditRequestService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.initStatusForm();
const id = this.route.snapshot.params['id'];
this.loadRequest(id);
this.loadTimeline(id);
}
initStatusForm() {
this.statusForm = this.fb.group({
document_state: ['', Validators.required],
approved_value: [0, Validators.min(0)],
commentary: ['']
});
}
loadRequest(id: string) {
this.creditRequestService.getOne(id).subscribe({
next: (response) => {
this.request = response.data;
},
error: (error) => {
console.error('Error al cargar solicitud:', error);
}
});
}
loadTimeline(id: string) {
this.creditRequestService.timeline(id).subscribe({
next: (response) => {
this.timeline = response;
},
error: (error) => {
console.error('Error al cargar timeline:', error);
}
});
}
canChangeStatus(): boolean {
return this.request?.document_state === 'PENDING' || this.request?.document_state === 'IN_REVIEW';
}
getStatusLabel(state?: string): string {
const labels: { [key: string]: string } = {
'PENDING': 'Pendiente',
'APPROVED': 'Aprobado',
'REJECTED': 'Rechazado',
'IN_REVIEW': 'En Revisión'
};
return labels[state || ''] || state || 'Desconocido';
}
onChangeStatus() {
if (this.statusForm.valid && this.request) {
const statusData = this.statusForm.value;
this.creditRequestService.changeStatus(this.request._id!, statusData).subscribe({
next: (response) => {
console.log('Estado cambiado:', response);
// Recargar datos
this.loadRequest(this.request!._id!);
this.loadTimeline(this.request!._id!);
this.statusForm.reset();
},
error: (error) => {
console.error('Error al cambiar estado:', error);
}
});
}
}
sendNotification() {
if (this.request) {
const emailParams: CbmCreditRequestModel.SendEmailParams = {
timezone: 'America/Guayaquil',
locale: 'es_EC',
emails: this.request.client_email
};
this.creditRequestService.sendEmail(this.request._id!, emailParams).subscribe({
next: (response) => {
console.log('Email enviado:', response);
},
error: (error) => {
console.error('Error al enviar email:', error);
}
});
}
}
}⚠️ Manejo de Errores
Errores de API
// Manejo de errores en operaciones de solicitudes de crédito
saveCreditRequest(requestData: CbmCreditRequestModel.SaveBody) {
this.creditRequestService.save(requestData).subscribe({
next: (response) => {
if (response.success) {
console.log('Solicitud creada exitosamente');
this.showSuccessMessage('Solicitud de crédito creada correctamente');
} else {
console.error('Error en la respuesta:', response);
this.showErrorMessage('Error al crear la solicitud de crédito');
}
},
error: (error) => {
console.error('Error HTTP:', error);
if (error.status === 400) {
this.showErrorMessage('Datos inválidos. Verifique la información del cliente y valores solicitados.');
} else if (error.status === 401) {
this.showErrorMessage('Sesión expirada. Inicie sesión nuevamente.');
} else if (error.status === 403) {
this.showErrorMessage('No tiene permisos para gestionar solicitudes de crédito.');
} else if (error.status === 422) {
this.showErrorMessage('Error de validación. Verifique los límites de crédito y documentación.');
} else {
this.showErrorMessage('Error interno del servidor.');
}
}
});
}Validación de Datos de Crédito
// Validaciones personalizadas para solicitudes de crédito
export class CreditRequestValidators {
static validRequestValue(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
return value > 0 ? null : { invalidRequestValue: true };
}
static validReasonLength(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
return value.trim().length >= 10 ? null : { invalidReasonLength: true };
}
static validApprovedValue(form: FormGroup): ValidationErrors | null {
const requestValue = form.get('request_value')?.value;
const approvedValue = form.get('approved_value')?.value;
if (!requestValue || !approvedValue) return null;
return approvedValue <= requestValue ? null : { invalidApprovedValue: true };
}
static validClientSelection(form: FormGroup): ValidationErrors | null {
const clientId = form.get('client_id')?.value;
const clientBranchId = form.get('client_branch_id')?.value;
if (!clientId || !clientBranchId) return null;
// Aquí podrías agregar validación adicional para verificar
// que la sucursal pertenece al cliente seleccionado
return null;
}
}
// Uso en formularios reactivos
this.requestForm = this.fb.group({
client_id: ['', Validators.required],
client_branch_id: ['', Validators.required],
request_value: ['', [Validators.required, CreditRequestValidators.validRequestValue]],
reason: ['', [Validators.required, CreditRequestValidators.validReasonLength]],
approved_value: [0, Validators.min(0)]
}, {
validators: [
CreditRequestValidators.validApprovedValue,
CreditRequestValidators.validClientSelection
]
});🔧 Configuración Avanzada
Configuración de Headers para Operaciones de Crédito
// Configuración de headers para operaciones de solicitudes de crédito seguras
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@NgModule({
providers: [
CbmCreditRequestModule.forRoot({
baseUrl: 'https://api.cbm.com/credit-requests'
}),
{
provide: HTTP_INTERCEPTORS,
useClass: CreditRequestSecurityInterceptor,
multi: true
}
]
})
export class AppModule {}Configuración de Timeouts para Operaciones Complejas
// Timeouts específicos para operaciones de cambio de estado complejas
changeStatusWithTimeout(id: string, statusData: CbmCreditRequestModel.ChangeStatusBody) {
return this.creditRequestService.changeStatus(id, statusData).pipe(
timeout(30000), // 30 segundos para operaciones de aprobación complejas
catchError(error => {
if (error.name === 'TimeoutError') {
return throwError(() => new Error('La operación de cambio de estado tomó demasiado tiempo'));
}
return throwError(() => error);
})
);
}Sistema de Reintentos para Operaciones Críticas
// Reintentos automáticos para operaciones críticas de crédito
saveCriticalCreditRequest(requestData: CbmCreditRequestModel.SaveBody, maxRetries = 3) {
return this.creditRequestService.save(requestData).pipe(
retry({
count: maxRetries,
delay: (error, retryCount) => {
console.log(`Reintento ${retryCount} para solicitud de crédito`);
return timer(retryCount * 2000);
}
}),
catchError(error => {
console.error(`Falló después de ${maxRetries} intentos:`, error);
return throwError(() => error);
})
);
}📋 Dependencias
Peer Dependencies (Requeridas)
{
"@angular/common": ">=20.1.5",
"@angular/core": ">=20.1.5"
}Dependencias Internas
{
"tslib": "^2.3.0"
}🛠️ Desarrollo
Estructura del Proyecto
credit-request-repository/
├── src/
│ ├── lib/
│ │ ├── credit-request.model.ts # Modelos de datos de solicitudes de crédito
│ │ ├── credit-request.module.ts # Configuración del módulo
│ │ ├── credit-request.service.ts # Servicio HTTP
│ │ ├── credit-request.repository.ts # Interfaz del repositorio
│ │ └── index.ts # Exportaciones
│ └── public-api.ts # API pública
├── ng-package.json # Configuración empaquetado
├── package.json # Dependencias
└── README.md # Esta documentaciónConstrucción
# Construir la librería
ng build credit-request-repository
# Construir en modo watch
ng build credit-request-repository --watch
# Construir para producción
ng build credit-request-repository --configuration productionPruebas
# Ejecutar pruebas unitarias
ng test credit-request-repository
# Ejecutar pruebas con coverage
ng test credit-request-repository --code-coverage
# Pruebas end-to-end
ng e2e credit-request-repository🎯 Mejores Prácticas
1. Gestión de Memoria y Recursos
// Limpiar suscripciones para evitar memory leaks
export class CreditRequestListComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
loadRequests() {
this.creditRequestService.list(params).pipe(
takeUntil(this.destroy$)
).subscribe({
next: (response) => {
this.requests = response.items;
}
});
}
}2. Optimización de Rendimiento con Grandes Volúmenes
// Usar OnPush change detection para mejor rendimiento
@Component({
selector: 'app-credit-request-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`
})
export class CreditRequestListComponent {
requests$ = this.creditRequestService.list(params).pipe(
shareReplay(1) // Compartir respuesta entre múltiples suscriptores
);
}3. Validación Completa de Solicitudes de Crédito
// Servicio de validación para solicitudes de crédito
@Injectable({ providedIn: 'root' })
export class CreditValidationService {
validateCreditRequest(request: any): string[] {
const errors: string[] = [];
if (!request.client_id) {
errors.push('Cliente es requerido');
}
if (!request.client_branch_id) {
errors.push('Sucursal del cliente es requerida');
}
if (request.request_value <= 0) {
errors.push('Valor solicitado debe ser mayor a cero');
}
if (!request.reason || request.reason.trim().length < 10) {
errors.push('La razón debe tener al menos 10 caracteres');
}
if (!request.seller_id) {
errors.push('Vendedor es requerido');
}
return errors;
}
validateBusinessRules(request: any, clientData: any): string[] {
const errors: string[] = [];
// Regla: El valor solicitado no puede exceder 3 veces el límite actual
if (request.request_value > (clientData.credit_limit * 3)) {
errors.push('Valor solicitado excede 3 veces el límite de crédito actual');
}
// Regla: Validar que el cliente no tenga solicitudes pendientes
if (clientData.has_pending_requests) {
errors.push('El cliente tiene solicitudes pendientes de aprobación');
}
return errors;
}
validateStatusChange(currentState: string, newState: string, userRole: string): boolean {
const allowedTransitions: { [key: string]: string[] } = {
'PENDING': ['APPROVED', 'REJECTED', 'IN_REVIEW'],
'IN_REVIEW': ['APPROVED', 'REJECTED'],
'APPROVED': [], // No se puede cambiar una vez aprobado
'REJECTED': ['PENDING'] // Se puede reenviar
};
return allowedTransitions[currentState]?.includes(newState) || false;
}
}4. Logging y Auditoría para Operaciones de Crédito
// Servicio de auditoría para operaciones de solicitudes de crédito críticas
@Injectable({ providedIn: 'root' })
export class CreditAuditService {
logCreditOperation(operation: string, requestData: any, userId: string) {
const auditEntry = {
timestamp: new Date().toISOString(),
operation,
requestId: requestData._id || 'NEW',
userId,
clientId: requestData.client_id,
clientDocument: requestData.client_document_number,
requestValue: requestData.request_value,
approvedValue: requestData.approved_value,
sellerId: requestData.seller_id,
documentState: requestData.document_state,
reason: requestData.reason,
type: requestData.type
};
console.log(`[CREDIT_AUDIT] ${JSON.stringify(auditEntry)}`);
// Enviar a servicio de auditoría financiera
this.financialAuditService.record('credit_request', auditEntry);
}
logStatusChange(requestId: string, oldState: string, newState: string, userId: string, commentary?: string) {
const statusChangeEntry = {
timestamp: new Date().toISOString(),
operation: 'STATUS_CHANGE',
requestId,
userId,
oldState,
newState,
commentary: commentary || '',
severity: newState === 'APPROVED' ? 'HIGH' : 'MEDIUM'
};
console.log(`[CREDIT_STATUS_CHANGE] ${JSON.stringify(statusChangeEntry)}`);
// Enviar a servicio de auditoría de cambios críticos
this.criticalAuditService.record('credit_status_change', statusChangeEntry);
}
logError(operation: string, error: any, requestData: any) {
const errorEntry = {
timestamp: new Date().toISOString(),
operation,
error: error.message,
status: error.status,
requestData,
severity: 'HIGH' // Los errores de crédito son críticos
};
console.error(`[CREDIT_ERROR] ${JSON.stringify(errorEntry)}`);
// Enviar a servicio de monitoreo de errores críticos
this.criticalErrorMonitoring.captureException(error, {
tags: { operation, module: 'credit-request', severity: 'high' },
extra: { requestData }
});
}
}🤝 Contribución
- Fork el repositorio
- Crea una rama para tu feature (
git checkout -b feature/nueva-funcionalidad) - Commit tus cambios (
git commit -am 'Agrega nueva funcionalidad') - Push a la rama (
git push origin feature/nueva-funcionalidad) - Abre un PullRequest
📄 Licencia
Este proyecto está bajo la Licencia MIT - ver el archivo LICENSE para más detalles.
📞 Soporte
Para soporte técnico o reportes de bugs, contacta al equipo de desarrollo de CBM:
- 📧 Email: [email protected]
- 💬 Slack: #desarrollo-cbm
- 📋 Issues: GitHub Issues
🔄 Changelog
v0.0.1
- ✅ Módulo de configuración con forRoot pattern
- ✅ Servicio que implementa interfaz del repositorio
- ✅ Operaciones CRUD completas (list, getOne, save)
- ✅ Sistema de timeline para seguimiento de cambios
- ✅ Cambio de estado con aprobaciones/rechazos
- ✅ Validación de operaciones de cliente
- ✅ Envío de notificaciones por email
- ✅ Modelos TypeScript bien tipados con datos de crédito
- ✅ Integración completa con HttpClient
- ✅ Documentación completa en español
Nota: Esta librería está optimizada para sistemas financieros que requieren gestión completa de solicitudes de crédito con validaciones de límites, seguimiento de estados, auditoría de operaciones críticas y notificaciones automáticas. Incluye reglas de negocio específicas para el manejo de riesgos crediticios.
