unit uSifXmlBuilder;

interface

uses
  System.SysUtils, System.Classes, uVeriFactuTypes;

type
  /// <summary>Datos de identificacion del emisor</summary>
  TSifEmisorInfo = record
    Nif: string;
    RazonSocial: string;
  end;

  /// <summary>Datos del destinatario / cliente</summary>
  TSifDestinatarioInfo = record
    Nif: string;
    RazonSocial: string;
    CodigoPais: string;
    TipoIdExtranjero: string;
    NumIdExtranjero: string;
    EsExtranjero: Boolean;
  end;

  /// <summary>Datos del software y desarrollador SIF</summary>
  TSifSistemaInfo = record
    NombreSistema: string;
    VersionSistema: string;
    NifDesarrollador: string;
    RazonSocialDesarrollador: string;
    InstalacionId: string;
  end;

  /// <summary>Datos de la factura para la construccion del XML SIF</summary>
  TSifFacturaData = record
    Emisor: TSifEmisorInfo;
    Destinatario: TSifDestinatarioInfo;
    Sistema: TSifSistemaInfo;
    NumSerieFactura: string;
    FechaExpedicion: string; // DD-MM-YYYY
    TipoFactura: TSifTipoFactura;
    ClaveRegimen: string;    // '01'
    DescripcionOperacion: string;
    Desgloses: TArray<TSifDesgloseIva>;
    CuotaTotal: Double;
    ImporteTotal: Double;
    HuellaHash: string;
    HuellaAnterior: string;
    EsPrimerRegistro: Boolean;
    FechaHoraGenISO: string;
  end;

  /// <summary>
  /// Generador de mensajes XML conforme al esquema XSD de la AEAT
  /// para el suministro de registros de facturacion (VERI*FACTU y SIF).
  /// </summary>
  TSifXmlBuilder = class
  private
    class function FormatFloatXml(AValue: Double): string; static;
    class function EscapeXml(const AText: string): string; static;
  public
    /// <summary>Construye el XML completo de Alta de Facturacion (RegistroAlta)</summary>
    class function GenerarXmlRegistroAlta(const AData: TSifFacturaData): string; static;

    /// <summary>Construye el Sobre SOAP completo para el envio telematico a la AEAT</summary>
    class function GenerarSobreSoapAlta(const AData: TSifFacturaData): string; static;
  end;

implementation

class function TSifXmlBuilder.FormatFloatXml(AValue: Double): string;
var
  LFormatSettings: TFormatSettings;
begin
  LFormatSettings := TFormatSettings.Create('es-ES');
  LFormatSettings.DecimalSeparator := '.';
  Result := FloatToStrF(AValue, ffFixed, 12, 2, LFormatSettings);
end;

