unit uDecaTypes;

{******************************************************************************}
{                                                                              }
{  Módulo DECA - Documento Electrónico de Control Administrativo               }
{  Conforme a Orden FOM/2861/2012 y Resolución DGT/DGTCF 5 de junio de 2026   }
{  Desarrollado para Delphi 11 Alexandria / FireDAC                            }
{                                                                              }
{******************************************************************************}

interface

uses
  System.SysUtils, System.Classes, System.Generics.Collections, System.JSON,
  System.DateUtils, System.Hash;

type
  // Estados del ciclo de vida del DeCA
  TDecaEstado = (decaBorrador, decaEmitido, decaRectificado, decaAnulado);

  // Tipo de operación en paradas intermedias
  TDecaOperacionParada = (opPaso, opCarga, opDescarga, opCargaDescarga);

  // Unidades admisibles en ADR
  TDecaUnidadADR = (uLitros, uKgNeto, uKgBruto);

  // Grupo de embalaje ADR
  TDecaGrupoEmbalaje = (geI, geII, geIII, geNoAplica);

  // ---------------------------------------------------------------------------
  // Registro: Entidad Fiscal (Cargador / Transportista / Almacén)
  // ---------------------------------------------------------------------------
  TDecaEntidadFiscal = record
    Nombre: string;
    NIF: string;
    Direccion: string;
    CodigoPostal: string;
    Localidad: string;
    Provincia: string;
    Pais: string;
    function Validar(out AError: string; const ATipoEntidad: string): Boolean;
    function ToJSON: TJSONObject;
    procedure FromJSON(AJSON: TJSONObject);
  end;

  // ---------------------------------------------------------------------------
  // Registro: Parada Intermedia de Ruta
  // ---------------------------------------------------------------------------
  TDecaParada = record
    IDParada: Int64;
    Orden: Integer;
    Nombre: string;
    NIF: string;
    Operacion: TDecaOperacionParada;
    KgDescarga: Double;
    Direccion: string;
    CodigoPostal: string;
    Localidad: string;
    Provincia: string;
    Detalle: string;
    function OperacionToString: string;
    procedure OperacionFromString(const AStr: string);
    function ToJSON: TJSONObject;
    procedure FromJSON(AJSON: TJSONObject);
  end;

  // ---------------------------------------------------------------------------
  // Registro: Línea de Mercancía Peligrosa (ADR)
  // ---------------------------------------------------------------------------
  TDecaLineaADR = record
    IDLineaADR: Int64;
    Orden: Integer;
    NumeroONU: string;           // Ej: 'UN 1230'
    Denominacion: string;        // Designación oficial de transporte
    Clase: string;               // 2, 3, 4.1, 8, 9, etc.
    Etiquetas: string;           // Modelos 3, 6.1
    GrupoEmbalaje: TDecaGrupoEmbalaje;
    CodigoTunel: string;         // 'D/E', '-'
    RequiereNombreTecnico: Boolean;
    NombreTecnico: string;
    Bultos: Integer;
    TipoBulto: string;           // 'Bidones de acero (1A1)'
    Cantidad: Double;
    Unidad: TDecaUnidadADR;
    DesdeLugar: string;          // 'origen' o ID de parada
    HastaLugar: string;          // 'destino' o ID de parada
    PeligrosoMedioAmbiente: Boolean;
    Menciones: string;
    function ToJSON: TJSONObject;
    procedure FromJSON(AJSON: TJSONObject);
  end;

  // ---------------------------------------------------------------------------
  // Clase: Documento DeCA Completo
  // ---------------------------------------------------------------------------
  TDecaDocumento = class
  private
    FIDDeca: Int64;
    FEjercicio: Word;
    FSerie: string;
    FNumero: Cardinal;
    FTokenPublico: string;
    FUrlDescargaQR: string;
    FEstado: TDecaEstado;
    FFechaServicio: TDate;
    FHoraServicio: TTime;
    FFechaEmision: TDateTime;
    FFechaModificacion: TDateTime;
    FUsuarioCreacion: string;
    FUsuarioModificacion: string;

    FCargador: TDecaEntidadFiscal;
    FTransportista: TDecaEntidadFiscal;

    FMatriculaTractora: string;
    FMatriculaRemolque: string;
    FTipoVehiculo: string;
    FConductorNombre: string;
    FConductorNIF: string;
    FConductorTelefono: string;

    FOrigen: TDecaEntidadFiscal;
    FDestino: TDecaEntidadFiscal;

    FMercanciaDescripcion: string;
    FPesoNetoKg: Double;
    FBultosTotal: Integer;
    FVolumenM3: Double;
    FObservaciones: string;
    FAutorizacionEspecial: string;

    FEsADR: Boolean;
    FADRExpedidorNombre: string;
    FADRExpedidorDireccion: string;
    FADRDestinatarioNombre: string;
    FADRDestinatarioDireccion: string;
    FADRReferencia: string;
    FADRMencionesGenerales: string;
    FADRRevisado: Boolean;

    FParadas: TList<TDecaParada>;
    FLineasADR: TList<TDecaLineaADR>;
  public
    constructor Create;
    destructor Destroy; override;

    function GenerarTokenCriptografico: string;
    function ValidarObligatorios(out AError: string): Boolean;
    function CodigoCompleto: string;

    property IDDeca: Int64 read FIDDeca write FIDDeca;
    property Ejercicio: Word read FEjercicio write FEjercicio;
    property Serie: string read FSerie write FSerie;
    property Numero: Cardinal read FNumero write FNumero;
    property TokenPublico: string read FTokenPublico write FTokenPublico;
    property UrlDescargaQR: string read FUrlDescargaQR write FUrlDescargaQR;
    property Estado: TDecaEstado read FEstado write FEstado;
    property FechaServicio: TDate read FFechaServicio write FFechaServicio;
    property HoraServicio: TTime read FHoraServicio write FHoraServicio;
    property FechaEmision: TDateTime read FFechaEmision write FFechaEmision;
    property FechaModificacion: TDateTime read FFechaModificacion write FFechaModificacion;
    property UsuarioCreacion: string read FUsuarioCreacion write FUsuarioCreacion;
    property UsuarioModificacion: string read FUsuarioModificacion write FUsuarioModificacion;

    property Cargador: TDecaEntidadFiscal read FCargador write FCargador;
    property Transportista: TDecaEntidadFiscal read FTransportista write FTransportista;

    property MatriculaTractora: string read FMatriculaTractora write FMatriculaTractora;
    property MatriculaRemolque: string read FMatriculaRemolque write FMatriculaRemolque;
    property TipoVehiculo: string read FTipoVehiculo write FTipoVehiculo;
    property ConductorNombre: string read FConductorNombre write FConductorNombre;
    property ConductorNIF: string read FConductorNIF write FConductorNIF;
    property ConductorTelefono: string read FConductorTelefono write FConductorTelefono;

    property Origen: TDecaEntidadFiscal read FOrigen write FOrigen;
    property Destino: TDecaEntidadFiscal read FDestino write FDestino;

    property MercanciaDescripcion: string read FMercanciaDescripcion write FMercanciaDescripcion;
    property PesoNetoKg: Double read FPesoNetoKg write FPesoNetoKg;
    property PesoBrutoKg: Double read FPesoNetoKg write FPesoNetoKg;
    property BultosTotal: Integer read FBultosTotal write FBultosTotal;
    property VolumenM3: Double read FVolumenM3 write FVolumenM3;
    property Observaciones: string read FObservaciones write FObservaciones;
    property AutorizacionEspecial: string read FAutorizacionEspecial write FAutorizacionEspecial;

    property EsADR: Boolean read FEsADR write FEsADR;
    property ADRExpedidorNombre: string read FADRExpedidorNombre write FADRExpedidorNombre;
    property ADRExpedidorDireccion: string read FADRExpedidorDireccion write FADRExpedidorDireccion;
    property ADRDestinatarioNombre: string read FADRDestinatarioNombre write FADRDestinatarioNombre;
    property ADRDestinatarioDireccion: string read FADRDestinatarioDireccion write FADRDestinatarioDireccion;
    property ADRReferencia: string read FADRReferencia write FADRReferencia;
    property ADRMencionesGenerales: string read FADRMencionesGenerales write FADRMencionesGenerales;
    property ADRRevisado: Boolean read FADRRevisado write FADRRevisado;

    property Paradas: TList<TDecaParada> read FParadas;
    property LineasADR: TList<TDecaLineaADR> read FLineasADR;
  end;

