unit uValidacionFiscal;

{
  uValidacionFiscal.pas
  Validación algorítmica de documentos de identificación fiscal españoles.

  Tipos soportados:
    - DNI  : 8 dígitos + 1 letra de control (módulo 23)
    - NIE  : X/Y/Z + 7 dígitos + 1 letra de control
    - CIF  : 1 letra de organización + 7 dígitos + 1 dígito o letra de control

  Uso:
    var LTipo: TTipoDocumento;
    if not TValidacionFiscal.ValidarDocumento(edtNIF.Text, LTipo) then
      // documento inválido
}

interface

type
  /// <summary>
  /// Tipo de documento identificativo detectado.
  /// </summary>
  TTipoDocumento = (tdDesconocido, tdDNI, tdNIE, tdCIF);

  /// <summary>
  /// Clase de utilidad para validar documentos fiscales españoles.
  /// Todos los métodos son de clase (no requieren instancia).
  /// </summary>
  TValidacionFiscal = class
  private
    class function LetraDNI(const ANumero: Integer): Char; static;
    class function SumaDigitos(const ANum: Integer): Integer; static;
  public
    /// <summary>
    /// Normaliza el documento: mayúsculas, sin espacios ni guiones.
    /// </summary>
    class function Normalizar(const ADoc: string): string; static;

    /// <summary>
    /// Valida un DNI español (8 dígitos + letra de control por módulo 23).
    /// </summary>
    class function ValidarDNI(const ADNI: string): Boolean; static;

    /// <summary>
    /// Valida un NIE español (X/Y/Z + 7 dígitos + letra de control).
    /// </summary>
    class function ValidarNIE(const ANIE: string): Boolean; static;

    /// <summary>
    /// Valida un CIF español (letra organización + 7 dígitos + control).
    /// </summary>
    class function ValidarCIF(const ACIF: string): Boolean; static;

    /// <summary>
    /// Detecta automáticamente el tipo y valida el documento.
    /// Devuelve el tipo detectado en ATipo aunque el documento sea inválido.
    /// </summary>
    class function ValidarDocumento(const ADoc: string; out ATipo: TTipoDocumento): Boolean; static;

    /// <summary>
    /// Devuelve el nombre del tipo de documento como cadena.
    /// </summary>
    class function TipoAsString(const ATipo: TTipoDocumento): string; static;

    /// <summary>
    /// Calcula el dígito de control Módulo 10 (3-1-3-1) para un código SSCC de 17 dígitos.
    /// </summary>
    class function CalcularDigitoControlSSCC(const A17Digits: string): Integer; static;

    /// <summary>
    /// Genera un código SSCC-18 estándar internacional completo (18 dígitos).
    /// </summary>
    class function GenerarSSCC18(const APrefijoGS1Empresa: string; ACorrelativoPale: Int64; ADigitoExtension: Integer = 3): string; static;
  end;

implementation

uses
  System.SysUtils;

const
  // Cadena de letras de control para DNI/NIE (índice = número mod 23)
  LETRAS_DNI  : string = 'TRWAGMYFPDXBNJZSQVHLCKE';
  // Letras de control para dígito de control del CIF (índice 0..9)
  LETRAS_CTRL : string = 'JABCDEFGHI';

{ TValidacionFiscal — Privados }

class function TValidacionFiscal.LetraDNI(const ANumero: Integer): Char;
begin
  // Las cadenas Delphi son 1-indexed; módulo 23 da índice 0..22 → sumar 1
  Result := LETRAS_DNI[(ANumero mod 23) + 1];
end;

class function TValidacionFiscal.SumaDigitos(const ANum: Integer): Integer;
begin
  // Para números de dos dígitos: suma sus cifras (ej: 16 → 7)
  if ANum < 10 then
    Result := ANum
  else
    Result := (ANum mod 10) + (ANum div 10);
end;

{ TValidacionFiscal — Públicos }

class function TValidacionFiscal.Normalizar(const ADoc: string): string;
begin
  Result := UpperCase(Trim(ADoc));
  Result := StringReplace(Result, ' ', '', [rfReplaceAll]);
  Result := StringReplace(Result, '-', '', [rfReplaceAll]);
  Result := StringReplace(Result, '.', '', [rfReplaceAll]);
end;

class function TValidacionFiscal.ValidarDNI(const ADNI: string): Boolean;
var
  LDoc: string;
  LNum: Integer;
begin
  Result := False;
  LDoc   := Normalizar(ADNI);

  // Formato: 8 dígitos + 1 letra
  if Length(LDoc) <> 9 then Exit;
  if not CharInSet(LDoc[9], ['A'..'Z']) then Exit;
  if not TryStrToInt(Copy(LDoc, 1, 8), LNum) then Exit;

  Result := LDoc[9] = LetraDNI(LNum);
end;

class function TValidacionFiscal.ValidarNIE(const ANIE: string): Boolean;
var
  LDoc: string;
  LNumStr: string;
  LNum: Integer;
begin
  Result := False;
  LDoc   := Normalizar(ANIE);

  // Formato: X/Y/Z + 7 dígitos + 1 letra
  if Length(LDoc) <> 9 then Exit;
  if not CharInSet(LDoc[1], ['X', 'Y', 'Z']) then Exit;
  if not CharInSet(LDoc[9], ['A'..'Z']) then Exit;

  // Sustituir primera letra por su dígito equivalente y aplicar algoritmo DNI
  case LDoc[1] of
    'X': LNumStr := '0' + Copy(LDoc, 2, 7);
    'Y': LNumStr := '1' + Copy(LDoc, 2, 7);
    'Z': LNumStr := '2' + Copy(LDoc, 2, 7);
  else
    Exit;
  end;

  if not TryStrToInt(LNumStr, LNum) then Exit;
  Result := LDoc[9] = LetraDNI(LNum);
