fix(customs-brokers): update VU and personnel for multi-tenancy support

This commit is contained in:
Galindo97
2026-02-24 17:41:40 -06:00
parent a52602dedf
commit 980c913d46
6 changed files with 124 additions and 65 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Python
__pycache__/
.mypy_cache/
*.py[cod]
*$py.class
*.so

View File

@@ -43,7 +43,7 @@ class CustomsBrokerResponseDTO(CustomsBrokerBaseDTO):
broker_key: str
tenant_id: int
company_id: int
vu: Optional["CustomsBrokerVUCreateDTO"] = None
vu: Optional["CustomsBrokerVUResponseDTO"] = None
class Config:
from_attributes = True
@@ -95,6 +95,11 @@ class CustomsBrokerVUCreateDTO(BaseModel):
doda_web_service_access_key: Optional[str] = None
doda_fiel_access_key: Optional[str] = None
doda_xml_files_path: Optional[str] = None
tenant_id: Optional[int] = None
company_id: Optional[int] = None
class CustomsBrokerVUResponseDTO(CustomsBrokerVUCreateDTO):
customs_broker_id: int
class Config:
from_attributes = True
@@ -112,6 +117,8 @@ class CustomsBrokerPersonnelDTO(BaseModel):
last_name: Optional[str] = None
middle_name: Optional[str] = None
email: Optional[str] = None
tenant_id: Optional[int] = None
company_id: Optional[int] = None
class Config:
from_attributes = True

View File

@@ -62,7 +62,7 @@ def update_customs_broker(
@router.put(
"/customs-broker-vu/{broker_key}",
response_model=dto.CustomsBrokerVUCreateDTO,
response_model=dto.CustomsBrokerVUResponseDTO,
)
def update_customs_broker_vu(
broker_key: str,
@@ -77,7 +77,7 @@ def update_customs_broker_vu(
if not broker:
raise HTTPException(status_code=404, detail="Customs Broker not found")
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data)
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data, tenant_id, company_id)
if not updated_vu:
raise HTTPException(status_code=404, detail="Customs Broker VU not found")
return updated_vu
@@ -102,7 +102,7 @@ def update_customs_broker_personnel(
raise HTTPException(status_code=404, detail="Customs Broker not found")
updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(
db, broker_key, line, personnel_data
db, broker_key, line, personnel_data, tenant_id, company_id
)
if not updated_personnel:
raise HTTPException(

View File

@@ -90,9 +90,17 @@ class CustomsBrokerVUService:
return new_vu
@staticmethod
def update_vu(db: Session, broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO):
def update_vu(db: Session, broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO, tenant_id: int, company_id: int):
# We need the custom broker ID to insert a new VU
broker = db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
broker = (
db.query(models.CustomsBroker)
.filter(
models.CustomsBroker.broker_key == broker_key,
models.CustomsBroker.tenant_id == tenant_id,
models.CustomsBroker.company_id == company_id,
)
.first()
)
if not broker:
return None
@@ -108,6 +116,8 @@ class CustomsBrokerVUService:
else:
# Create new
new_vu_data = vu_data.model_dump()
new_vu_data["tenant_id"] = tenant_id
new_vu_data["company_id"] = company_id
new_vu = models.CustomsBrokerVU(customs_broker_id=broker.id, **new_vu_data)
db.add(new_vu)
db.commit()
@@ -125,24 +135,36 @@ class CustomsBrokerVUService:
class CustomsBrokerPersonnelService:
@staticmethod
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int):
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int, tenant_id: int, company_id: int):
return (
db.query(models.CustomsBrokerPersonnel)
.join(models.CustomsBroker)
.filter(
models.CustomsBroker.broker_key == broker_key,
models.CustomsBrokerPersonnel.line == line,
models.CustomsBroker.tenant_id == tenant_id,
models.CustomsBroker.company_id == company_id,
)
.first()
)
@staticmethod
def create_personnel(db: Session, broker_key: str, personnel_data: dto.CustomsBrokerPersonnelDTO):
broker = db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
def create_personnel(db: Session, broker_key: str, personnel_data: dto.CustomsBrokerPersonnelDTO, tenant_id: int, company_id: int):
broker = (
db.query(models.CustomsBroker)
.filter(
models.CustomsBroker.broker_key == broker_key,
models.CustomsBroker.tenant_id == tenant_id,
models.CustomsBroker.company_id == company_id,
)
.first()
)
if not broker:
return None
new_personnel_data = personnel_data.model_dump()
new_personnel_data["tenant_id"] = tenant_id
new_personnel_data["company_id"] = company_id
new_personnel = models.CustomsBrokerPersonnel(customs_broker_id=broker.id, **new_personnel_data)
db.add(new_personnel)
db.commit()
@@ -155,16 +177,22 @@ class CustomsBrokerPersonnelService:
broker_key: str,
line: int,
personnel_data: dto.CustomsBrokerPersonnelDTO,
tenant_id: int,
company_id: int,
):
personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(
db, broker_key, line
db, broker_key, line, tenant_id, company_id
)
if personnel:
for key, value in personnel_data.model_dump(exclude_unset=True).items():
setattr(personnel, key, value)
db.commit()
db.refresh(personnel)
return personnel
return personnel
else:
return CustomsBrokerPersonnelService.create_personnel(
db, broker_key, personnel_data, tenant_id, company_id
)
@staticmethod
def delete_personnel(db: Session, broker_key: str, line: int):

