npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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ón

Construcció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 production

Pruebas

# 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

  1. Fork el repositorio
  2. Crea una rama para tu feature (git checkout -b feature/nueva-funcionalidad)
  3. Commit tus cambios (git commit -am 'Agrega nueva funcionalidad')
  4. Push a la rama (git push origin feature/nueva-funcionalidad)
  5. 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:

🔄 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.