end;

class function TValidacionFiscal.ValidarCIF(const ACIF: string): Boolean;
var
  LDoc: string;
  LLetraOrg: Char;
  LSumaPar, LSumaImpar, LTotal, LControl: Integer;
  LDigit, I: Integer;
  LCharControl: Char;
begin
  Result := False;
  LDoc   := Normalizar(ACIF);

  // Formato: 1 letra + 7 dígitos + 1 dígito o letra
  if Length(LDoc) <> 9 then Exit;

  LLetraOrg := LDoc[1];

  // Letras de organización válidas para CIF español
  if not CharInSet(LLetraOrg,
    ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','U','V','W']) then
    Exit;

  // Las posiciones 2..8 deben ser todas numéricas
  for I := 2 to 8 do
    if not CharInSet(LDoc[I], ['0'..'9']) then Exit;

  // La posición 9 debe ser dígito o letra mayúscula
  if not (CharInSet(LDoc[9], ['0'..'9']) or CharInSet(LDoc[9], ['A'..'Z'])) then Exit;

  // --- Algoritmo de cálculo del dígito de control ---
  // Posiciones impares del cuerpo (1ª, 3ª, 5ª, 7ª → índices 2,4,6,8 en el string):
  //   multiplicar por 2; si resultado ≥ 10, sumar sus cifras; acumular en LSumaImpar
  // Posiciones pares del cuerpo (2ª, 4ª, 6ª → índices 3,5,7 en el string):
  //   sumar directamente en LSumaPar
  LSumaPar   := 0;
  LSumaImpar := 0;

  for I := 1 to 7 do
  begin
    LDigit := Ord(LDoc[I + 1]) - Ord('0');
    if (I mod 2) <> 0 then  // posición impar del cuerpo
    begin
      LDigit     := LDigit * 2;
      LSumaImpar := LSumaImpar + SumaDigitos(LDigit);
    end
    else                    // posición par del cuerpo
      LSumaPar := LSumaPar + LDigit;
  end;

  LTotal   := LSumaPar + LSumaImpar;
  LControl := (10 - (LTotal mod 10)) mod 10;

  // Control en forma de letra (tabla JABCDEFGHI)
  LCharControl := LETRAS_CTRL[LControl + 1];

  // Aceptar tanto la forma de dígito como la de letra
  if CharInSet(LDoc[9], ['0'..'9']) then
    Result := (Ord(LDoc[9]) - Ord('0')) = LControl
  else
    Result := LDoc[9] = LCharControl;
end;

class function TValidacionFiscal.ValidarDocumento(const ADoc: string;
  out ATipo: TTipoDocumento): Boolean;
var
  LDoc: string;
begin
  ATipo  := tdDesconocido;
  Result := False;

  LDoc := Normalizar(ADoc);
  if LDoc = '' then Exit;

  if CharInSet(LDoc[1], ['X', 'Y', 'Z']) then
  begin
    ATipo  := tdNIE;
    Result := ValidarNIE(LDoc);
  end
  else if CharInSet(LDoc[1], ['0'..'9']) then
  begin
    ATipo  := tdDNI;
    Result := ValidarDNI(LDoc);
  end
  else if CharInSet(LDoc[1],
    ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','U','V','W']) then
  begin
    ATipo  := tdCIF;
    Result := ValidarCIF(LDoc);
  end;
end;

class function TValidacionFiscal.TipoAsString(const ATipo: TTipoDocumento): string;
begin
  case ATipo of
    tdDNI: Result := 'DNI';
    tdNIE: Result := 'NIE';
    tdCIF: Result := 'CIF';
  else
    Result := 'Desconocido';
  end;
end;

class function TValidacionFiscal.CalcularDigitoControlSSCC(const A17Digits: string): Integer;
var
  I, LDigit, LSum, LWeight: Integer;
begin
  LSum := 0;
  for I := 17 downto 1 do
  begin
    LDigit := StrToIntDef(A17Digits[I], 0);
    if ((18 - I) mod 2) <> 0 then
      LWeight := 3
    else
      LWeight := 1;
    LSum := LSum + (LDigit * LWeight);
  end;
  
  Result := (10 - (LSum mod 10)) mod 10;
end;

class function TValidacionFiscal.GenerarSSCC18(const APrefijoGS1Empresa: string; ACorrelativoPale: Int64; ADigitoExtension: Integer): string;
var
  LBase17: string;
  LNumDigitsSecuencia: Integer;
  LCheckDigit: Integer;
  LCleanPrefix: string;
begin
  LCleanPrefix := Trim(APrefijoGS1Empresa);
  LNumDigitsSecuencia := 17 - 1 - Length(LCleanPrefix);
  if LNumDigitsSecuencia < 1 then
    LNumDigitsSecuencia := 6;

  LBase17 := IntToStr(ADigitoExtension) + 
             LCleanPrefix + 
             Format('%.*d', [LNumDigitsSecuencia, ACorrelativoPale]);
             
  LCheckDigit := CalcularDigitoControlSSCC(LBase17);
  Result := LBase17 + IntToStr(LCheckDigit);
end;

end.