class function TSifXmlBuilder.EscapeXml(const AText: string): string;
begin
  Result := AText;
  Result := StringReplace(Result, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
  Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
  Result := StringReplace(Result, '"', '&quot;', [rfReplaceAll]);
  Result := StringReplace(Result, '''', '&apos;', [rfReplaceAll]);
end;

class function TSifXmlBuilder.GenerarXmlRegistroAlta(const AData: TSifFacturaData): string;
var
  LXml: TStringBuilder;
  I: Integer;
begin
  LXml := TStringBuilder.Create;
  try
    LXml.AppendLine('    <sif:RegistroAlta>');
    LXml.AppendLine('      <sif:IDVersion>1.0</sif:IDVersion>');
    LXml.AppendLine('      <sif:IDFactura>');
    LXml.AppendFormat('        <sif:IDEmisorFactura>%s</sif:IDEmisorFactura>%s', [EscapeXml(AData.Emisor.Nif), sLineBreak]);
    LXml.AppendFormat('        <sif:NumSerieFactura>%s</sif:NumSerieFactura>%s', [EscapeXml(AData.NumSerieFactura), sLineBreak]);
    LXml.AppendFormat('        <sif:FechaExpedicionFactura>%s</sif:FechaExpedicionFactura>%s', [AData.FechaExpedicion, sLineBreak]);
    LXml.AppendLine('      </sif:IDFactura>');
    LXml.AppendFormat('      <sif:NombreRazonEmisor>%s</sif:NombreRazonEmisor>%s', [EscapeXml(AData.Emisor.RazonSocial), sLineBreak]);
    LXml.AppendFormat('      <sif:TipoFactura>%s</sif:TipoFactura>%s', [TipoFacturaToString(AData.TipoFactura), sLineBreak]);
    LXml.AppendFormat('      <sif:DescripcionOperacion>%s</sif:DescripcionOperacion>%s', [EscapeXml(AData.DescripcionOperacion), sLineBreak]);

    // Destinatarios (Si no es simplificada pura)
    if (AData.TipoFactura <> tfF2) and (Trim(AData.Destinatario.Nif) <> '') then
    begin
      LXml.AppendLine('      <sif:Destinatarios>');
      LXml.AppendLine('        <sif:IDDestinatario>');
      LXml.AppendFormat('          <sif:NombreRazon>%s</sif:NombreRazon>%s', [EscapeXml(AData.Destinatario.RazonSocial), sLineBreak]);
      if AData.Destinatario.EsExtranjero then
      begin
        LXml.AppendLine('          <sif:IDOtro>');
        LXml.AppendFormat('            <sif:CodigoPais>%s</sif:CodigoPais>%s', [AData.Destinatario.CodigoPais, sLineBreak]);
        LXml.AppendFormat('            <sif:IDType>%s</sif:IDType>%s', [AData.Destinatario.TipoIdExtranjero, sLineBreak]);
        LXml.AppendFormat('            <sif:ID>%s</sif:ID>%s', [EscapeXml(AData.Destinatario.NumIdExtranjero), sLineBreak]);
        LXml.AppendLine('          </sif:IDOtro>');
      end
      else
      begin
        LXml.AppendFormat('          <sif:NIF>%s</sif:NIF>%s', [EscapeXml(AData.Destinatario.Nif), sLineBreak]);
      end;
      LXml.AppendLine('        </sif:IDDestinatario>');
      LXml.AppendLine('      </sif:Destinatarios>');
    end;

    // Desglose de Bases e Impuestos
    LXml.AppendLine('      <sif:Desglose>');
    for I := 0 to High(AData.Desgloses) do
    begin
      LXml.AppendLine('        <sif:DetalleDesglose>');
      LXml.AppendLine('          <sif:Impuesto>01</sif:Impuesto>');
      LXml.AppendFormat('          <sif:ClaveRegimen>%s</sif:ClaveRegimen>%s', [AData.ClaveRegimen, sLineBreak]);
      LXml.AppendFormat('          <sif:TipoImpositivo>%s</sif:TipoImpositivo>%s', [FormatFloatXml(AData.Desgloses[I].TipoIva), sLineBreak]);
      LXml.AppendFormat('          <sif:BaseImponibleOimporteNoSujeto>%s</sif:BaseImponibleOimporteNoSujeto>%s', [FormatFloatXml(AData.Desgloses[I].BaseImponible), sLineBreak]);
      LXml.AppendFormat('          <sif:CuotaRepercutida>%s</sif:CuotaRepercutida>%s', [FormatFloatXml(AData.Desgloses[I].CuotaIva), sLineBreak]);

      if AData.Desgloses[I].TipoRecargoEq > 0 then
      begin
        LXml.AppendFormat('          <sif:TipoRecargoEquivalencia>%s</sif:TipoRecargoEquivalencia>%s', [FormatFloatXml(AData.Desgloses[I].TipoRecargoEq), sLineBreak]);
        LXml.AppendFormat('          <sif:CuotaRecargoEquivalencia>%s</sif:CuotaRecargoEquivalencia>%s', [FormatFloatXml(AData.Desgloses[I].CuotaRecargoEq), sLineBreak]);
      end;
      LXml.AppendLine('        </sif:DetalleDesglose>');
    end;
    LXml.AppendLine('      </sif:Desglose>');

    // Totales
    LXml.AppendFormat('      <sif:CuotaTotal>%s</sif:CuotaTotal>%s', [FormatFloatXml(AData.CuotaTotal), sLineBreak]);
    LXml.AppendFormat('      <sif:ImporteTotal>%s</sif:ImporteTotal>%s', [FormatFloatXml(AData.ImporteTotal), sLineBreak]);

    // Encadenamiento
    LXml.AppendLine('      <sif:Encadenamiento>');
    if AData.EsPrimerRegistro or (Trim(AData.HuellaAnterior) = '') then
    begin
      LXml.AppendLine('        <sif:PrimerRegistro>S</sif:PrimerRegistro>');
    end
    else
    begin
      LXml.AppendLine('        <sif:RegistroAnterior>');
      LXml.AppendFormat('          <sif:IDEmisorFactura>%s</sif:IDEmisorFactura>%s', [EscapeXml(AData.Emisor.Nif), sLineBreak]);
      LXml.AppendFormat('          <sif:NumSerieFactura>%s</sif:NumSerieFactura>%s', [EscapeXml(AData.NumSerieFactura), sLineBreak]);
      LXml.AppendFormat('          <sif:FechaExpedicionFactura>%s</sif:FechaExpedicionFactura>%s', [AData.FechaExpedicion, sLineBreak]);
      LXml.AppendFormat('          <sif:Huella>%s</sif:Huella>%s', [AData.HuellaAnterior, sLineBreak]);
      LXml.AppendLine('        </sif:RegistroAnterior>');
    end;
    LXml.AppendLine('      </sif:Encadenamiento>');

    // Sistema Informatico (Datos del software)
    LXml.AppendLine('      <sif:SistemaInformatico>');
    LXml.AppendFormat('        <sif:NombreRazon>%s</sif:NombreRazon>%s', [EscapeXml(AData.Sistema.RazonSocialDesarrollador), sLineBreak]);
    LXml.AppendFormat('        <sif:NIF>%s</sif:NIF>%s', [EscapeXml(AData.Sistema.NifDesarrollador), sLineBreak]);
    LXml.AppendFormat('        <sif:NombreSistemaInformatico>%s</sif:NombreSistemaInformatico>%s', [EscapeXml(AData.Sistema.NombreSistema), sLineBreak]);
    LXml.AppendFormat('        <sif:IdSistemaInformatico>%s</sif:IdSistemaInformatico>%s', [EscapeXml(AData.Sistema.InstalacionId), sLineBreak]);
    LXml.AppendFormat('        <sif:Version>%s</sif:Version>%s', [EscapeXml(AData.Sistema.VersionSistema), sLineBreak]);
    LXml.AppendFormat('        <sif:NumeroInstalacion>%s</sif:NumeroInstalacion>%s', [EscapeXml(AData.Sistema.InstalacionId), sLineBreak]);
    LXml.AppendLine('        <sif:TipoUsoPosibleSoloVerifactu>N</sif:TipoUsoPosibleSoloVerifactu>');
    LXml.AppendLine('        <sif:TipoUsoPosibleMultiOT>N</sif:TipoUsoPosibleMultiOT>');
    LXml.AppendLine('        <sif:IndicadorMultiplesOT>N</sif:IndicadorMultiplesOT>');
    LXml.AppendLine('      </sif:SistemaInformatico>');

    // Timestamp y Huella
    LXml.AppendFormat('      <sif:FechaHoraHusoGenRegistro>%s</sif:FechaHoraHusoGenRegistro>%s', [AData.FechaHoraGenISO, sLineBreak]);
    LXml.AppendLine('      <sif:TipoHuella>01</sif:TipoHuella>');
    LXml.AppendFormat('      <sif:Huella>%s</sif:Huella>%s', [AData.HuellaHash, sLineBreak]);

    LXml.AppendLine('    </sif:RegistroAlta>');
    Result := LXml.ToString;
  finally
    LXml.Free;
  end;
end;

class function TSifXmlBuilder.GenerarSobreSoapAlta(const AData: TSifFacturaData): string;
var
  LSoap: TStringBuilder;
  LXmlRegistro: string;
begin
  LSoap := TStringBuilder.Create;
  try
    LXmlRegistro := GenerarXmlRegistroAlta(AData);

    LSoap.AppendLine('<?xml version="1.0" encoding="utf-8"?>');
    LSoap.AppendLine('<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sif="https://www.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tiva/sif/ws/SuministroLR.xsd">');
    LSoap.AppendLine('  <soapenv:Header/>');
    LSoap.AppendLine('  <soapenv:Body>');
    LSoap.AppendLine('    <sif:RegFactuSistemaFacturacion>');
    LSoap.AppendLine('      <sif:Cabecera>');
    LSoap.AppendLine('        <sif:ObligadoEmision>');
    LSoap.AppendFormat('          <sif:NombreRazon>%s</sif:NombreRazon>%s', [EscapeXml(AData.Emisor.RazonSocial), sLineBreak]);
    LSoap.AppendFormat('          <sif:NIF>%s</sif:NIF>%s', [EscapeXml(AData.Emisor.Nif), sLineBreak]);
    LSoap.AppendLine('        </sif:ObligadoEmision>');
    LSoap.AppendLine('      </sif:Cabecera>');
    LSoap.AppendLine(LXmlRegistro);
    LSoap.AppendLine('    </sif:RegFactuSistemaFacturacion>');
    LSoap.AppendLine('  </soapenv:Body>');
    LSoap.AppendLine('</soapenv:Envelope>');

    Result := LSoap.ToString;
  finally
    LSoap.Free;
  end;
end;

end.
