@cbm-common/bank-movements-repository
v0.0.1
Published
Biblioteca Angular para la gestión de movimientos bancarios en el sistema CBM. Implementa el patrón Repository para proporcionar una capa de abstracción sobre las operaciones bancarias.
Readme
Bank Movements Repository
Biblioteca Angular para la gestión de movimientos bancarios en el sistema CBM. Implementa el patrón Repository para proporcionar una capa de abstracción sobre las operaciones bancarias.
📦 Instalación
npm install @cbm-common/bank-movements-repository⚙️ Configuración
Módulo Principal (NgModules)
Importa el módulo en tu aplicación principal y configura la URL base de la API:
import { CbmBankMovementsModule } from '@cbm-common/bank-movements-repository';
@NgModule({
imports: [
// ... otros módulos
CbmBankMovementsModule.forRoot({
baseUrl: 'https://api.cbm.com/v1'
})
],
// ... otras configuraciones
})
export class AppModule { }Aplicaciones Standalone
Para aplicaciones que utilizan el nuevo sistema standalone de Angular, configura el provider directamente:
import { bootstrapApplication } from '@angular/platform-browser';
import { BANK_MOVEMENTS_MODULE_CONFIG } from '@cbm-common/bank-movements-repository';
bootstrapApplication(AppComponent, {
providers: [
// ... otros providers
{
provide: BANK_MOVEMENTS_MODULE_CONFIG,
useValue: {
baseUrl: 'https://api.cbm.com/v1'
}
}
]
});🏗️ Arquitectura
La biblioteca sigue el patrón Repository para separar la lógica de negocio de la implementación técnica:
CbmBankMovementsRepository (Interfaz + Implementación)
↓
CbmBankMovementsService (Lógica HTTP)
↓
HttpClient (Comunicación con API)Componentes Principales
- ICbmBankMovementsRepository: Interfaz que define el contrato del repositorio
- CbmBankMovementsRepository: Implementación concreta del repositorio
- CbmBankMovementsService: Servicio que maneja las llamadas HTTP
- CbmBankMovementsModule: Módulo de configuración
- CbmBankMovementsModel: Modelos de datos y tipos TypeScript
🚀 Uso Básico
Inyección de Dependencias
import { Component, inject } from '@angular/core';
import { CbmBankMovementsRepository } from '@cbm-common/bank-movements-repository';
@Component({
selector: 'app-bank-movements',
template: `
<div class="movements-container">
<h2>Movimientos Bancarios</h2>
<button (click)="loadMovements()">Cargar Movimientos</button>
<div *ngIf="loading">Cargando...</div>
<div *ngFor="let movement of movements" class="movement-item">
<span>{{ movement.operation_number }}</span>
<span>{{ movement.value | currency }}</span>
<span>{{ movement.date | date }}</span>
</div>
</div>
`
})
export class BankMovementsComponent {
private repository = inject(CbmBankMovementsRepository);
movements: CbmBankMovementsModel.ListResponse.Item[] = [];
loading = false;
loadMovements() {
this.loading = true;
const params: CbmBankMovementsModel.ListParams = {
page: 1,
size: 20,
bank_id: 'bank123',
date_begin: new Date('2024-01-01').getTime(),
date_end: new Date('2024-12-31').getTime()
};
this.repository.list(params).subscribe({
next: (response) => {
this.movements = response.items;
this.loading = false;
},
error: (error) => {
console.error('Error al cargar movimientos:', error);
this.loading = false;
}
});
}
}Validación de Operaciones
import { Component, inject } from '@angular/core';
import { CbmBankMovementsRepository } from '@cbm-common/bank-movements-repository';
@Component({
selector: 'app-operation-validator',
template: `
<form (ngSubmit)="validateOperation()" #form="ngForm">
<input [(ngModel)]="operationNumber" name="operation" placeholder="Número de operación" required>
<input [(ngModel)]="bankId" name="bank" placeholder="ID del banco" required>
<button type="submit" [disabled]="!form.valid">Validar</button>
</form>
<div *ngIf="validationResult">
<h3>Resultado de Validación</h3>
<p>Origen: {{ validationResult.data?.collection_origin_name }}</p>
<p>Módulo: {{ validationResult.data?.event_module }}</p>
</div>
`
})
export class OperationValidatorComponent {
private repository = inject(CbmBankMovementsRepository);
operationNumber = '';
bankId = '';
validationResult: CbmBankMovementsModel.ValidationOperationResponse | null = null;
validateOperation() {
const params: CbmBankMovementsModel.ValidationOperationParams = {
operation_number: this.operationNumber,
bank_id: this.bankId
};
this.repository.validationOperation(params).subscribe({
next: (response) => {
this.validationResult = response;
},
error: (error) => {
console.error('Error en validación:', error);
}
});
}
}📋 API Reference
ICbmBankMovementsRepository
Interfaz principal que define las operaciones disponibles:
interface ICbmBankMovementsRepository {
list(params: CbmBankMovementsModel.ListParams): Observable<CbmBankMovementsModel.ListResponse>;
validationOperation(params: CbmBankMovementsModel.ValidationOperationParams): Observable<CbmBankMovementsModel.ValidationOperationResponse>;
}CbmBankMovementsService
Servicio que implementa la lógica HTTP:
@Injectable({ providedIn: 'root' })
export class CbmBankMovementsService {
constructor(
private http: HttpClient,
@Inject(BANK_MOVEMENTS_MODULE_CONFIG) private config: ICbmBankMovementsModuleConfig
) {}
list(params: CbmBankMovementsModel.ListParams): Observable<CbmBankMovementsModel.ListResponse>;
validationOperation(params: CbmBankMovementsModel.ValidationOperationParams): Observable<CbmBankMovementsModel.ValidationOperationResponse>;
}📊 Modelos de Datos
ListParams
Parámetros para consultar la lista de movimientos:
interface ListParams {
page: number; // Número de página (requerido)
size: number; // Tamaño de página (requerido)
reverse_status?: boolean; // Estado de reversión
operation_number?: string; // Número de operación
payment_term_id?: string; // ID del término de pago
bank_id?: string; // ID del banco
date_end?: number; // Fecha fin (timestamp)
date_begin?: number; // Fecha inicio (timestamp)
}ListResponse
Respuesta de la consulta de movimientos:
interface ListResponse {
success: boolean;
pageNum: number;
pageSize: number;
pages: number;
total: number;
items: ListResponse.Item[];
}
interface ListResponse.Item {
_id?: string;
company_id?: string;
company_branch_id?: string;
bank_id?: string;
bank_name?: string;
payment_term_id?: string;
payment_term_code?: string;
payment_term_name?: string;
collection_origin_id?: string;
collection_origin_name?: string;
operation_number?: string;
date?: number;
value?: number;
type?: string;
created_user?: string;
reverse_status?: boolean;
created_at?: number;
reverse_at?: number;
reverse_reason?: string;
reverse_user?: string;
}ValidationOperationParams
Parámetros para validar una operación:
interface ValidationOperationParams {
operation_number: string; // Número de operación (requerido)
bank_id: string; // ID del banco (requerido)
}ValidationOperationResponse
Respuesta de la validación:
interface ValidationOperationResponse {
success?: boolean;
data?: {
collection_origin_id: string;
collection_origin_name: string;
event_module: string;
};
message?: string;
}🔧 Ejemplos Avanzados
Con Reactive Forms
import { Component, inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CbmBankMovementsRepository } from '@cbm-common/bank-movements-repository';
@Component({
selector: 'app-movements-filter',
template: `
<form [formGroup]="filterForm" (ngSubmit)="applyFilters()">
<div class="form-row">
<input formControlName="bank_id" placeholder="ID del banco">
<input formControlName="operation_number" placeholder="Número de operación">
</div>
<div class="form-row">
<input formControlName="date_begin" type="date">
<input formControlName="date_end" type="date">
</div>
<div class="form-row">
<select formControlName="reverse_status">
<option [value]="null">Todos</option>
<option [value]="true">Revertidos</option>
<option [value]="false">No revertidos</option>
</select>
</div>
<button type="submit" [disabled]="filterForm.invalid">Aplicar Filtros</button>
</form>
`
})
export class MovementsFilterComponent {
private fb = inject(FormBuilder);
private repository = inject(CbmBankMovementsRepository);
filterForm: FormGroup = this.fb.group({
bank_id: [''],
operation_number: [''],
date_begin: [''],
date_end: [''],
reverse_status: [null]
});
applyFilters() {
const filters = this.filterForm.value;
// Convertir fechas a timestamps
if (filters.date_begin) {
filters.date_begin = new Date(filters.date_begin).getTime();
}
if (filters.date_end) {
filters.date_end = new Date(filters.date_end).getTime();
}
const params: CbmBankMovementsModel.ListParams = {
page: 1,
size: 20,
...filters
};
this.repository.list(params).subscribe({
next: (response) => {
console.log('Movimientos filtrados:', response.items);
}
});
}
}Con Signals (Angular 17+)
import { Component, inject, signal, computed } from '@angular/core';
import { CbmBankMovementsRepository } from '@cbm-common/bank-movements-repository';
@Component({
selector: 'app-movements-signals',
template: `
<div class="movements-dashboard">
<h2>Dashboard de Movimientos</h2>
<div class="stats">
<div class="stat">
<span class="label">Total Movimientos:</span>
<span class="value">{{ totalMovements() }}</span>
</div>
<div class="stat">
<span class="label">Valor Total:</span>
<span class="value">{{ totalValue() | currency }}</span>
</div>
</div>
<button (click)="loadMovements()" [disabled]="loading()">
{{ loading() ? 'Cargando...' : 'Cargar Movimientos' }}
</button>
<div *ngIf="error()" class="error">
Error: {{ error() }}
</div>
<div class="movements-list">
<div *ngFor="let movement of movements()" class="movement-card">
<h3>{{ movement.operation_number }}</h3>
<p>Banco: {{ movement.bank_name }}</p>
<p>Valor: {{ movement.value | currency }}</p>
<p>Fecha: {{ movement.date | date:'short' }}</p>
<p *ngIf="movement.reverse_status" class="reversed">
⚠️ Movimiento revertido
</p>
</div>
</div>
</div>
`
})
export class MovementsSignalsComponent {
private repository = inject(CbmBankMovementsRepository);
// Signals
movements = signal<CbmBankMovementsModel.ListResponse.Item[]>([]);
loading = signal(false);
error = signal<string | null>(null);
// Computed signals
totalMovements = computed(() => this.movements().length);
totalValue = computed(() =>
this.movements().reduce((sum, movement) => sum + (movement.value || 0), 0)
);
loadMovements() {
this.loading.set(true);
this.error.set(null);
const params: CbmBankMovementsModel.ListParams = {
page: 1,
size: 50
};
this.repository.list(params).subscribe({
next: (response) => {
this.movements.set(response.items);
this.loading.set(false);
},
error: (err) => {
this.error.set(err.message || 'Error desconocido');
this.loading.set(false);
}
});
}
}Paginación Personalizada
import { Component, inject } from '@angular/core';
import { CbmBankMovementsRepository } from '@cbm-common/bank-movements-repository';
@Component({
selector: 'app-paginated-movements',
template: `
<div class="pagination-controls">
<button (click)="previousPage()" [disabled]="currentPage === 1">
← Anterior
</button>
<span>Página {{ currentPage }} de {{ totalPages }}</span>
<button (click)="nextPage()" [disabled]="currentPage === totalPages">
Siguiente →
</button>
</div>
<div class="page-size-selector">
<label>Elementos por página:</label>
<select [(ngModel)]="pageSize" (change)="changePageSize()">
<option [value]="10">10</option>
<option [value]="20">20</option>
<option [value]="50">50</option>
</select>
</div>
`
})
export class PaginatedMovementsComponent {
private repository = inject(CbmBankMovementsRepository);
currentPage = 1;
pageSize = 20;
totalPages = 1;
totalItems = 0;
movements: CbmBankMovementsModel.ListResponse.Item[] = [];
loadPage(page: number = 1) {
const params: CbmBankMovementsModel.ListParams = {
page,
size: this.pageSize
};
this.repository.list(params).subscribe({
next: (response) => {
this.movements = response.items;
this.currentPage = response.pageNum;
this.totalPages = response.pages;
this.totalItems = response.total;
}
});
}
previousPage() {
if (this.currentPage > 1) {
this.loadPage(this.currentPage - 1);
}
}
nextPage() {
if (this.currentPage < this.totalPages) {
this.loadPage(this.currentPage + 1);
}
}
changePageSize() {
this.currentPage = 1;
this.loadPage(1);
}
}⚠️ Manejo de Errores
La biblioteca utiliza HttpClient de Angular, por lo que los errores HTTP se manejan de forma estándar:
import { HttpErrorResponse } from '@angular/common/http';
this.repository.list(params).subscribe({
next: (response) => {
// Operación exitosa
console.log('Movimientos cargados:', response.items);
},
error: (error: HttpErrorResponse) => {
switch (error.status) {
case 400:
console.error('Parámetros inválidos:', error.error);
break;
case 401:
console.error('No autorizado - revisar token de autenticación');
// Redirigir a login
break;
case 403:
console.error('Acceso denegado');
break;
case 404:
console.error('Recurso no encontrado');
break;
case 500:
console.error('Error interno del servidor');
break;
default:
console.error('Error desconocido:', error.message);
}
}
});Estrategias de Reintento
import { retry, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
this.repository.list(params).pipe(
retry({
count: 3,
delay: (error, retryCount) => {
console.log(`Reintento ${retryCount} después del error:`, error);
return retryCount * 1000; // Delay incremental
}
}),
catchError((error) => {
console.error('Error después de reintentos:', error);
return throwError(() => error);
})
).subscribe({
next: (response) => {
// Manejar respuesta exitosa
},
error: (error) => {
// Manejar error final
}
});🔄 Integración con RxJS
Operadores Avanzados
import { map, filter, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({...})
export class AdvancedMovementsComponent {
private searchSubject = new Subject<string>();
private repository = inject(CbmBankMovementsRepository);
movements$ = this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(searchTerm => {
const params: CbmBankMovementsModel.ListParams = {
page: 1,
size: 20,
operation_number: searchTerm
};
return this.repository.list(params);
}),
map(response => response.items),
catchError(error => {
console.error('Error en búsqueda:', error);
return [];
})
);
onSearchChange(searchTerm: string) {
this.searchSubject.next(searchTerm);
}
}Caching con ShareReplay
import { shareReplay, refCount } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CachedBankMovementsService {
private cache$?: Observable<CbmBankMovementsModel.ListResponse>;
constructor(private repository: CbmBankMovementsRepository) {}
getMovements(params: CbmBankMovementsModel.ListParams) {
if (!this.cache$) {
this.cache$ = this.repository.list(params).pipe(
shareReplay(1, 300000) // Cache por 5 minutos
);
}
return this.cache$;
}
clearCache() {
this.cache$ = undefined;
}
}📋 Dependencias
Peer Dependencies (Requeridas)
{
"@angular/core": ">=20.1.5",
"@angular/common": ">=20.1.5",
"rxjs": ">=7.0.0"
}Dependencias Internas
{
"tslib": "^2.3.0"
}🛠️ Desarrollo
Estructura del Proyecto
bank-movements-repository/
├── src/
│ ├── lib/
│ │ ├── bank-movements.model.ts # Modelos de datos
│ │ ├── bank-movements.module.ts # Configuración del módulo
│ │ ├── bank-movements.repository.ts # Implementación del repositorio
│ │ ├── bank-movements.service.ts # Servicio HTTP
│ │ └── index.ts # Exportaciones públicas
│ └── public-api.ts # API pública
├── ng-package.json # Configuración de empaquetado
├── package.json # Dependencias y configuración
└── README.md # Esta documentaciónConstrucción
# Construir la librería
ng build bank-movements-repository
# Construir en modo watch
ng build bank-movements-repository --watch
# Construir para producción
ng build bank-movements-repository --configuration productionPruebas
# Ejecutar pruebas unitarias
ng test bank-movements-repository
# Ejecutar pruebas con coverage
ng test bank-movements-repository --code-coverage
# Ejecutar pruebas en modo watch
ng test bank-movements-repository --watchPublicación
# Construir para distribución
ng build bank-movements-repository --configuration production
# Navegar al directorio de distribución
cd dist/bank-movements-repository
# Publicar en npm
npm publish🎯 Mejores Prácticas
1. Configuración Centralizada
// config.ts
export const BANK_MOVEMENTS_CONFIG = {
baseUrl: environment.apiUrl,
timeout: 30000,
retries: 3
};
// app.module.ts
CbmBankMovementsModule.forRoot(BANK_MOVEMENTS_CONFIG)2. Manejo de Estado Global
import { Store } from '@ngrx/store';
@Injectable({ providedIn: 'root' })
export class BankMovementsFacade {
movements$ = this.store.select(selectMovements);
loading$ = this.store.select(selectMovementsLoading);
constructor(
private store: Store,
private repository: CbmBankMovementsRepository
) {}
loadMovements(params: CbmBankMovementsModel.ListParams) {
this.store.dispatch(loadMovements({ params }));
}
}3. Interceptores HTTP Personalizados
@Injectable()
export class BankMovementsInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.includes('/bank-movements')) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${this.getToken()}`)
});
return next.handle(authReq);
}
return next.handle(req);
}
}🤝 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 Pull Request
📄 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
- ✅ Implementación inicial del repositorio de movimientos bancarios
- ✅ Soporte para consultas paginadas con filtros
- ✅ Validación de operaciones bancarias
- ✅ Patrón Repository con interfaz
- ✅ Configuración flexible via InjectionToken
- ✅ Documentación completa en español
Nota: Esta biblioteca está diseñada para trabajar con la API de CBM v1. Asegúrate de que tu servidor backend sea compatible con los endpoints documentados.
Running end-to-end tests
For end-to-end (e2e) testing, run:
ng e2eAngular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the Angular CLI Overview and Command Reference page.