implementation

{ TDecaEntidadFiscal }

function TDecaEntidadFiscal.Validar(out AError: string; const ATipoEntidad: string): Boolean;
begin
  Result := False;
  if Trim(Nombre) = '' then
  begin
    AError := Format('El nombre/razón social de %s es obligatorio.', [ATipoEntidad]);
    Exit;
  end;
  if Trim(NIF) = '' then
  begin
    AError := Format('El NIF de %s es obligatorio.', [ATipoEntidad]);
    Exit;
  end;
  if Trim(Direccion) = '' then
  begin
    AError := Format('La dirección de %s es obligatoria.', [ATipoEntidad]);
    Exit;
  end;
  if Trim(Localidad) = '' then
  begin
    AError := Format('La localidad de %s es obligatoria.', [ATipoEntidad]);
    Exit;
  end;
  if Trim(Provincia) = '' then
  begin
    AError := Format('La provincia de %s es obligatoria.', [ATipoEntidad]);
    Exit;
  end;
  Result := True;
end;

function TDecaEntidadFiscal.ToJSON: TJSONObject;
begin
  Result := TJSONObject.Create;
  Result.AddPair('nombre', Nombre);
  Result.AddPair('nif', NIF);
  Result.AddPair('direccion', Direccion);
  Result.AddPair('cp', CodigoPostal);
  Result.AddPair('localidad', Localidad);
  Result.AddPair('provincia', Provincia);
  Result.AddPair('pais', Pais);
