diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py index a54f8cf7..a98f78eb 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py @@ -9,6 +9,7 @@ from sqlalchemy.exc import IntegrityError from fastapi import HTTPException import zlib import logging +import re from .models import TariffFraction from .dto import TariffFractionCreateDTO, TariffFractionUpdateDTO @@ -22,7 +23,42 @@ logger = logging.getLogger(__name__) class TariffFractionMapper: """Helper to map Sitar responses to Local domain objects""" - + + @staticmethod + def _digits_only(value: Optional[str]) -> str: + return re.sub(r"\D", "", (value or "").strip()) + + @staticmethod + def _format_mx_fraction(code: str) -> str: + if code.isdigit() and len(code) == 8: + return f"{code[:2]}.{code[2:4]}.{code[4:6]}.{code[6:]}" + if code.isdigit() and len(code) == 6: + return f"{code[:2]}.{code[2:4]}.{code[4:]}" + return code + + @staticmethod + def _format_usa_fraction(code: str) -> str: + if code.isdigit() and len(code) == 10: + return f"{code[:4]}.{code[4:6]}.{code[6:8]}.{code[8:]}" + if code.isdigit() and len(code) == 8: + return f"{code[:4]}.{code[4:6]}.{code[6:]}" + return code + + @staticmethod + def _normalized_pair(raw_code: Optional[str], raw_fraction: Optional[str], formatter) -> Tuple[str, str]: + """Return (code_without_separators, formatted_fraction).""" + code = TariffFractionMapper._digits_only(raw_code) + fraction = (raw_fraction or "").strip() + if not code: + code = TariffFractionMapper._digits_only(fraction) + if not fraction: + fraction = formatter(code) + elif "." not in fraction and "-" not in fraction: + fraction = formatter(TariffFractionMapper._digits_only(fraction)) + if not fraction: + fraction = formatter(code) + return code, fraction + @staticmethod def to_domain(fraccion: FraccionesResponse) -> TariffFraction: # Generate ID: Use SYSID if available, else composite hash of code + nico @@ -33,22 +69,9 @@ class TariffFractionMapper: unique_str = f"{fraccion.FRACCION}-{fraccion.NICO}" fake_id = zlib.crc32(unique_str.encode('utf-8')) - # UX Enhauncement: Sitar API returns empty strings for some fields. - # We fill them with fallbacks so the frontend table isn't 90% empty. - code_val = fraccion.FRACCION - - # Formatting Logic: if FRACCIONPUNTO is empty, try to format code_val - formatted_fraction = code_val - if fraccion.FRACCIONPUNTO: - formatted_fraction = fraccion.FRACCIONPUNTO - elif code_val and code_val.isdigit() and len(code_val) == 8: - # Standard 8 digit format: XX.XX.XX.XX - formatted_fraction = f"{code_val[:2]}.{code_val[2:4]}.{code_val[4:6]}.{code_val[6:]}" - elif code_val and code_val.isdigit() and len(code_val) == 6: - # 6 digit (subheading): XX.XX.XX - formatted_fraction = f"{code_val[:2]}.{code_val[2:4]}.{code_val[4:]}" - - fraction_val = formatted_fraction + code_val, fraction_val = TariffFractionMapper._normalized_pair( + fraccion.FRACCION, fraccion.FRACCIONPUNTO, TariffFractionMapper._format_mx_fraction + ) description_val = fraccion.DESCRIPCION if fraccion.DESCRIPCION else "(Sin descripción)" tf = TariffFraction( @@ -73,10 +96,15 @@ class TariffFractionMapper: @staticmethod def to_domain_usa(item: FraccionesUSAResponse) -> TariffFraction: """Map US Fraction to Domain""" + code_val, fraction_val = TariffFractionMapper._normalized_pair( + item.FRACCION_SIN_PUNTO, + item.FRACCION_CON_PUNTO or item.FRACCION_MOSTRAR, + TariffFractionMapper._format_usa_fraction, + ) return TariffFraction( id=item.CONSECUTIVO, - code=item.FRACCION_SIN_PUNTO or "", - fraction=item.FRACCION_CON_PUNTO or "", + code=code_val, + fraction=fraction_val, description=item.DESCRIPCION or "(Sin descripción)", nico=None, # Not applicable umt=item.UNIDADCANTIDAD, @@ -183,28 +211,27 @@ class TariffFractionService: # Map filters sitar_fraccion = None sitar_nico = None + sitar_description = None - # Default level logic - level_filter = 5 # Default legacy + # Legacy parity: base query is always Nivel = 5 unless caller explicitly requests another level. + level_filter = 5 if filters and filters.get("level") is not None: level_filter = filters["level"] - - # Allow disabling level filter explicitly + # UI compatibility: level -1 means "sin filtro de nivel". if level_filter == -1: level_filter = None if filters: if filters.get("search"): - term = filters["search"] - # Heuristic: if search starts with digit (after removing dots), treat as code/fraccion/nico - # This covers "0101", "01.01", "020691A" + term = str(filters["search"]).strip() + # Legacy-like behavior: + # - Numeric search targets fracción first. + # - Text search targets descripción. clean_term = term.replace(".", "") - if clean_term and clean_term[0].isdigit(): + if clean_term.isdigit(): sitar_fraccion = clean_term else: - # Attempt description search via API first - logger.info(f"Search term '{term}' identified as text. Attempting API description search.") - pass + sitar_description = term if filters.get("code"): sitar_fraccion = filters["code"] @@ -212,14 +239,8 @@ class TariffFractionService: sitar_fraccion = filters["fraction"] if filters.get("nico"): sitar_nico = filters["nico"] - - # Determine description filter - sitar_description = None - # Only use description if we didn't use it as code above - if filters and filters.get("search"): - clean_term = filters["search"].replace(".", "") - if not (clean_term and clean_term[0].isdigit()): - sitar_description = filters["search"] + if sitar_fraccion is not None: + sitar_fraccion = str(sitar_fraccion).replace(".", "").strip() # Note: Sitar search might not return total count. # We fetch page items. Pagination might be tricky if Sitar doesn't return total. @@ -239,6 +260,8 @@ class TariffFractionService: # Map items items = [TariffFractionMapper.to_domain(item) for item in sitar_items] + # Legacy browse behavior: keep table in ascending fracción order. + items = sorted(items, key=lambda row: ((row.code or ""), (row.nico or ""))) # Estimate total (Sitar service doesn't return total currently) # If we got full limit, assume there are more. @@ -289,8 +312,6 @@ class TariffFractionService: query = query.filter(TariffFraction.umt.ilike(f"%{filters['umt']}%")) total = query.count() - # Add deterministic sort order - query = query.order_by(TariffFraction.fraction) items = query.offset(skip).limit(limit).all() return items, total diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py index c316dc08..b30d2d59 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py @@ -6,6 +6,7 @@ from datetime import datetime from typing import Optional, Any from pydantic import BaseModel, Field, ConfigDict, model_validator +import re class USTariffFractionCreateDTO(BaseModel): @@ -62,10 +63,20 @@ class USTariffFractionResponseDTO(BaseModel): if raw_code: code_str = str(raw_code) - # fraction keeps the original formatted string - fraction = code_str - # code strips dots and hyphens - code = code_str.replace(".", "").replace("-", "") + fraction_raw = "" + if isinstance(data, dict): + fraction_raw = str(data.get("fraction") or "") + else: + fraction_raw = str(getattr(data, "fraction", "") or "") + + code = re.sub(r"[.\s-]", "", code_str) + fraction = fraction_raw.strip() or code_str + if "." not in fraction and "-" not in fraction: + only_digits = re.sub(r"[.\s-]", "", fraction) + if len(only_digits) == 10: + fraction = f"{only_digits[:4]}.{only_digits[4:6]}.{only_digits[6:8]}.{only_digits[8:]}" + elif len(only_digits) == 8: + fraction = f"{only_digits[:4]}.{only_digits[4:6]}.{only_digits[6:]}" if isinstance(data, dict): data["code"] = code diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index e07bf0fd..a1cee9f5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -28,6 +28,7 @@ def _sitar_row_to_us_response_payload(item: FraccionesUSAResponse) -> dict: return { "id": item.CONSECUTIVO, "code": canon, + "fraction": item.FRACCION_CON_PUNTO or item.FRACCION_MOSTRAR or canon, "prefix": item.FRACCION_SIN_PUNTO, "type_code": str(item.NIVEL) if item.NIVEL is not None else None, "ad_valorem": american_fraction_ad_valorem_from_row(item), diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py index 661bf01b..e5f8cfbf 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py @@ -128,7 +128,7 @@ class USTariffFractionService: ) total = query.count() - items = query.order_by(USTariffFraction.code).offset(skip).limit(limit).all() + items = query.offset(skip).limit(limit).all() return items, total diff --git a/backend/tests/unit/general_catalogs/fractions/test_tariff_fraction_mapper.py b/backend/tests/unit/general_catalogs/fractions/test_tariff_fraction_mapper.py new file mode 100644 index 00000000..06e61c8c --- /dev/null +++ b/backend/tests/unit/general_catalogs/fractions/test_tariff_fraction_mapper.py @@ -0,0 +1,57 @@ +from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.service import ( + TariffFractionMapper, +) +from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.dto import ( + USTariffFractionResponseDTO, +) +from api.v1.modules.sitar.fracciones_usa.schemas import FraccionesUSAResponse + + +def test_to_domain_usa_keeps_separate_code_and_fraction(): + row = FraccionesUSAResponse( + CONSECUTIVO=10, + FRACCION_SIN_PUNTO="1234567890", + FRACCION_CON_PUNTO="1234.56.78.90", + DESCRIPCION="Test", + UNIDADCANTIDAD="KG", + TARIFA1="5%", + TARIFA2="0%", + ) + + mapped = TariffFractionMapper.to_domain_usa(row) + + assert mapped.code == "1234567890" + assert mapped.fraction == "1234.56.78.90" + + +def test_to_domain_usa_formats_fraction_when_only_code_available(): + row = FraccionesUSAResponse( + CONSECUTIVO=11, + FRACCION_SIN_PUNTO="9876543210", + FRACCION_CON_PUNTO=None, + FRACCION_MOSTRAR=None, + DESCRIPCION="Fallback", + UNIDADCANTIDAD="PZA", + TARIFA1="7.5%", + TARIFA2="0%", + ) + + mapped = TariffFractionMapper.to_domain_usa(row) + + assert mapped.code == "9876543210" + assert mapped.fraction == "9876.54.32.10" + + +def test_us_response_dto_preserves_fraction_when_provided(): + dto = USTariffFractionResponseDTO.model_validate( + { + "id": 1, + "code": "1111.22.33.44", + "fraction": "1111.22.33.44", + "description": "DTO test", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + } + ) + assert dto.code == "1111223344" + assert dto.fraction == "1111.22.33.44" diff --git a/frontend/src/lib/api/dashboard/a76/sitar.ts b/frontend/src/lib/api/dashboard/a76/sitar.ts index 829b1b42..9692ab5a 100644 --- a/frontend/src/lib/api/dashboard/a76/sitar.ts +++ b/frontend/src/lib/api/dashboard/a76/sitar.ts @@ -39,16 +39,81 @@ export interface SitarALADI { } export async function getSitarTLCS(filters: { fraccion: string; nico?: string }): Promise> { - const queryParams = new URLSearchParams(filters); - return await api.get(`/v1/sitar/tlcs/?${queryParams.toString()}`); + const queryParams = new URLSearchParams( + Object.entries(filters).reduce( + (acc, [key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { + acc[key] = String(value); + } + return acc; + }, + {} as Record + ) + ); + return await api.get(`/v1/sitar/tlcs/?${queryParams.toString()}`); } export async function getSitarPROSEC(filters: { fraccion: string; nico?: string }): Promise> { - const queryParams = new URLSearchParams(filters); - return await api.get(`/v1/sitar/prosec/?${queryParams.toString()}`); + const queryParams = new URLSearchParams( + Object.entries(filters).reduce( + (acc, [key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { + acc[key] = String(value); + } + return acc; + }, + {} as Record + ) + ); + return await api.get(`/v1/sitar/prosec/?${queryParams.toString()}`); } export async function getSitarALADI(filters: { fraccion: string; nico?: string }): Promise> { - const queryParams = new URLSearchParams(filters); - return await api.get(`/v1/sitar/aladi2/?${queryParams.toString()}`); + const queryParams = new URLSearchParams( + Object.entries(filters).reduce( + (acc, [key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { + acc[key] = String(value); + } + return acc; + }, + {} as Record + ) + ); + return await api.get(`/v1/sitar/aladi2/?${queryParams.toString()}`); +} + +export type SitarGenericRecord = Record; + +export type SitarDatasetEndpoint = + | 'reit' + | 'requisito-previo' + | 'informacion-general' + | 'regulaciones' + | 'fundamentos-tlc' + | 'cuotas2' + | 'cupos' + | 'noms' + | 'precios-estimados' + | 'ieps' + | 'rcg2' + | 'vehiculos-marcas' + | 'vehiculos-modelos'; + +export async function getSitarDataset( + endpoint: SitarDatasetEndpoint, + filters: { fraccion: string; nico?: string; [key: string]: string | undefined } +): Promise> { + const queryParams = new URLSearchParams( + Object.entries(filters).reduce( + (acc, [key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { + acc[key] = String(value); + } + return acc; + }, + {} as Record + ) + ); + return await api.get(`/v1/sitar/${endpoint}/?${queryParams.toString()}`); } diff --git a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte index dc78cf10..49305d14 100644 --- a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte @@ -1041,8 +1041,8 @@ - - + + @@ -1054,8 +1054,8 @@ class="cursor-pointer border-b transition-colors hover:bg-gray-100 dark:hover:bg-gray-700" onclick={() => selectUSFraction(fraction)} > - + diff --git a/frontend/src/lib/components/dashboard/goods/fractions/SitarFractionTabs.svelte b/frontend/src/lib/components/dashboard/goods/fractions/SitarFractionTabs.svelte new file mode 100644 index 00000000..af6fc266 --- /dev/null +++ b/frontend/src/lib/components/dashboard/goods/fractions/SitarFractionTabs.svelte @@ -0,0 +1,408 @@ + + +
+
+
+