View File

@@ -1,4 +1,5 @@
import { api } from '$lib/api';
import { companyStore } from '$lib/stores/company.svelte'; // <--- NUEVO: Importamos el store para el fallback
import type { ApiResponse } from '$lib/api';
export interface CustomsBroker {
@@ -26,6 +27,8 @@ export interface CustomsBroker {
}
export interface CustomsBrokerVU {
tenant_id?: string | null;
company_id?: string | null;
certificate_path?: string | null;
key_path?: string | null;
access_key?: string | null;
@@ -95,7 +98,9 @@ export interface CustomsBrokerListResponse {
*/
export const customsBrokersApi = {
list: (companyId: string, page = 1, pageSize = 50) => {
return api.get<CustomsBrokerListResponse>(`/v1/a76/customs-brokers?company_id=${companyId}&page=${page}&page_size=${pageSize}`);
return api.get<CustomsBrokerListResponse>(
`/v1/a76/customs-brokers?company_id=${companyId}&page=${page}&page_size=${pageSize}`
);
},
get: (brokerKey: string, companyId: string) => {
@@ -111,20 +116,47 @@ export const customsBrokersApi = {
*/
update: (brokerKey: string, data: CreateCustomsBrokerData) => {
const companyId = data.company_id;
return api.put<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data);
return api.put<CustomsBroker>(
`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`,
data
);
},
/**
* Elimina un agente aduanal
*/
delete: (brokerKey: string, companyId: string) => {
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
return api.delete<CustomsBroker>(
`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`
);
},
/**
* Actualiza la información de Ventanilla Única (VU)
*/
updateVU: (brokerKey: string, data: CustomsBrokerVU, companyId: string) => {
return api.put<CustomsBrokerVU>(`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${companyId}`, data);
// LOGICA DE RESCATE:
// Si companyId llega nulo/undefined, intentamos obtenerlo del store global
let finalCompanyId = companyId;
if (!finalCompanyId && companyStore.activeCompany?.id) {
finalCompanyId = companyStore.activeCompany.id.toString();
console.warn("WARN: companyId no fue provisto a updateVU, usando companyStore:", finalCompanyId);
}
// Aseguramos que el payload tenga los IDs
const payload = {
...data,
company_id: finalCompanyId,
tenant_id: finalCompanyId
};
console.log('[DEBUG] Enviando payload VU:', payload);
return api.put<CustomsBrokerVU>(
`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${finalCompanyId}`,
payload
);
},
updatePersonnel: (brokerKey: string, line: number, data: CustomsBrokerPersonnel, companyId: string) => {

View File

@@ -187,8 +187,7 @@
const brokerColumns = createBrokerColumns(handleActionSuccess);
</script>
<div class="flex flex-col h-[calc(100vh-4rem)] p-4 gap-4 pb-15">
<!-- Title -->
<div class="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 pb-15">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-1">
<h1 class="text-2xl font-bold">GESTIÓN ADUANAL</h1>
@@ -196,17 +195,17 @@
</div>
</div>
<Tabs.Root bind:value={activeTab} class="flex-1 flex flex-col overflow-hidden">
<Tabs.List class="w-full justify-start border-b rounded-none bg-transparent p-0 mb-4">
<Tabs.Root bind:value={activeTab} class="flex flex-1 flex-col overflow-hidden">
<Tabs.List class="mb-4 w-full justify-start rounded-none border-b bg-transparent p-0">
<Tabs.Trigger
value="brokers"
class="data-[state=active]:border-primary border-b-2 border-transparent rounded-none"
class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
>
Agentes Aduanales
</Tabs.Trigger>
<Tabs.Trigger
value="customs"
class="data-[state=active]:border-primary border-b-2 border-transparent rounded-none"
class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
>
Secciones Aduanales
</Tabs.Trigger>
@@ -214,13 +213,11 @@
<Tabs.Content
value="brokers"
class="flex-1 flex gap-4 overflow-hidden mt-0 data-[state=inactive]:hidden"
class="mt-0 flex flex-1 gap-4 overflow-hidden data-[state=inactive]:hidden"
>
<!-- Left Panel: Table -->
<div class="flex-1 flex flex-col gap-4 overflow-hidden">
<!-- Filters -->
<div class="border rounded-lg bg-card">
<div class="p-4 space-y-4">
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
<div class="rounded-lg border bg-card">
<div class="space-y-4 p-4">
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold">Filtros</h2>
<span class="text-xs text-muted-foreground">Busque por nombre o patente</span>
@@ -244,23 +241,20 @@
oninput={handleSearch}
/>
</div>
<div class="flex items-end">
<!-- Placeholder for layout balance -->
</div>
<div class="flex items-end"></div>
</div>
</div>
</div>
<!-- Table -->
<div class="flex-1 flex flex-col border rounded-lg overflow-hidden">
<div class="flex items-center justify-between p-3 border-b bg-muted/30">
<div class="flex flex-1 flex-col overflow-hidden rounded-lg border">
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
<h2 class="text-sm font-semibold">Listado</h2>
<div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground">
{filteredItems.length} registros
</span>
<Button variant="outline" size="sm" onclick={loadItems}>
<RefreshCw class="h-4 w-4 mr-2" />
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
</div>
@@ -275,9 +269,8 @@
idField="broker_key"
/>
</div>
<!-- Simple Pagination Controls -->
{#if totalItems > pageSize}
<div class="p-2 border-t flex justify-end gap-2">
<div class="flex justify-end gap-2 border-t p-2">
<Button
variant="outline"
size="sm"
@@ -286,7 +279,7 @@
>
Anterior
</Button>
<span class="flex items-center text-xs text-muted-foreground px-2">
<span class="flex items-center px-2 text-xs text-muted-foreground">
Página {currentPage} de {Math.ceil(totalItems / pageSize)}
</span>
<Button
@@ -302,33 +295,32 @@
</div>
</div>
<!-- Right Panel: Details -->
<div
class="w-96 flex-none flex flex-col border rounded-xl bg-muted/30 shadow-sm overflow-hidden"
class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm"
>
<div class="p-4 border-b bg-card">
<p class="text-[10px] uppercase tracking-widest opacity-80 text-muted-foreground">
<div class="border-b bg-card p-4">
<p class="text-[10px] tracking-widest text-muted-foreground uppercase opacity-80">
Detalles del Agente
</p>
<h2
class="text-xl font-black font-mono tracking-tighter truncate"
class="truncate font-mono text-xl font-black tracking-tighter"
title={selectedItem?.name || ''}
>
{selectedItem?.name || '---'}
</h2>
<div class="flex items-center gap-2 mt-1">
<span class="text-xs font-mono text-muted-foreground"
<div class="mt-1 flex items-center gap-2">
<span class="font-mono text-xs text-muted-foreground"
>Patente: {selectedItem?.broker_key || ''}</span
>
</div>
</div>
<div class="flex-1 overflow-auto p-5 space-y-6 bg-card">
<div class="flex-1 space-y-6 overflow-auto bg-card p-5">
{#if selectedItem}
<div class="grid grid-cols-1 gap-4">
<div class="space-y-1">
<Label
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
>
<FileText size={10} /> Licencia / Autorización
</Label>
@@ -338,21 +330,21 @@
{#if selectedItem.tax_id}
<div class="space-y-1">
<Label
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
>
<Hash size={10} /> RFC / Tax ID
</Label>
<p class="text-sm font-mono">{selectedItem.tax_id}</p>
<p class="font-mono text-sm">{selectedItem.tax_id}</p>
</div>
{/if}
<div class="pt-4 border-t space-y-3">
<div class="space-y-3 border-t pt-4">
<Label
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
>
<MapPin size={10} /> Dirección
</Label>
<div class="text-sm space-y-1">
<div class="space-y-1 text-sm">
<p>{selectedItem.address || ''}</p>
<p>
{[selectedItem.city, selectedItem.state].filter(Boolean).join(', ')}
@@ -363,9 +355,9 @@
</div>
</div>
<div class="pt-4 border-t space-y-3">
<div class="space-y-3 border-t pt-4">
<Label
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
>
<Phone size={10} /> Contacto
</Label>
@@ -391,9 +383,9 @@
</div>
{:else}
<div
class="flex flex-col items-center justify-center h-full text-center text-muted-foreground opacity-50"
class="flex h-full flex-col items-center justify-center text-center text-muted-foreground opacity-50"
>
<Building2 class="h-12 w-12 mb-3" />
<Building2 class="mb-3 h-12 w-12" />
<p class="text-sm">Selecciona un agente</p>
</div>
{/if}
@@ -401,9 +393,8 @@
</div>
</Tabs.Content>
<Tabs.Content value="customs" class="flex-1 overflow-auto mt-0 data-[state=inactive]:hidden">
<!-- Reusing DataTable from Customs Sections -->
<Card.Root class="h-full flex flex-col border-none shadow-none">
<Tabs.Content value="customs" class="mt-0 flex-1 overflow-auto data-[state=inactive]:hidden">
<Card.Root class="flex h-full flex-col border-none shadow-none">
<Card.Content class="flex-1 p-0">
<SectionsDataTable
data={sections}
@@ -418,13 +409,13 @@
</Tabs.Root>
</div>
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
>
<div class="px-4 py-4 max-w-[1400px] mx-auto">
<div class="mx-auto max-w-[1400px] px-4 py-4">
<div class="flex justify-end gap-2">
{#if activeTab === 'brokers'}
<Button size="sm" href="/dashboard/customs_brokers/edit">
<Plus class="h-4 w-4 mr-1" />
<Button size="sm" href="/dashboard/customs_brokers/edit/new">
<Plus class="mr-1 h-4 w-4" />
Nuevo
</Button>
<Button variant="outline" size="sm" onclick={handleEdit} disabled={!selectedItem}>
@@ -441,7 +432,7 @@
</Button>
{:else}
<Button size="sm" onclick={() => toast.info('Pendiente')}>
<Plus class="h-4 w-4 mr-1" />
<Plus class="mr-1 h-4 w-4" />
Nueva Sección
</Button>
{/if}