end;

procedure TDecaEntidadFiscal.FromJSON(AJSON: TJSONObject);
begin
  if AJSON = nil then Exit;
  Nombre := AJSON.GetValue<string>('nombre', '');
  NIF := AJSON.GetValue<string>('nif', '');
  Direccion := AJSON.GetValue<string>('direccion', '');
  CodigoPostal := AJSON.GetValue<string>('cp', '');
  Localidad := AJSON.GetValue<string>('localidad', '');
  Provincia := AJSON.GetValue<string>('provincia', '');
  Pais := AJSON.GetValue<string>('pais', 'España');
end;

{ TDecaParada }

function TDecaParada.OperacionToString: string;
begin
  case Operacion of
    opPaso: Result := 'paso';
    opCarga: Result := 'carga';
    opDescarga: Result := 'descarga';
    opCargaDescarga: Result := 'carga_descarga';
  else
    Result := 'paso';
  end;
end;

procedure TDecaParada.OperacionFromString(const AStr: string);
begin
  if SameText(AStr, 'carga') then Operacion := opCarga
  else if SameText(AStr, 'descarga') then Operacion := opDescarga
  else if SameText(AStr, 'carga_descarga') then Operacion := opCargaDescarga
  else Operacion := opPaso;
end;

function TDecaParada.ToJSON: TJSONObject;
begin
  Result := TJSONObject.Create;
  Result.AddPair('id', TJSONNumber.Create(IDParada));
  Result.AddPair('orden', TJSONNumber.Create(Orden));
  Result.AddPair('nombre', Nombre);
  Result.AddPair('nif', NIF);
  Result.AddPair('operacion', OperacionToString);
  Result.AddPair('kg_descarga', TJSONNumber.Create(KgDescarga));
  Result.AddPair('direccion', Direccion);
  Result.AddPair('cp', CodigoPostal);
  Result.AddPair('localidad', Localidad);
  Result.AddPair('provincia', Provincia);
  Result.AddPair('detalle', Detalle);
end;

procedure TDecaParada.FromJSON(AJSON: TJSONObject);
begin
  if AJSON = nil then Exit;
  IDParada := AJSON.GetValue<Int64>('id', 0);
  Orden := AJSON.GetValue<Integer>('orden', 1);
  Nombre := AJSON.GetValue<string>('nombre', '');
  NIF := AJSON.GetValue<string>('nif', '');
  OperacionFromString(AJSON.GetValue<string>('operacion', 'paso'));
  KgDescarga := AJSON.GetValue<Double>('kg_descarga', 0.0);
  Direccion := AJSON.GetValue<string>('direccion', '');
  CodigoPostal := AJSON.GetValue<string>('cp', '');
  Localidad := AJSON.GetValue<string>('localidad', '');
  Provincia := AJSON.GetValue<string>('provincia', '');
  Detalle := AJSON.GetValue<string>('detalle', '');
end;

{ TDecaLineaADR }

function TDecaLineaADR.ToJSON: TJSONObject;
begin
  Result := TJSONObject.Create;
  Result.AddPair('orden', TJSONNumber.Create(Orden));
  Result.AddPair('un', NumeroONU);
  Result.AddPair('denominacion', Denominacion);
  Result.AddPair('clase', Clase);
  Result.AddPair('etiquetas', Etiquetas);
  Result.AddPair('grupo', Ord(GrupoEmbalaje));
  Result.AddPair('tunel', CodigoTunel);
  Result.AddPair('tecnico_necesario', TJSONBool.Create(RequiereNombreTecnico));
  Result.AddPair('nombre_tecnico', NombreTecnico);
  Result.AddPair('bultos', TJSONNumber.Create(Bultos));
  Result.AddPair('tipo_bulto', TipoBulto);
  Result.AddPair('cantidad', TJSONNumber.Create(Cantidad));
  Result.AddPair('unidad', Ord(Unidad));
  Result.AddPair('desde', DesdeLugar);
  Result.AddPair('hasta', HastaLugar);
  Result.AddPair('medio_ambiente', TJSONBool.Create(PeligrosoMedioAmbiente));
  Result.AddPair('menciones', Menciones);