+ Información Arancelaria (Solo Consulta) +

+
+
+
CódigoPrefijoClaveFracción Ad valorem Costo Fijo Descripción{fraction.fraction || fraction.code} {fraction.code || '—'}{fraction.fraction || '—'} {fraction.adv_impo ?? '—'} {fraction.adv_expo ?? '—'} {fraction.description || ''}
+ + + + + + + + + + + + + + + + + + +
FracciónUMTUMAdvalorem ImpoAdvalorem Expo
+ {headerFraction || '-'} + {selectedFraction?.umt || '-'}{selectedFraction?.um_code || '-'}{selectedFraction?.adv_impo || '-'}{selectedFraction?.adv_expo || '-'}
+ + + + {#if !hasSelection} +
+ Selecciona una fracción de la tabla para ver su detalle SITAR (Descripción, TLCS, PROSEC, ALADI). +
+ {:else} +
+ (activeTab = v as TabKey)} class="h-full w-full flex flex-col"> + + Descripción + TLCS + PROSEC + ALADI + IMMEX + ACUERDOS + + + {#if activeError} +
{activeError}
+ {/if} + + +
+

+ Descripción de la Fracción +

+
+ {selectedFraction?.description || 'No hay descripción disponible para esta fracción.'} +
+
+
+ + +
+
+

+ Información TLCS +

+
+
+ + + + + + + + + + + {#each sitarTLCSData as item} + + + + + + + {:else} + + {/each} + +
PaísTasaD.O.FNotas
{item.PAIS}{item.TASATXT}{item.DOF || '-'}{item.NOTA || '-'}
No hay información de TLCS disponible para esta fracción.
+
+
+
+ + +
+
+

+ Programa PROSEC +

+
+
+ + + + + + + + + + + {#each sitarPROSECData as item} + + + + + + + {:else} + + {/each} + +
ArtículoSectorTasa TxtD.O.F
{item.PRODUCTO}{item.SECTOR}{item.TASA}{item.DOF || '-'}
No hay información de PROSEC disponible para esta fracción.
+
+
+
+ + +
+
+

+ Acuerdo ALADI +

+
+
+ + + + + + + + + + + {#each sitarALADIData as item} + + + + + + + {:else} + + {/each} + +
AcuerdoPaísTasaD.O.F
{item.ACUERDO}{item.PAIS}{item.TASATXT}{item.DOF || '-'}
No hay información de ALADI disponible para esta fracción.
+
+
+
+ + +
+
+

+ IMMEX / REIT +

+
+
+ + + + + + + + + + + + {#each sitarIMMEXData as item} + + + + + + + + {:else} + + + + {/each} + +
ArtículoFundamentoAcuerdoPermisoD.O.F
{item.ARTICULO || ''}{item.FUNDAMENTO || ''}{item.ACUERDO || ''}{item.PERMISO || '-'}{item.DOF || '-'}
+ No hay información de IMMEX disponible para esta fracción. +
+
+
+
+ + +
+
+

+ Acuerdos / Requisitos previos +

+
+
+ + + + + + + + + + + {#each sitarAcuerdosData as item} + + + + + + + {:else} + + + + {/each} + +
DescripciónPermisoD.O.FVigencia
{item.DESCRIPCION || ''}{item.PERMISO || '-'}{item.DOF || '-'}{item.VIGENCIA || '-'}
+ No hay información de ACUERDOS disponible para esta fracción. +
+
+
+
+
+
+ {/if} + diff --git a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte index d4577e32..478f431d 100644 --- a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte +++ b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte @@ -20,22 +20,29 @@ import { untrack } from 'svelte'; import * as AlertDialog from '$lib/components/ui/alert-dialog'; import TariffFractionFormDialog from './TariffFractionFormDialog.svelte'; + import SitarFractionTabs from './SitarFractionTabs.svelte'; import { toast } from 'svelte-sonner'; import { currentUser, userHasPermission } from '$lib/auth'; import ErrorState from '$lib/components/dashboard/common/error-state.svelte'; + import { + getTariffFractionDisplayFraction, + getTariffFractionDisplayKey + } from '$lib/utils/tariff-fraction-display'; let { title = 'Fracciones Arancelarias', catalog = 'mex', // 'mex' or 'usa' levelFilter = null, // null or number readOnly = false, - basePerm: customBasePerm = null + basePerm: customBasePerm = null, + showSitarTabsOnSelect = false }: { title?: string; catalog?: string; levelFilter?: number | null; readOnly?: boolean; basePerm?: string | null; + showSitarTabsOnSelect?: boolean; } = $props(); let fractions = $state([]); @@ -74,6 +81,7 @@ let isFormDialogOpen = $state(false); let selectedFraction = $state(null); + let selectedDetailFraction = $state(null); let isManageMode = $state(false); // If true, opens form in edit mode // Delete confirmation @@ -110,6 +118,11 @@ } else { fractions = [...fractions, ...newItems]; } + if (selectedDetailFraction) { + selectedDetailFraction = + [...fractions, ...newItems].find((item) => item.id === selectedDetailFraction?.id) || + selectedDetailFraction; + } totalFractions = payload.total || 0; // Safer end-of-data detection @@ -171,6 +184,11 @@ isFormDialogOpen = true; } + function selectFractionDetail(fraction: TariffFraction) { + if (!showSitarTabsOnSelect || catalog !== 'mex') return; + selectedDetailFraction = fraction; + } + function confirmDelete(fraction: TariffFraction) { fractionToDelete = fraction; showDeleteConfirm = true; @@ -244,7 +262,8 @@ {/if} - +
+
@@ -289,9 +308,12 @@ {:else} {#each fractions as fraction (fraction.id)} - - {fraction.um_code || fraction.code} - {fraction.fraction} + selectFractionDetail(fraction)} + > + {getTariffFractionDisplayKey(fraction)} + {getTariffFractionDisplayFraction(fraction)} {fraction.description} @@ -346,6 +368,12 @@
+ {#if showSitarTabsOnSelect && catalog === 'mex'} +
+ +
+ {/if} +
Mostrando {fractions.length} de {totalFractions} registros
{/if} diff --git a/frontend/src/lib/components/dashboard/goods/modales/TariffFractionSelector.svelte b/frontend/src/lib/components/dashboard/goods/modales/TariffFractionSelector.svelte index b0461aa5..e23f5e9a 100644 --- a/frontend/src/lib/components/dashboard/goods/modales/TariffFractionSelector.svelte +++ b/frontend/src/lib/components/dashboard/goods/modales/TariffFractionSelector.svelte @@ -13,6 +13,7 @@ buildMexTariffDigitsFromCatalogRow, formatMexTariffDigitsForDisplay } from '$lib/utils/mexican-tariff-fraction'; + import { getTariffFractionDisplayKey } from '$lib/utils/tariff-fraction-display'; import { m } from '$lib/i18n/messages'; let { @@ -139,7 +140,7 @@ open = false; }} > - {fraction.um_code} + {getTariffFractionDisplayKey(fraction)} {formatMexTariffDigitsForDisplay( buildMexTariffDigitsFromCatalogRow(fraction) diff --git a/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte b/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte index 1cf8e8f8..206ab402 100644 --- a/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte @@ -9,6 +9,7 @@ getTariffFractions, type TariffFraction } from "$lib/api/dashboard/a76/general_catalogs/tariff-fractions"; + import { getTariffFractionDisplayKey } from '$lib/utils/tariff-fraction-display'; import { companyStore } from "$lib/stores/company.svelte"; import { m } from '$lib/i18n/messages'; @@ -155,7 +156,7 @@
- {item.fraction || item.code} + {getTariffFractionDisplayKey(item)}
diff --git a/frontend/src/lib/utils/tariff-fraction-display.test.ts b/frontend/src/lib/utils/tariff-fraction-display.test.ts new file mode 100644 index 00000000..26f0d7bc --- /dev/null +++ b/frontend/src/lib/utils/tariff-fraction-display.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { + getTariffFractionDisplayFraction, + getTariffFractionDisplayKey +} from './tariff-fraction-display'; +import type { TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions'; + +function buildFraction(partial: Partial): TariffFraction { + return { + id: 1, + code: '', + fraction: '', + description: null, + nico: null, + umt: null, + adv_impo: null, + adv_expo: null, + updated_at: null, + dof: null, + aplica_ieps: null, + um_code: null, + ...partial + }; +} + +describe('tariff-fraction-display', () => { + it('uses technical code for key column', () => { + const row = buildFraction({ + code: '01012101', + fraction: '0101.21.01', + um_code: '06' + }); + expect(getTariffFractionDisplayKey(row)).toBe('01012101'); + }); + + it('uses formatted fraction for fraction column', () => { + const row = buildFraction({ + code: '1234567890', + fraction: '1234.56.78.90' + }); + expect(getTariffFractionDisplayFraction(row)).toBe('1234.56.78.90'); + }); +}); diff --git a/frontend/src/lib/utils/tariff-fraction-display.ts b/frontend/src/lib/utils/tariff-fraction-display.ts new file mode 100644 index 00000000..bd2f73c5 --- /dev/null +++ b/frontend/src/lib/utils/tariff-fraction-display.ts @@ -0,0 +1,9 @@ +import type { TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions'; + +export function getTariffFractionDisplayKey(fraction: TariffFraction): string { + return fraction.code || '-'; +} + +export function getTariffFractionDisplayFraction(fraction: TariffFraction): string { + return fraction.fraction || '-'; +} diff --git a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/sitar/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/sitar/+page.svelte index 52e9771c..ce52605a 100644 --- a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/sitar/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/sitar/+page.svelte @@ -6,6 +6,7 @@