@cbm-common/incoterm-repository
v0.0.1
Published
Biblioteca Angular especializada en la gestión de Incoterms (International Commercial Terms). Proporciona una API completa para administrar términos comerciales internacionales que definen las responsabilidades de compradores y vendedores en transacciones
Downloads
9
Readme
Incoterm Repository
Biblioteca Angular especializada en la gestión de Incoterms (International Commercial Terms). Proporciona una API completa para administrar términos comerciales internacionales que definen las responsabilidades de compradores y vendedores en transacciones comerciales.
📋 Características Principales
- ✅ Gestión de Incoterms: Administración completa de términos comerciales internacionales
- ✅ Códigos Estandarizados: Soporte para códigos Incoterm oficiales (EXW, FOB, CIF, DDP, etc.)
- ✅ Control de Estados: Habilitar/deshabilitar Incoterms según necesidades del negocio
- ✅ Búsqueda Avanzada: Filtrado por código, nombre y estado
- ✅ Arquitectura Moderna: Basada en Angular 20.1.5 con inyección de dependencias
- ✅ Patrón Repository: Separación clara entre lógica de negocio y acceso a datos
- ✅ TypeScript Tipado: Interfaces completamente tipadas para mejor desarrollo
🚀 Instalación
npm install @cbm-common/incoterm-repository⚙️ Configuración
1. Importar el Módulo
import { CbmIncotermModule } from '@cbm-common/incoterm-repository';
@NgModule({
imports: [
CbmIncotermModule.forRoot({
baseUrl: 'https://api.cbm.com/incoterms'
})
]
})
export class AppModule { }2. Configuración Standalone (Angular 20+)
import { CbmIncotermModule } from '@cbm-common/incoterm-repository';
@Component({
standalone: true,
imports: [CbmIncotermModule.forRoot({
baseUrl: 'https://api.cbm.com/incoterms'
})]
})
export class AppComponent { }📚 API Reference
Interfaz del Repositorio
interface ICbmIncotermRepository {
// Listado paginado con filtros
list(params: CbmIncotermModel.ListParams): Observable<CbmIncotermModel.ListResponse>;
}💡 Uso Básico
Inyección del Servicio
import { CbmIncotermRepository } from '@cbm-common/incoterm-repository';
@Component({
selector: 'app-incoterm-manager'
})
export class IncotermManagerComponent {
incoterms: CbmIncotermModel.ListResponse.Data[] = [];
constructor(private incotermRepository: CbmIncotermRepository) {
this.loadIncoterms();
}
loadIncoterms() {
const params: CbmIncotermModel.ListParams = {
enabled: true,
name: 'FOB'
};
this.incotermRepository.list(params).subscribe({
next: (response) => {
if (response.success) {
this.incoterms = response.data;
console.log('Incoterms cargados:', this.incoterms);
}
},
error: (error) => {
console.error('Error al cargar incoterms:', error);
}
});
}
}Filtrado por Código
searchByCode() {
const params: CbmIncotermModel.ListParams = {
code: 'CIF',
enabled: true
};
this.incotermRepository.list(params).subscribe({
next: (response) => {
if (response.success && response.data.length > 0) {
const cifIncoterm = response.data[0];
console.log('Incoterm CIF encontrado:', cifIncoterm);
console.log('Nombre:', cifIncoterm.name);
console.log('Código:', cifIncoterm.code);
}
},
error: (error) => console.error('Incoterm no encontrado:', error)
});
}Filtrado por Nombre
searchByName() {
const params: CbmIncotermModel.ListParams = {
name: 'Free',
enabled: true
};
this.incotermRepository.list(params).subscribe({
next: (response) => {
if (response.success) {
console.log('Incoterms que contienen "Free":', response.data);
response.data.forEach(incoterm => {
console.log(`${incoterm.code}: ${incoterm.name}`);
});
}
},
error: (error) => console.error('Error en búsqueda:', error)
});
}📊 Modelos de Datos
Parámetros de Búsqueda
interface ListParams {
enabled?: boolean; // Filtrar por estado activo/inactivo
code?: string; // Filtrar por código Incoterm
name?: string; // Filtrar por nombre (búsqueda parcial)
}Respuesta de Listado
interface ListResponse {
success: boolean; // Indicador de éxito de la operación
data: ListResponse.Data[]; // Array de incoterms
}
namespace ListResponse {
interface Data {
_id: string; // ID único del incoterm
code?: string; // Código Incoterm (ej: EXW, FOB, CIF)
name?: string; // Nombre completo del incoterm
enabled?: boolean; // Estado activo/inactivo
created_user?: string; // Usuario que creó el registro
created_at?: number; // Fecha de creación (timestamp)
updated_user?: string; // Usuario que actualizó el registro
updated_at?: number; // Fecha de actualización (timestamp)
}
}Ejemplo de Datos
{
"success": true,
"data": [
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j1",
"code": "FOB",
"name": "Free On Board",
"enabled": true,
"created_user": "admin",
"created_at": 1694000000000,
"updated_user": "admin",
"updated_at": 1694100000000
},
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j2",
"code": "CIF",
"name": "Cost, Insurance and Freight",
"enabled": true,
"created_user": "admin",
"created_at": 1694000000000
},
{
"_id": "64f1a2b3c4d5e6f7g8h9i0j3",
"code": "DDP",
"name": "Delivered Duty Paid",
"enabled": false,
"created_user": "admin",
"created_at": 1694000000000,
"updated_user": "admin",
"updated_at": 1694200000000
}
]
}🔍 Casos de Uso Comunes
1. Selector de Incoterms en Formularios
@Component({
selector: 'app-incoterm-selector'
})
export class IncotermSelectorComponent {
incoterms: CbmIncotermModel.ListResponse.Data[] = [];
selectedIncoterm: string = '';
constructor(private incotermRepository: CbmIncotermRepository) {
this.loadActiveIncoterms();
}
loadActiveIncoterms() {
const params: CbmIncotermModel.ListParams = {
enabled: true
};
this.incotermRepository.list(params).subscribe({
next: (response) => {
if (response.success) {
this.incoterms = response.data.sort((a, b) =>
(a.code || '').localeCompare(b.code || '')
);
}
},
error: (error) => {
console.error('Error al cargar incoterms:', error);
}
});
}
onIncotermChange(incotermId: string) {
const selected = this.incoterms.find(inc => inc._id === incotermId);
if (selected) {
console.log('Incoterm seleccionado:', selected.code, '-', selected.name);
// Aquí puedes emitir el evento o actualizar el formulario
}
}
getIncotermDisplayName(incoterm: CbmIncotermModel.ListResponse.Data): string {
return `${incoterm.code} - ${incoterm.name}`;
}
}2. Gestión de Incoterms en Configuración
@Component({
selector: 'app-incoterm-settings'
})
export class IncotermSettingsComponent {
allIncoterms: CbmIncotermModel.ListResponse.Data[] = [];
activeIncoterms: CbmIncotermModel.ListResponse.Data[] = [];
inactiveIncoterms: CbmIncotermModel.ListResponse.Data[] = [];
constructor(private incotermRepository: CbmIncotermRepository) {
this.loadAllIncoterms();
}
loadAllIncoterms() {
// Cargar todos los incoterms
this.incotermRepository.list({}).subscribe({
next: (response) => {
if (response.success) {
this.allIncoterms = response.data;
this.categorizeIncoterms();
}
}
});
}
private categorizeIncoterms() {
this.activeIncoterms = this.allIncoterms.filter(inc => inc.enabled);
this.inactiveIncoterms = this.allIncoterms.filter(inc => !inc.enabled);
}
getStats() {
return {
total: this.allIncoterms.length,
active: this.activeIncoterms.length,
inactive: this.inactiveIncoterms.length,
activePercentage: Math.round((this.activeIncoterms.length / this.allIncoterms.length) * 100)
};
}
searchIncoterms(searchTerm: string) {
if (!searchTerm.trim()) {
this.categorizeIncoterms();
return;
}
const filtered = this.allIncoterms.filter(inc =>
inc.code?.toLowerCase().includes(searchTerm.toLowerCase()) ||
inc.name?.toLowerCase().includes(searchTerm.toLowerCase())
);
this.activeIncoterms = filtered.filter(inc => inc.enabled);
this.inactiveIncoterms = filtered.filter(inc => !inc.enabled);
}
}3. Dashboard de Incoterms
@Component({
selector: 'app-incoterm-dashboard'
})
export class IncotermDashboardComponent {
incoterms: CbmIncotermModel.ListResponse.Data[] = [];
stats = {
totalIncoterms: 0,
activeIncoterms: 0,
inactiveIncoterms: 0,
recentlyUpdated: 0
};
constructor(private incotermRepository: CbmIncotermRepository) {
this.loadDashboardData();
}
loadDashboardData() {
this.incotermRepository.list({}).subscribe({
next: (response) => {
if (response.success) {
this.incoterms = response.data;
this.calculateStats();
}
},
error: (error) => {
console.error('Error al cargar datos del dashboard:', error);
}
});
}
private calculateStats() {
this.stats.totalIncoterms = this.incoterms.length;
this.stats.activeIncoterms = this.incoterms.filter(inc => inc.enabled).length;
this.stats.inactiveIncoterms = this.incoterms.filter(inc => !inc.enabled).length;
// Contar incoterms actualizados en las últimas 24 horas
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
this.stats.recentlyUpdated = this.incoterms.filter(inc =>
inc.updated_at && inc.updated_at > oneDayAgo
).length;
}
getIncotermsByStatus(enabled: boolean): CbmIncotermModel.ListResponse.Data[] {
return this.incoterms.filter(inc => inc.enabled === enabled);
}
getMostUsedIncoterms(): CbmIncotermModel.ListResponse.Data[] {
// En un caso real, esto vendría de estadísticas de uso
return this.incoterms.slice(0, 5);
}
}4. Validador de Incoterms
@Injectable({ providedIn: 'root' })
export class IncotermValidatorService {
private validCodes = ['EXW', 'FCA', 'CPT', 'CIP', 'DAT', 'DAP', 'DDP', 'FAS', 'FOB', 'CFR', 'CIF'];
constructor(private incotermRepository: CbmIncotermRepository) {}
validateIncotermCode(code: string): boolean {
return this.validCodes.includes(code.toUpperCase());
}
validateIncotermExists(code: string): Observable<boolean> {
const params: CbmIncotermModel.ListParams = {
code: code.toUpperCase(),
enabled: true
};
return this.incotermRepository.list(params).pipe(
map(response => response.success && response.data.length > 0),
catchError(() => of(false))
);
}
getIncotermByCode(code: string): Observable<CbmIncotermModel.ListResponse.Data | null> {
const params: CbmIncotermModel.ListParams = {
code: code.toUpperCase(),
enabled: true
};
return this.incotermRepository.list(params).pipe(
map(response => {
if (response.success && response.data.length > 0) {
return response.data[0];
}
return null;
}),
catchError(() => of(null))
);
}
getValidationErrors(code: string): string[] {
const errors: string[] = [];
if (!code || !code.trim()) {
errors.push('El código del incoterm es obligatorio');
} else if (!this.validateIncotermCode(code)) {
errors.push(`El código "${code}" no es un código Incoterm válido`);
}
return errors;
}
}🏗️ Arquitectura y Patrones
Patrón Repository
La biblioteca implementa el patrón Repository para mantener la separación entre la lógica de negocio y el acceso a datos:
// Interfaz del repositorio
export interface ICbmIncotermRepository {
list(params: CbmIncotermModel.ListParams): Observable<CbmIncotermModel.ListResponse>;
}
// Implementación del servicio
@Injectable({ providedIn: 'root' })
export class CbmIncotermService implements ICbmIncotermRepository {
constructor(
private readonly http: HttpClient,
@Inject(INCOTERM_MODULE_CONFIG)
private readonly config: ICbmIncotermModuleConfig
) {}
list(params: CbmIncotermModel.ListParams): Observable<CbmIncotermModel.ListResponse> {
return this.http.get<CbmIncotermModel.ListResponse>(this.config.baseUrl, { params: { ...params } });
}
}
// Wrapper del repositorio
@Injectable({ providedIn: 'root' })
export class CbmIncotermRepository implements ICbmIncotermRepository {
constructor(private service: CbmIncotermService) {}
list(params: CbmIncotermModel.ListParams): Observable<CbmIncotermModel.ListResponse> {
return this.service.list(params);
}
}Configuración Centralizada
// Configuración del módulo
@NgModule({
imports: [
CbmIncotermModule.forRoot({
baseUrl: environment.apiUrl + '/incoterms'
})
]
})
export class AppModule {}🔧 Mejores Prácticas
Manejo de Errores
@Component({
selector: 'app-safe-incoterm-handler'
})
export class SafeIncotermHandlerComponent {
incoterms: CbmIncotermModel.ListResponse.Data[] = [];
loading = false;
error: string | null = null;
constructor(private incotermRepository: CbmIncotermRepository) {}
safeLoadIncoterms() {
this.loading = true;
this.error = null;
const params: CbmIncotermModel.ListParams = {
enabled: true
};
this.incotermRepository.list(params).subscribe({
next: (response) => {
this.loading = false;
if (response.success && response.data) {
this.incoterms = response.data;
} else {
this.error = 'Respuesta inválida del servidor';
}
},
error: (error) => {
this.loading = false;
if (error.status === 404) {
this.error = 'Servicio de incoterms no disponible';
} else if (error.status === 500) {
this.error = 'Error interno del servidor';
} else {
this.error = 'Error de conexión';
}
}
});
}
}Caché de Incoterms
@Injectable({ providedIn: 'root' })
export class IncotermCacheService {
private cache = new Map<string, CbmIncotermModel.ListResponse.Data[]>();
private readonly CACHE_DURATION = 30 * 60 * 1000; // 30 minutos
constructor(private incotermRepository: CbmIncotermRepository) {}
getIncoterms(params: CbmIncotermModel.ListParams = {}, forceRefresh = false): Observable<CbmIncotermModel.ListResponse.Data[]> {
const cacheKey = JSON.stringify(params);
const cached = this.cache.get(cacheKey);
if (cached && !forceRefresh && !this.isCacheExpired(cached)) {
return of(cached);
}
return this.incotermRepository.list(params).pipe(
map(response => {
if (response.success) {
this.cache.set(cacheKey, response.data);
return response.data;
}
throw new Error('Error al obtener incoterms');
})
);
}
private isCacheExpired(incoterms: CbmIncotermModel.ListResponse.Data[]): boolean {
if (incoterms.length === 0) return true;
const mostRecent = Math.max(...incoterms.map(inc => inc.updated_at || inc.created_at || 0));
return Date.now() - mostRecent > this.CACHE_DURATION;
}
invalidateCache() {
this.cache.clear();
}
// Método específico para obtener incoterms activos
getActiveIncoterms(): Observable<CbmIncotermModel.ListResponse.Data[]> {
return this.getIncoterms({ enabled: true });
}
// Método específico para buscar por código
getIncotermByCode(code: string): Observable<CbmIncotermModel.ListResponse.Data | null> {
return this.getIncoterms({ code, enabled: true }).pipe(
map(incoterms => incoterms.find(inc => inc.code === code) || null)
);
}
}Servicio de Utilidades para Incoterms
@Injectable({ providedIn: 'root' })
export class IncotermUtilsService {
// Códigos Incoterm estándar con sus descripciones
private readonly INCOTERM_DEFINITIONS = {
'EXW': 'Ex Works - El vendedor entrega la mercancía en sus instalaciones',
'FCA': 'Free Carrier - El vendedor entrega al transportista designado',
'CPT': 'Carriage Paid To - El vendedor paga el transporte hasta el destino',
'CIP': 'Carriage and Insurance Paid To - Transporte y seguro pagados por el vendedor',
'DAT': 'Delivered at Terminal - Entregado en terminal designada',
'DAP': 'Delivered at Place - Entregado en lugar convenido',
'DDP': 'Delivered Duty Paid - Entregado con derechos pagados',
'FAS': 'Free Alongside Ship - Libre al costado del buque',
'FOB': 'Free On Board - Libre a bordo',
'CFR': 'Cost and Freight - Costo y flete',
'CIF': 'Cost, Insurance and Freight - Costo, seguro y flete'
};
getIncotermDescription(code: string): string {
return this.INCOTERM_DEFINITIONS[code.toUpperCase()] || `Incoterm ${code} - Descripción no disponible`;
}
getAllIncotermCodes(): string[] {
return Object.keys(this.INCOTERM_DEFINITIONS);
}
isValidIncotermCode(code: string): boolean {
return code.toUpperCase() in this.INCOTERM_DEFINITIONS;
}
formatIncotermDisplay(incoterm: CbmIncotermModel.ListResponse.Data): string {
if (incoterm.code && incoterm.name) {
return `${incoterm.code} - ${incoterm.name}`;
}
return incoterm.name || incoterm.code || 'Incoterm sin nombre';
}
groupIncotermsByCategory(incoterms: CbmIncotermModel.ListResponse.Data[]): Map<string, CbmIncotermModel.ListResponse.Data[]> {
const groups = new Map<string, CbmIncotermModel.ListResponse.Data[]>();
incoterms.forEach(incoterm => {
const category = this.getIncotermCategory(incoterm.code || '');
if (!groups.has(category)) {
groups.set(category, []);
}
groups.get(category)!.push(incoterm);
});
return groups;
}
private getIncotermCategory(code: string): string {
const firstLetter = code.charAt(0).toUpperCase();
const categories = {
'E': 'Salida',
'F': 'Transporte Principal no Pagado',
'C': 'Transporte Principal Pagado',
'D': 'Llegada'
};
return categories[firstLetter] || 'Sin Categoría';
}
}📋 Lista de Verificación para Implementación
- [ ] ✅ Configurar el módulo
CbmIncotermModule.forRoot()en el AppModule - [ ] ✅ Inyectar
CbmIncotermRepositoryen los componentes que lo necesiten - [ ] ✅ Implementar manejo de errores para todas las operaciones
- [ ] ✅ Validar códigos Incoterm antes de usarlos
- [ ] ✅ Implementar indicadores de carga durante las operaciones
- [ ] ✅ Manejar estados de error y respuestas no exitosas
- [ ] ✅ Considerar implementar caché para incoterms (cambian con poca frecuencia)
- [ ] ✅ Probar todas las operaciones de búsqueda y filtrado
- [ ] ✅ Verificar funcionamiento con diferentes códigos Incoterm
- [ ] ✅ Documentar casos de uso específicos de la aplicación
🔗 Dependencias
{
"peerDependencies": {
"@angular/common": "20.1.5",
"@angular/core": "20.1.5"
},
"dependencies": {
"tslib": "2.3.0"
}
}📝 Notas de Versión
v0.0.1
- ✅ Implementación inicial del repositorio de incoterms
- ✅ Soporte completo para listado con filtros avanzados
- ✅ Gestión de códigos y nombres de incoterms
- ✅ Control de estados activo/inactivo
- ✅ Arquitectura basada en patrón Repository
- ✅ Configuración simplificada con
forRoot() - ✅ Compatibilidad con Angular 20.1.5
- ✅ Interfaces TypeScript completamente tipadas
- ✅ Documentación completa en español
- ✅ Ejemplos prácticos de implementación
- ✅ Servicios de utilidad para validación y formato
- ✅ Soporte para caché de datos
- ✅ Manejo robusto de errores
🤝 Contribución
Para contribuir a esta biblioteca:
- Fork el repositorio
- Crear una rama para la nueva funcionalidad
- Implementar los cambios siguiendo los patrones establecidos
- Agregar pruebas unitarias si es necesario
- Enviar un Pull Request con descripción detallada
📞 Soporte
Para soporte técnico o consultas sobre el uso de la biblioteca, contactar al equipo de desarrollo de CBM.
CBM (Contabilidad y Facturación Moderna) - Repositorio de Incoterms v0.0.1