end;

procedure TDecaLineaADR.FromJSON(AJSON: TJSONObject);
begin
  if AJSON = nil then Exit;
  Orden := AJSON.GetValue<Integer>('orden', 1);
  NumeroONU := AJSON.GetValue<string>('un', '');
  Denominacion := AJSON.GetValue<string>('denominacion', '');
  Clase := AJSON.GetValue<string>('clase', '');
  Etiquetas := AJSON.GetValue<string>('etiquetas', '');
  GrupoEmbalaje := TDecaGrupoEmbalaje(AJSON.GetValue<Integer>('grupo', 3));
  CodigoTunel := AJSON.GetValue<string>('tunel', '');
  RequiereNombreTecnico := AJSON.GetValue<Boolean>('tecnico_necesario', False);
  NombreTecnico := AJSON.GetValue<string>('nombre_tecnico', '');
  Bultos := AJSON.GetValue<Integer>('bultos', 0);
  TipoBulto := AJSON.GetValue<string>('tipo_bulto', '');
  Cantidad := AJSON.GetValue<Double>('cantidad', 0.0);
  Unidad := TDecaUnidadADR(AJSON.GetValue<Integer>('unidad', 1));
  DesdeLugar := AJSON.GetValue<string>('desde', 'origen');
  HastaLugar := AJSON.GetValue<string>('hasta', 'destino');
  PeligrosoMedioAmbiente := AJSON.GetValue<Boolean>('medio_ambiente', False);
  Menciones := AJSON.GetValue<string>('menciones', '');
end;

{ TDecaDocumento }

constructor TDecaDocumento.Create;
begin
  inherited Create;
  FEjercicio := CurrentYear;
  FSerie := 'DCA';
  FEstado := decaBorrador;
  FFechaEmision := Now;
  FFechaServicio := Date;
  FParadas := TList<TDecaParada>.Create;
  FLineasADR := TList<TDecaLineaADR>.Create;
  FTokenPublico := GenerarTokenCriptografico;
end;

destructor TDecaDocumento.Destroy;
begin
  FParadas.Free;
  FLineasADR.Free;
  inherited;
end;

function TDecaDocumento.GenerarTokenCriptografico: string;
var
  LRaw: string;
begin
  // Genera hash SHA-256 no predecible a partir de GUID + Timestamp + Random
  LRaw := TGUID.NewGuid.ToString + IntToStr(DateTimeToUnix(Now, True)) + IntToStr(Random(999999));
  Result := THashSHA2.GetHashString(LRaw);
end;

function TDecaDocumento.CodigoCompleto: string;
begin
  Result := Format('%s-%d-%.6d', [FSerie, FEjercicio, FNumero]);
end;

function TDecaDocumento.ValidarObligatorios(out AError: string): Boolean;
var
  LErr: string;
begin
  Result := False;

  // 1. Cargador Contractual
  if not FCargador.Validar(LErr, 'Cargador Contractual') then
  begin
    AError := LErr;
    Exit;
  end;

  // 2. Transportista Efectivo
  if not FTransportista.Validar(LErr, 'Transportista Efectivo') then
  begin
    AError := LErr;
    Exit;
  end;

  // 3. Origen y Destino
  if Trim(FOrigen.Direccion) = '' then
  begin
    AError := 'La dirección de Origen es obligatoria.';
    Exit;
  end;
  if Trim(FDestino.Direccion) = '' then
  begin
    AError := 'La dirección de Destino final es obligatoria.';
    Exit;
  end;

  // 4. Mercancía y Peso
  if Trim(FMercanciaDescripcion) = '' then
  begin
    AError := 'La naturaleza/descripción de la mercancía es obligatoria.';
    Exit;
  end;
  if FPesoNetoKg <= 0 then
  begin
    AError := 'El peso neto de la mercancía transportada debe ser superior a 0 kg.';
    Exit;
  end;

  // 5. Matrícula
  if Trim(FMatriculaTractora) = '' then
  begin
    AError := 'La matrícula del camión o cabeza tractora es obligatoria.';
    Exit;
  end;

  // 6. Fecha de servicio
  if FFechaServicio <= 0 then
  begin
    AError := 'La fecha de realización del transporte es obligatoria.';
    Exit;
  end;

  Result := True;
end;

end.
