Appearance
Cotización Dólar - Documentación Técnica Backend
⚠️ DOCUMENTACIÓN RETROSPECTIVA - Generada a partir de código implementado el 2026-02-11
Módulo: Ventas Feature: Cotización Dólar Fecha: 2026-02-11 Documento de negocio: Cotización Dólar - Resource
Estado de Arquitectura
ARQUITECTURA LEGACY - Este recurso NO sigue la arquitectura 5-layer DDD moderna del sistema (API → Service → Domain → Model → DB). Está implementado con un script PHP procedimental sin separación de capas.
| Capa Esperada | Estado | Ubicación |
|---|---|---|
| Route (Slim) | No existe | N/A |
| Controller | No existe | N/A |
| Service | No existe | N/A |
| Domain | No existe | N/A |
| Model | No existe | N/A |
| Validator | No existe | N/A |
| Script Legacy | Implementado | backend/dolar.php |
Patrón implementado: Script PHP procedimental con switch por REQUEST_METHOD
Implementación Actual
Script: backend/dolar.php
Ubicación: /var/www/Bautista/server/backend/dolar.php
Responsabilidades:
- Manejo de endpoints GET y POST para cotización del dólar
- Conexión directa a base de datos usando clase
Databaselegacy - Lógica de negocio embebida (upsert pattern: SELECT + UPDATE/INSERT)
- Autenticación via JWT payload global
- Sin validación de input (delegada al proxy frontend)
- Sin auditoría
Dependencias:
JwtHandler.php- Manejo de tokens JWTexceptions.php- Excepciones personalizadasexceptionHandler.php- Manejo global de excepcionessuccess.php- Helpers de respuesta exitosamethods.php- Métodos utilitariosvalidator.php- Validador (NO usado en este endpoint)connect.php- ClaseDatabasepara conexión PDO
Flujo de autenticación:
- Lee
$GLOBALS['payload'](inyectado por middleware JWT) - Extrae
db(nombre de base de datos) yschema(schema PostgreSQL) - Crea instancia de
Database($db, $schema)para configurarsearch_path
API Endpoints Implementados
GET /api/dolar
Responsabilidad: Obtener la cotización más reciente del dólar
Autenticación: JWT requerido (token en Authorization: Bearer)
Request:
- Method:
GET - Headers:
Authorization: Bearer {token}X-Schema: {schema}(opcional, puede venir del JWT)
- Query params: Ninguno
- Body: N/A
Response (200 OK):
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": {
"id": 5,
"fecha": "2026-02-10",
"valor": 1250.5
}
}Si no hay registros:
json
{
"status": 200,
"message": "Datos recibidos correctamente.",
"data": []
}Lógica implementada:
- Conexión a base de datos usando schema del payload
- Query SQL:
SELECT * FROM dolar ORDER BY fecha DESC LIMIT 1 - Ejecuta prepared statement
- Si encuentra 1 fila:
- Extrae datos
- Convierte
valora float - Retorna con status 200
- Si no encuentra filas:
- Retorna array vacío con status 200
Códigos de status:
200 OK- Operación exitosa (con o sin datos)401 Unauthorized- Token JWT inválido/expirado500 Internal Server Error- Error en BD o excepción no controlada
POST /api/dolar
Responsabilidad: Registrar o actualizar cotización del dólar para una fecha
Autenticación: JWT requerido
Request:
- Method:
POST - Headers:
Authorization: Bearer {token}Content-Type: application/jsonX-Schema: {schema}(opcional)
- Body:
json
{
"fecha": "2026-02-11",
"valor": 1255.75
}Request DTO:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
fecha | string (date) | Sí | Fecha de la cotización (formato YYYY-MM-DD) |
valor | number | Sí | Valor del dólar en pesos argentinos |
Response (204 No Content):
Sin body (status code 204)
json
{
"status": 204,
"message": "Datos recibidos correctamente."
}Lógica implementada (Upsert Pattern):
- Decodifica JSON del body
- Conexión a base de datos
- SELECT: Verifica si existe cotización para la fecha:
SELECT * FROM dolar WHERE fecha = :fecha
- Decisión basada en rowCount:
- Si
rowCount() > 0: Ejecuta UPDATE (sobreescribe valor existente)UPDATE dolar SET valor = :valor WHERE fecha = :fecha
- Si
rowCount() === 0: Ejecuta INSERT (nueva cotización)INSERT INTO dolar (fecha, valor) VALUES (:fecha, :valor)
- Si
- Ejecuta prepared statement con parámetros
- Si éxito:
- Retorna status 204 (No Content)
- Si falla:
- Lanza
UpdateError("Error al actualizar la cotización del dólar")
- Lanza
Códigos de status:
204 No Content- Operación exitosa (INSERT o UPDATE)401 Unauthorized- Token JWT inválido500 Internal Server Error- Error en BD o lógica
⚠️ Problemas identificados:
Race condition potencial: El patrón SELECT + UPDATE/INSERT no es atómico. Dos requests simultáneos para la misma fecha podrían generar duplicados.
- Solución recomendada: Usar
INSERT ... ON CONFLICT (fecha) DO UPDATE SET valor = EXCLUDED.valor - Mitigación necesaria: Agregar constraint
UNIQUE(fecha)en la tabla
- Solución recomendada: Usar
Sin validación de input: El script confía en validación del proxy frontend. Si se invoca directamente, no valida tipos ni obligatoriedad.
- Solución recomendada: Implementar Validator middleware
Sin auditoría: No registra quién modificó la cotización ni cuándo (falta campo
updated_aty registro en tabla de auditoría).
Modelo de Datos
Tabla: dolar
Nivel de schema: EMPRESA y SUCURSAL (multi-tenant)
Definida en migración: migrations/tenancy/20240823200729_new_table_dolar.php
Descripción: Registro de cotización de dólar
| Campo | Tipo | Constraints | Descripción |
|---|---|---|---|
id | SERIAL | PRIMARY KEY, NOT NULL | Identificador único auto-incremental |
fecha | DATE | NOT NULL | Fecha de actualización del valor del dólar |
valor | DECIMAL(16,5) | NOT NULL | Precio del dólar en pesos argentinos |
Índices implementados: Ninguno (solo PRIMARY KEY)
Índices recomendados:
UNIQUE INDEX idx_dolar_fecha ON dolar(fecha)- Para garantizar unicidad y optimizar SELECT con WHERE fecha
Foreign Keys: Ninguna
Constraints adicionales: Ninguno
Nivel de configuración:
- La migración se ejecuta solo si
isVentasEnabled() || isComprasEnabled() - Se crea en niveles
EMPRESAySUCURSAL(cada sucursal puede tener su propia cotización)
Soft delete: No implementado (no tiene columna deleted_at)
Timestamps: No implementados (no tiene created_at ni updated_at)
Validaciones Implementadas
Validación Estructural (Nivel de Proxy Frontend)
El proxy en public/php/backend/dolar.php valida:
| Campo | Regla | Mensaje de error |
|---|---|---|
valor | required|numeric | "El campo valor es obligatorio" / "El campo valor debe ser numérico" |
fecha | required|date | "El campo fecha es obligatorio" / "El campo fecha debe ser una fecha válida" |
Observación: Esta validación NO está en el backend real (backend/dolar.php), sino en el proxy del frontend. El endpoint backend NO valida input.
Validación de Negocio
Implementada en frontend (JavaScript):
valor >= 0- No puede ser negativo
Sin validación backend de reglas de negocio
Integración con Base de Datos
Clase Database (Legacy)
Ubicación: connection/connect.php
Uso en el script:
php
$database = new Database($db, $schema);
$conn = $database->getConnection();Funcionalidad:
- Crea conexión PDO a PostgreSQL
- Configura
search_pathal schema especificado (multi-tenancy) - Retorna instancia PDO para queries
- No maneja transacciones en este script (cada query es auto-commit)
Queries SQL Ejecutadas
Query 1: Obtener última cotización
sql
SELECT * FROM dolar ORDER BY fecha DESC LIMIT 1- Retorna la fila más reciente por fecha
- Sin filtro adicional (podría estar en nivel EMPRESA o SUCURSAL según schema activo)
- Preparada con PDO (
prepare+execute)
Query 2: Verificar existencia por fecha
sql
SELECT * FROM dolar WHERE fecha = :fecha- Retorna todas las filas con la fecha especificada
- Debería retornar máximo 1 fila (pero no garantizado por constraint)
Query 3: Actualizar valor existente
sql
UPDATE dolar SET valor = :valor WHERE fecha = :fecha- Actualiza todas las filas que coincidan con la fecha (debería ser solo 1)
- Sin validación de
rowCountdespués del UPDATE
Query 4: Insertar nueva cotización
sql
INSERT INTO dolar (fecha, valor) VALUES (:fecha, :valor)- Inserta nuevo registro
- El campo
idse genera automáticamente (SERIAL)
Seguridad
Autenticación
- JWT requerido: El script lee
$GLOBALS['payload']que debe ser inyectado por middleware JWT previo - Middleware externo: El JWT se valida en
JwtHandler.phpANTES de invocar este script - Sin validación redundante: El script no valida el token, confía en que el middleware lo hizo
Autorización
- Sin validación de permisos: El script NO valida si el usuario tiene permiso
VENTAS_BASES_COT-DOLAR - Permisos delegados: Se asume que la validación de permisos se hace en:
- Frontend (ocultar menú si no tiene permiso)
- Proxy PHP (puede validar sesión/permisos)
⚠️ Problema de seguridad: Si se invoca directamente el endpoint backend, no valida permisos
SQL Injection
- Protegido: Usa prepared statements con
bindParamo array de parámetros - Sin concatenación de SQL: Todo correcto
Multi-tenancy
- Schema isolation: Usa
search_pathvia claseDatabase - Inyección de schema: El schema viene del JWT payload (
$GLOBALS['payload']['schema']) - Header X-Schema: El frontend puede enviar
X-Schemapara cambiar schema (validado por middleware)
⚠️ Consideración: Como la tabla existe en niveles EMPRESA y SUCURSAL, cada sucursal puede tener cotizaciones independientes. Verificar si esto es intencional o debería estar solo en EMPRESA.
Auditoría
Sin auditoría implementada: Este script legacy NO registra:
- Quién modificó la cotización
- Cuándo se modificó
- Qué valores anteriores tenía
Recomendación: Al migrar a 5-layer DDD, implementar:
- Trait
Auditableen Service AuditLoggerpara registrar operaciones CUD- Campos
created_at,updated_at,deleted_aten tabla
Integración con Otros Módulos
Módulo Compras (Subdiario de Compras)
Uso: Lee la última cotización del dólar al registrar comprobantes de compra
Query probable: Similar a GET endpoint (SELECT * FROM dolar ORDER BY fecha DESC LIMIT 1)
Almacenamiento: Guarda el valor del dólar vigente en el comprobante de compra
Sensores Legacy (Dolarización de Montos)
Uso: Lee cotizaciones históricas para dolarizar montos de ventas y stock
Query probable: Consulta tabla dolar filtrada por fechas específicas
Tabla relacionada: factura.dolar (campo marcado "sin uso" pero consultado por sensor)
⚠️ Pregunta pendiente: Ver Preguntas sobre Cotización Dólar - Pregunta #3 sobre el campo factura.dolar
Manejo de Errores
Excepciones Lanzadas
| Excepción | Cuándo | Status HTTP |
|---|---|---|
UpdateError | Falla el UPDATE o INSERT | 500 |
Cualquier Exception | Error de BD, conexión, etc. | 500 |
Manejo Global
ExceptionHandler::handle($e)- Captura y formatea todas las excepciones- Respuesta JSON con estructura:
{"error": "mensaje"}
Errores NO Manejados
- Método HTTP no soportado: Retorna 404 con mensaje "Recurso no encontrado" (debería ser 405 Method Not Allowed)
- Parámetros faltantes/inválidos: Sin validación backend, falla silenciosamente o retorna error de PDO
Performance
Índices
- Índice único natural: Campo
fechadebería tener UNIQUE constraint + índice - Consulta de última cotización: LIMIT 1 + ORDER BY fecha DESC es eficiente
- Sin paginación: No aplica (siempre retorna 1 registro o array vacío)
Transacciones
- Sin transacciones explícitas: Cada query es auto-commit
- Race condition: El patrón SELECT + UPDATE/INSERT debería ejecutarse en transacción
Recomendación:
php
$conn->beginTransaction();
try {
// SELECT FOR UPDATE + UPDATE/INSERT
$conn->commit();
} catch (Exception $e) {
$conn->rollback();
throw $e;
}Caching
- Sin cache: Cada request ejecuta query en BD
- Oportunidad de mejora: Cachear última cotización (invalidar al POST)
Testing
Sin tests implementados: El código legacy no tiene tests automatizados
Tests recomendados al migrar a 5-layer DDD:
Unit Tests:
DolarService::insert()- Verificar upsert patternDolarService::getUltima()- Verificar query correctaDolarModel::findByFecha()- Verificar query parametrizadaDolarValidator::validate()- Reglas de validación
Integration Tests:
- POST crea nueva cotización
- POST actualiza cotización existente (upsert)
- GET retorna última cotización
- GET sin datos retorna array vacío
- Verificar multi-tenancy (cotización en schema correcto)
Migración a Arquitectura Moderna (Roadmap)
Estructura Objetivo (5-Layer DDD)
Routes/Ventas/DolarRoutes.php
↓
controller/modulo-ventas/DolarController.php
↓
service/Ventas/DolarService.php
↓
models/modulo-ventas/DolarModel.php
↓
Tabla: dolarAdicionales:
Validators/Ventas/DolarValidator.php- Validación estructuralResources/Ventas/DolarRequestDTO.php- DTO de entradaResources/Ventas/DolarResponseDTO.php- DTO de salida
Cambios Necesarios
Routes:
php
$group->get('', [DolarController::class, 'getUltima']);
$group->post('', [DolarController::class, 'insert'])
->add(new ValidationMiddleware(DolarValidator::class));Controller:
php
class DolarController extends Controller
{
public function __construct(
private DolarService $service,
private AuditLogger $audit
) {
parent::__construct();
}
public function getUltima(Request $request, Response $response): Response
{
$result = $this->service->getUltima();
return $this->success($response, 200, $result);
}
public function insert(Request $request, Response $response): Response
{
$data = DolarRequestDTO::fromArray($request->getParsedBody());
$result = $this->service->insert($data);
return $this->success($response, 201, $result);
}
}Service (con auditoría y upsert atómico):
php
class DolarService implements AuditableInterface
{
use Conectable, Auditable;
private DolarModel $model;
public function __construct(ConnectionManager $manager, ?AuditLogger $audit = null)
{
$this->setConnectionManager($manager);
$this->setAuditLogger($audit);
$this->model = new DolarModel($manager->get('oficial'));
}
public function getUltima(): ?DolarResponseDTO
{
return $this->model->findUltima();
}
public function insert(DolarRequestDTO $data): DolarResponseDTO
{
$this->connections->beginTransaction('oficial');
try {
// Upsert atómico
$result = $this->model->upsert($data);
// Auditoría
$this->registrarAuditoria(
"UPSERT",
"VENTAS",
$this->model->getTable(),
$result->id
);
$this->connections->commit('oficial');
return $result;
} catch (Exception $e) {
$this->connections->rollback('oficial');
throw $e;
}
}
}Model (con upsert PostgreSQL nativo):
php
class DolarModel extends Model
{
protected string $table = 'dolar';
public function findUltima(): ?DolarResponseDTO
{
$sql = "SELECT * FROM {$this->table}
WHERE deleted_at IS NULL
ORDER BY fecha DESC
LIMIT 1";
$stmt = $this->conn->prepare($sql);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? DolarResponseDTO::fromArray($row) : null;
}
public function upsert(DolarRequestDTO $data): DolarResponseDTO
{
$sql = "INSERT INTO {$this->table} (fecha, valor)
VALUES (:fecha, :valor)
ON CONFLICT (fecha)
DO UPDATE SET valor = EXCLUDED.valor
RETURNING id, fecha, valor";
$stmt = $this->conn->prepare($sql);
$stmt->execute([
'fecha' => $data->fecha,
'valor' => $data->valor
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return DolarResponseDTO::fromArray($row);
}
}Validator:
php
class DolarValidator
{
public static function rules(): array
{
return [
'fecha' => 'required|date',
'valor' => 'required|numeric|min:0'
];
}
}Migraciones de Base de Datos Requeridas
php
// Migration: AddConstraintUniqueFechaDolar
$table->addIndex(['fecha'], ['unique' => true, 'name' => 'idx_dolar_fecha_unique']);php
// Migration: AddTimestampsToDolar
$table->addColumn('created_at', 'timestamp', ['null' => true, 'default' => 'CURRENT_TIMESTAMP'])
->addColumn('updated_at', 'timestamp', ['null' => true])
->addColumn('deleted_at', 'timestamp', ['null' => true])
->update();Preguntas Técnicas Pendientes
⚠️ Información Faltante: Hay preguntas técnicas sobre esta funcionalidad que requieren validación.
Ver: Preguntas sobre Cotización Dólar
Resumen de preguntas técnicas:
- #5: Upsert sin constraint UNIQUE - riesgo de duplicados
- #6: API externa dolarapi.com - confiabilidad y contingencia
- #7: Redirección post-guardado - flujo de usuario
Referencias
⚠️ NOTA IMPORTANTE: Esta documentación fue generada automáticamente analizando el código implementado. Este recurso se encuentra en arquitectura legacy (script PHP procedimental) y NO sigue los patrones 5-layer DDD del sistema. Se recomienda migración a arquitectura moderna para incorporar validaciones, auditoría, tests y mejores prácticas de seguridad.