unit uEdicomProcessor;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils, System.Generics.Collections,
  System.JSON, System.NetEncoding, Data.DB, FireDAC.Comp.Client, Xml.xmldom, Xml.XMLIntf, Xml.XMLDoc, System.StrUtils, System.DateUtils;

type
  TDiscountRec = record
    Tipo: string;
    CodigoMotivo: string;
    Porcentaje: Double;
    Importe: Double;
  end;

  TOrderLineRec = record
    Posicion: Integer;
    Ean: string;
    Descripcion: string;
    Cantidad: Double;
    Bultos: Integer;
    CantidadBulto: Double;
    Precio: Double;
    Total: Double;
    Descuentos: TArray<TDiscountRec>;
  end;

  TEdicomProcessor = class
  private
    class function FindClientByEan(AConnection: TFDConnection; const AEan: string; var AClienteId, ADestinoId: Integer): Boolean;
    class function FindDestinoByEan(AConnection: TFDConnection; AClienteId: Integer; const AEan: string; var ADestinoId: Integer; var AResolvedClienteId: Integer): Boolean;
    class function FindDestinoSolicitanteByEan(AConnection: TFDConnection; AClienteId: Integer; const AEan: string; var ADestinoId: Integer; var AResolvedClienteId: Integer): Boolean;
    class function FindArticleByEan(AConnection: TFDConnection; const AEan: string; var AArticuloId: Integer; var ADesc: string): Boolean;
    class function ImportSingleOrderFromXmlBlock(const AXmlBlock: string; AEmpresaId: Integer; var AErrorMsg: string): Integer;
  public
    class function ExtractXmlBlocksFromContent(const AContent: string; out AMessageIds: TArray<string>): TArray<string>;
    class function ImportOrders(const AFilePath: string; AEmpresaId: Integer; var AErrorMsg: string): Integer;
    class function ImportOrdersFromEdifactContent(const AEdifactContent: string; AEmpresaId: Integer; var AErrorMsg: string): Integer;
    class function ImportOrdersFromXmlContent(const AXmlContent: string; AEmpresaId: Integer; var AErrorMsg: string): Integer; overload;
    class function ImportOrdersFromXmlContent(const AXmlContent: string; AEmpresaId: Integer; var AErrorMsg: string; out AMessageIds: TArray<string>): Integer; overload;
    class function ImportOrderFromJson(const AJson: string; AEmpresaId: Integer; var AErrorMsg: string): Boolean;
    class function ExportInvoice(const AInvoiceId: string; const AOutputPath: string; var AErrorMsg: string): Boolean;
  end;

implementation

uses
  dmg_Main, uEdicomInvoiceExporter;

class function TEdicomProcessor.FindDestinoByEan(AConnection: TFDConnection; AClienteId: Integer; const AEan: string; var ADestinoId: Integer; var AResolvedClienteId: Integer): Boolean;
var
  Qry: TFDQuery;
  LCleanEan, LEanPattern: string;
begin
  Result := False;
  ADestinoId := 0;
  AResolvedClienteId := 0;

  LCleanEan := Trim(AEan);
  if LCleanEan = '' then Exit;

  LEanPattern := '%' + LCleanEan + '%';

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := AConnection;
    // Buscar destino específico del cliente coincidente por ean_receptor, ean_cliente, ean_emisor o ean_facturacion
    Qry.SQL.Text :=
      'SELECT id, id_cliente, ean_receptor, ean_cliente, ean_emisor, ean_facturacion FROM ge_clientes_destinos ' +
      'WHERE (id_cliente = :cli OR :cli = 0) ' +
      '  AND (ean_receptor = :ean OR ean_cliente = :ean OR ean_emisor = :ean OR ean_facturacion = :ean ' +
      '       OR ean_receptor LIKE :ean_like OR ean_cliente LIKE :ean_like OR ean_emisor LIKE :ean_like OR ean_facturacion LIKE :ean_like) ' +
      '  AND activo = 1 ' +
      'ORDER BY (IF(ean_receptor = :ean, 0, IF(ean_cliente = :ean, 1, IF(ean_emisor = :ean, 2, 3)))), (IF(id_cliente = :cli, 0, 1)) LIMIT 1';
    Qry.ParamByName('cli').AsInteger := AClienteId;
    Qry.ParamByName('ean').AsString := LCleanEan;
    Qry.ParamByName('ean_like').AsString := LEanPattern;
    Qry.Open;

    if not Qry.IsEmpty then
    begin
      ADestinoId := Qry.FieldByName('id').AsInteger;
      AResolvedClienteId := Qry.FieldByName('id_cliente').AsInteger;
      Result := True;
    end;
  finally
    Qry.Free;
  end;
end;

class function TEdicomProcessor.FindDestinoSolicitanteByEan(AConnection: TFDConnection; AClienteId: Integer; const AEan: string; var ADestinoId: Integer; var AResolvedClienteId: Integer): Boolean;
var
  Qry: TFDQuery;
  LCleanEan, LEanPattern: string;
begin
  Result := False;
  ADestinoId := 0;
  AResolvedClienteId := 0;

  LCleanEan := Trim(AEan);
  if LCleanEan = '' then Exit;

  LEanPattern := '%' + LCleanEan + '%';

  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := AConnection;
    // Buscar destino de solicitante / emisor del pedido (priorizando ean_emisor y ean_cliente)
    Qry.SQL.Text :=
      'SELECT id, id_cliente, ean_emisor, ean_cliente, ean_facturacion, ean_receptor FROM ge_clientes_destinos ' +
      'WHERE (id_cliente = :cli OR :cli = 0) ' +
      '  AND (ean_emisor = :ean OR ean_cliente = :ean OR ean_facturacion = :ean OR ean_receptor = :ean ' +
      '       OR ean_emisor LIKE :ean_like OR ean_cliente LIKE :ean_like OR ean_facturacion LIKE :ean_like OR ean_receptor LIKE :ean_like) ' +
      '  AND activo = 1 ' +
      'ORDER BY (IF(ean_emisor = :ean, 0, IF(ean_cliente = :ean, 1, IF(ean_facturacion = :ean, 2, 3)))), (IF(id_cliente = :cli, 0, 1)) LIMIT 1';
    Qry.ParamByName('cli').AsInteger := AClienteId;
    Qry.ParamByName('ean').AsString := LCleanEan;
    Qry.ParamByName('ean_like').AsString := LEanPattern;
    Qry.Open;

    if not Qry.IsEmpty then
    begin
      ADestinoId := Qry.FieldByName('id').AsInteger;
      AResolvedClienteId := Qry.FieldByName('id_cliente').AsInteger;
      Result := True;
    end;
  finally
    Qry.Free;
  end;
end;

class function TEdicomProcessor.FindClientByEan(AConnection: TFDConnection; const AEan: string; var AClienteId, ADestinoId: Integer): Boolean;
var
  Qry: TFDQuery;
  LCleanEan, LEanPattern: string;
begin
  Result := False;
  AClienteId := 0;
  ADestinoId := 0;
  
  LCleanEan := Trim(AEan);
  if LCleanEan = '' then Exit;
  
  LEanPattern := '%' + LCleanEan + '%';
  
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := AConnection;
    // 1. Buscar en ge_clientes por coincidencia exacta o subcadena en ean
    Qry.SQL.Text := 
      'SELECT id FROM ge_clientes ' +
      'WHERE (ean = :ean OR ean LIKE :ean_like) AND activo = 1 ' +
      'ORDER BY (IF(ean = :ean, 0, 1)) LIMIT 1';
    Qry.ParamByName('ean').AsString := LCleanEan;
    Qry.ParamByName('ean_like').AsString := LEanPattern;
    Qry.Open;
    
    if not Qry.IsEmpty then
    begin
      AClienteId := Qry.FieldByName('id').AsInteger;
      Result := True;
      Exit;
    end;
    
    // 2. Si no está en ge_clientes, buscar si coincide con ean_cliente, ean_emisor o ean_facturacion de algún destino
    Qry.Close;
    Qry.SQL.Text := 
      'SELECT id_cliente, id FROM ge_clientes_destinos ' +
      'WHERE (ean_cliente = :ean OR ean_emisor = :ean OR ean_facturacion = :ean ' +
      '       OR ean_cliente LIKE :ean_like OR ean_emisor LIKE :ean_like OR ean_facturacion LIKE :ean_like) AND activo = 1 ' +
      'ORDER BY (IF(ean_cliente = :ean, 0, IF(ean_emisor = :ean, 1, 2))) LIMIT 1';
    Qry.ParamByName('ean').AsString := LCleanEan;
    Qry.ParamByName('ean_like').AsString := LEanPattern;
    Qry.Open;

    if not Qry.IsEmpty then
    begin
      AClienteId := Qry.FieldByName('id_cliente').AsInteger;
      ADestinoId := Qry.FieldByName('id').AsInteger;
      Result := True;
      Exit;
    end;

    // 3. Buscar si coincide directamente como ean_receptor de un destino específico
    Qry.Close;
    Qry.SQL.Text := 
      'SELECT id_cliente, id FROM ge_clientes_destinos ' +
      'WHERE (ean_receptor = :ean OR ean_receptor LIKE :ean_like) AND activo = 1 ' +
      'ORDER BY (IF(ean_receptor = :ean, 0, 1)) LIMIT 1';
    Qry.ParamByName('ean').AsString := LCleanEan;
    Qry.ParamByName('ean_like').AsString := LEanPattern;
    Qry.Open;

    if not Qry.IsEmpty then
    begin
      AClienteId := Qry.FieldByName('id_cliente').AsInteger;
      ADestinoId := Qry.FieldByName('id').AsInteger;
      Result := True;
    end;
  finally
    Qry.Free;
  end;
end;

class function TEdicomProcessor.FindArticleByEan(AConnection: TFDConnection; const AEan: string; var AArticuloId: Integer; var ADesc: string): Boolean;
var
  Qry: TFDQuery;
begin
  Result := False;
  AArticuloId := 0;
  ADesc := '';
  
  if Trim(AEan) = '' then Exit;
  
  Qry := TFDQuery.Create(nil);
  try
    Qry.Connection := AConnection;
    Qry.SQL.Text := 'SELECT id, descripcion FROM ge_articulos WHERE ean = :ean LIMIT 1';
    Qry.ParamByName('ean').AsString := AEan;
    Qry.Open;
    
    if not Qry.IsEmpty then
    begin
      AArticuloId := Qry.FieldByName('id').AsInteger;
      ADesc := Qry.FieldByName('descripcion').AsString;
      Result := True;
    end;
  finally
    Qry.Free;
  end;
end;

function SplitEdifactSegments(const AText: string; ASegTerm, AEscChar: Char): TArray<string>;
var
  LList: TList<string>;
  I: Integer;
  LSeg: string;
  LChar: Char;
  LEscaped: Boolean;
begin
  LList := TList<string>.Create;
  try
    LSeg := '';
    LEscaped := False;
    for I := 1 to Length(AText) do
    begin
      LChar := AText[I];
      if (LChar = #13) or (LChar = #10) then
        Continue;
      if LEscaped then
      begin
        LSeg := LSeg + LChar;
        LEscaped := False;
      end
      else if LChar = AEscChar then
      begin
        LEscaped := True;
      end
      else if LChar = ASegTerm then
      begin
        if Trim(LSeg) <> '' then
          LList.Add(Trim(LSeg));
        LSeg := '';
      end
      else
        LSeg := LSeg + LChar;
    end;
    if Trim(LSeg) <> '' then
      LList.Add(Trim(LSeg));
    Result := LList.ToArray;
  finally
    LList.Free;
  end;
end;

function SplitEdifactElements(const ASeg: string; AElemSep, AEscChar: Char): TArray<string>;
var
  LList: TList<string>;
  I: Integer;
  LElem: string;
  LChar: Char;
  LEscaped: Boolean;
begin
  LList := TList<string>.Create;
  try
    LElem := '';
    LEscaped := False;
    for I := 1 to Length(ASeg) do
    begin
      LChar := ASeg[I];
      if LEscaped then
      begin
        LElem := LElem + LChar;
        LEscaped := False;
      end
      else if LChar = AEscChar then
      begin
        LEscaped := True;
      end
      else if LChar = AElemSep then
      begin
        LList.Add(LElem);
        LElem := '';
      end
      else
        LElem := LElem + LChar;
    end;
    LList.Add(LElem);
    Result := LList.ToArray;
  finally
    LList.Free;
  end;
end;

function SplitEdifactComponents(const AElem: string; ACompSep, AEscChar: Char): TArray<string>;
var
  LList: TList<string>;
  I: Integer;
  LComp: string;
  LChar: Char;
  LEscaped: Boolean;
begin
  LList := TList<string>.Create;
  try
    LComp := '';
    LEscaped := False;
    for I := 1 to Length(AElem) do
    begin
      LChar := AElem[I];
      if LEscaped then
      begin
        LComp := LComp + LChar;
        LEscaped := False;
      end
      else if LChar = AEscChar then
      begin
        LEscaped := True;
      end
      else if LChar = ACompSep then
      begin
        LList.Add(LComp);
        LComp := '';
      end
      else
        LComp := LComp + LChar;
    end;
    LList.Add(LComp);
    Result := LList.ToArray;
  finally
    LList.Free;
  end;
end;

function ParseEdifactDate(const AVal, AFmt: string): TDateTime;
var
  LStr: string;
  LY, LM, LD: Word;
begin
  Result := Date;
  LStr := Trim(AVal);
  if Length(LStr) >= 8 then
  begin
    LY := StrToIntDef(Copy(LStr, 1, 4), YearOf(Date));
    LM := StrToIntDef(Copy(LStr, 5, 2), MonthOf(Date));
    LD := StrToIntDef(Copy(LStr, 7, 2), DayOf(Date));
    if (LY > 1900) and (LM in [1..12]) and (LD in [1..31]) then
      Result := EncodeDate(LY, LM, LD);
  end;
end;

class function TEdicomProcessor.ImportOrdersFromEdifactContent(const AEdifactContent: string; AEmpresaId: Integer; var AErrorMsg: string): Integer;
var
  LSegments: TArray<string>;
  LSeg, LTag: string;
  LElems, LComps: TArray<string>;
  I, J: Integer;
  LSegTerm, LElemSep, LCompSep, LEscChar: Char;
  
  // Variables del pedido activo
  LInOrder: Boolean;
  LNumPedido: string;
  LFechaPedido: TDateTime;
  LFechaEntrega: TDateTime;
  LFechaEntregaLimite: TDateTime;
  LHasEntregaLimite: Boolean;
  LEdiOrderType: string;
  LEdiIdenticket: string;
  LEdiIncoterm: string;
  LClienteId: Integer;
  LDestinoId: Integer;
  LCentro, LDepartamento: string;
  LTotalCount: Integer;
  LOrderLines: TList<TOrderLineRec>;
  LCurLineRec: TOrderLineRec;
  LHasCurLine: Boolean;
  LHeaderDiscounts: TArray<TDiscountRec>;
  LPartiesEans: TList<TPair<string, string>>;
  FS: TFormatSettings;

  procedure SaveActiveEdifactOrder;
  var
    Qry: TFDQuery;
    LOrderId: Integer;
    LBase, LIva, LTotal: Double;
    LRec: TOrderLineRec;
    LDisc: TDiscountRec;
    LLineId: Integer;
    LEanInfo: string;
    LPair: TPair<string, string>;
    LArtId: Integer;
    LArtDesc: string;
    LTempId, LTempDest, LDestinoCliId: Integer;
  begin
    if LNumPedido = '' then Exit;

    // 1. Extraer identificadores según rol en PARTIES
    var LEanDeliveryPoint: string := '';
    var LEanBuyer: string := '';
    var LEanIssuer: string := '';

    for LPair in LPartiesEans do
    begin
      if (SameText(LPair.Key, 'DP') or SameText(LPair.Key, 'UD') or SameText(LPair.Key, 'CN')) and (LEanDeliveryPoint = '') then
        LEanDeliveryPoint := Trim(LPair.Value)
      else if (SameText(LPair.Key, 'BY') or SameText(LPair.Key, 'IV') or SameText(LPair.Key, 'MR')) and (LEanBuyer = '') then
        LEanBuyer := Trim(LPair.Value)
      else if SameText(LPair.Key, 'MS') and (LEanIssuer = '') then
        LEanIssuer := Trim(LPair.Value);
    end;

    // 2. Resolver Cliente principal (priorizando BY / Comprador)
    if LEanBuyer <> '' then
    begin
      if FindClientByEan(dmgMain.dbConn, LEanBuyer, LTempId, LTempDest) then
        LClienteId := LTempId;
    end;

    // 3. Resolver Destino específico del cliente
    // 3.1. Intentar por Centro / Departamento si vienen en RFF+ZZZ / RFF+API
    if (LCentro <> '') and (LDepartamento <> '') then
    begin
      var LQryDest := TFDQuery.Create(nil);
      try
        LQryDest.Connection := dmgMain.dbConn;
        LQryDest.SQL.Text :=
          'SELECT id, id_cliente FROM ge_clientes_destinos ' +
          'WHERE (id_cliente = :cli OR :cli = 0) ' +
          '  AND (LPAD(TRIM(centro), 4, ''0'') = LPAD(:centro, 4, ''0'') OR TRIM(centro) = :centro) ' +
          '  AND (LPAD(TRIM(departamento), 4, ''0'') = LPAD(:depto, 4, ''0'') OR TRIM(departamento) = :depto) ' +
          '  AND activo = 1 LIMIT 1';
        LQryDest.ParamByName('cli').AsInteger := LClienteId;
        LQryDest.ParamByName('centro').AsString := LCentro;
        LQryDest.ParamByName('depto').AsString := LDepartamento;
        LQryDest.Open;
        if not LQryDest.IsEmpty then
        begin
          LDestinoId := LQryDest.FieldByName('id').AsInteger;
          if LClienteId = 0 then
            LClienteId := LQryDest.FieldByName('id_cliente').AsInteger;
        end;
      finally
        LQryDest.Free;
      end;
    end;

    // 3.2. Si aún no tenemos destino, buscar por punto de entrega (DP / UD / CN)
    if (LDestinoId = 0) and (LEanDeliveryPoint <> '') then
    begin
      if FindDestinoByEan(dmgMain.dbConn, LClienteId, LEanDeliveryPoint, LDestinoId, LDestinoCliId) then
      begin
        if LClienteId = 0 then
          LClienteId := LDestinoCliId;
      end;
    end;

    // 3.3. Resolver Destino Solicitante (cliente_solicitante_id)
    var LSolicitanteId: Integer := 0;
    var LSolicitanteCliId: Integer := 0;
    if (LEanBuyer <> '') or (LEanIssuer <> '') then
    begin
      var LEanToSearch := LEanBuyer;
      if LEanToSearch = '' then LEanToSearch := LEanIssuer;
      FindDestinoSolicitanteByEan(dmgMain.dbConn, LClienteId, LEanToSearch, LSolicitanteId, LSolicitanteCliId);
    end;
    if LSolicitanteId = 0 then
      LSolicitanteId := LDestinoId;

    // 4. Si aún no tenemos cliente, buscar secuencialmente en el resto de qualificadores
    if LClienteId = 0 then
    begin
      for var LPriority in TArray<string>.Create('BY', 'IV', 'MR', 'MS', 'DP', 'UD', 'SU') do
      begin
        if LClienteId > 0 then Break;
        for LPair in LPartiesEans do
        begin
          if SameText(LPair.Key, LPriority) and (LPair.Value <> '') then
          begin
            if FindClientByEan(dmgMain.dbConn, LPair.Value, LTempId, LTempDest) then
            begin
              LClienteId := LTempId;
              if (LDestinoId = 0) and (LTempDest > 0) then
                LDestinoId := LTempDest;
              Break;
            end;
          end;
        end;
      end;
    end;

    if LClienteId = 0 then
    begin
      LEanInfo := '';
      for LPair in LPartiesEans do
        LEanInfo := LEanInfo + Format('  [%s] %s', [LPair.Key, LPair.Value]) + sLineBreak;
      if LEanInfo = '' then
        LEanInfo := '  (ningún segmento NAD encontrado en el archivo EDIFACT)';
      AErrorMsg := Format(
        'No se encontró ningún cliente en la base de datos que coincida con los códigos EAN del pedido %s.' + sLineBreak +
        'Códigos EAN intentados:' + sLineBreak + '%s' +
        'Registre primero el EAN en la ficha del cliente (ge_clientes.ean) o en su destino (ge_clientes_destinos.ean_cliente, ean_receptor o ean_emisor).',
        [LNumPedido, LEanInfo]);
      Exit;
    end;

    if LOrderLines.Count = 0 then
    begin
      AErrorMsg := Format('El pedido %s no contiene líneas de artículos válidas (segmentos LIN con EAN).', [LNumPedido]);
      Exit;
    end;

    // Fallback: si alguna línea viene sin precio (o 0) y el artículo está en BD, usar su precio por defecto (pvp1)
    for var LIdx := 0 to LOrderLines.Count - 1 do
    begin
      var LLineItem := LOrderLines[LIdx];
      if (LLineItem.Precio <= 0.0) and (LLineItem.Ean <> '') then
      begin
        var LTempArtId: Integer := 0;
        var LTempArtDesc: string := '';
        if FindArticleByEan(dmgMain.dbConn, LLineItem.Ean, LTempArtId, LTempArtDesc) and (LTempArtId > 0) then
        begin
          var QryPrice := TFDQuery.Create(nil);
          try
            QryPrice.Connection := dmgMain.dbConn;
            QryPrice.SQL.Text := 'SELECT pvp1 FROM ge_articulos WHERE id = :id LIMIT 1';
            QryPrice.ParamByName('id').AsInteger := LTempArtId;
            QryPrice.Open;
            if not QryPrice.IsEmpty and (QryPrice.Fields[0].AsFloat > 0) then
            begin
              LLineItem.Precio := QryPrice.Fields[0].AsFloat;
              LLineItem.Total := LLineItem.Cantidad * LLineItem.Precio;
              LOrderLines[LIdx] := LLineItem;
            end;
          finally
            QryPrice.Free;
          end;
        end;
      end;
    end;

    LBase := 0;
    for LRec in LOrderLines do
      LBase := LBase + LRec.Total;
    LIva := LBase * 0.10;
    LTotal := LBase + LIva;

    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text :=
        'INSERT INTO ge_pedidos (empresa_id, cliente_id, cliente_solicitante_id, clientedestino_id, numero_pedido, ' +
        '  fecha_pedido, fecha_entrega, fecha_entrega_limite, estado, base_imponible, total, iva, observaciones, ' +
        '  edi_order_type, edi_identicket, edi_incoterm, id_user_creator, id_user_update) ' +
        'VALUES (:empresa_id, :cliente_id, :cliente_solicitante_id, :clientedestino_id, :numero_pedido, ' +
        '  :fecha_pedido, :fecha_entrega, :fecha_entrega_limite, ''PENDIENTE'', :base_imponible, :total, :iva, :obs, ' +
        '  :edi_order_type, :edi_identicket, :edi_incoterm, 1, 1)';
      Qry.ParamByName('empresa_id').AsInteger := AEmpresaId;
      Qry.ParamByName('cliente_id').AsInteger := LClienteId;
      Qry.ParamByName('cliente_solicitante_id').DataType := ftInteger;
      if LSolicitanteId > 0 then
        Qry.ParamByName('cliente_solicitante_id').AsInteger := LSolicitanteId
      else
        Qry.ParamByName('cliente_solicitante_id').Clear;
      Qry.ParamByName('clientedestino_id').DataType := ftInteger;
      if LDestinoId > 0 then
        Qry.ParamByName('clientedestino_id').AsInteger := LDestinoId
      else
        Qry.ParamByName('clientedestino_id').Clear;
      Qry.ParamByName('numero_pedido').AsString := LNumPedido;
      Qry.ParamByName('fecha_pedido').AsDateTime := LFechaPedido;
      Qry.ParamByName('fecha_entrega').AsDateTime := LFechaEntrega;

      Qry.ParamByName('fecha_entrega_limite').DataType := ftDate;
      if LHasEntregaLimite then
        Qry.ParamByName('fecha_entrega_limite').AsDate := LFechaEntregaLimite
      else
        Qry.ParamByName('fecha_entrega_limite').Clear;

      Qry.ParamByName('base_imponible').AsFloat := LBase;
      Qry.ParamByName('total').AsFloat := LTotal;
      Qry.ParamByName('iva').AsFloat := LIva;
      Qry.ParamByName('obs').AsString := 'Importado desde fichero EDIFACT (.EDI)';

      Qry.ParamByName('edi_order_type').DataType := ftString;
      if LEdiOrderType <> '' then Qry.ParamByName('edi_order_type').AsString := LEdiOrderType else Qry.ParamByName('edi_order_type').Clear;

      Qry.ParamByName('edi_identicket').DataType := ftString;
      if LEdiIdenticket <> '' then Qry.ParamByName('edi_identicket').AsString := LEdiIdenticket else Qry.ParamByName('edi_identicket').Clear;

      Qry.ParamByName('edi_incoterm').DataType := ftString;
      if LEdiIncoterm <> '' then Qry.ParamByName('edi_incoterm').AsString := LEdiIncoterm else Qry.ParamByName('edi_incoterm').Clear;

      Qry.ExecSQL;

      Qry.SQL.Text := 'SELECT LAST_INSERT_ID()';
      Qry.Open;
      LOrderId := Qry.Fields[0].AsInteger;
      Qry.Close;

      for LDisc in LHeaderDiscounts do
      begin
        Qry.SQL.Text :=
          'INSERT INTO ge_pedidos_descuentos_cargos (id_pedido, tipo, codigo_motivo, porcentaje, importe) ' +
          'VALUES (:id_pedido, :tipo, :codigo_motivo, :porcentaje, :importe)';
        Qry.ParamByName('id_pedido').AsInteger := LOrderId;
        Qry.ParamByName('tipo').AsString := LDisc.Tipo;
        Qry.ParamByName('codigo_motivo').AsString := LDisc.CodigoMotivo;
        Qry.ParamByName('porcentaje').AsFloat := LDisc.Porcentaje;
        Qry.ParamByName('importe').AsFloat := LDisc.Importe;
        Qry.ExecSQL;
      end;

      for LRec in LOrderLines do
      begin
        Qry.SQL.Text :=
          'INSERT INTO ge_pedidos_lineas (' +
          '  id_pedido, id_articulo, descripcion, cantidad, bultos, precio, total, ' +
          '  ean_producto, cantidad_pedida_original, numero_linea_cliente' +
          ') VALUES (' +
          '  :id_pedido, :id_articulo, :desc, :cant, :bultos, :precio, :total, ' +
          '  :ean, :cant_orig, :num_lin' +
          ')';
        Qry.ParamByName('id_pedido').AsInteger := LOrderId;

        if FindArticleByEan(dmgMain.dbConn, LRec.Ean, LArtId, LArtDesc) then
          Qry.ParamByName('id_articulo').AsInteger := LArtId
        else
          Qry.ParamByName('id_articulo').AsInteger := 1;

        var LFinalDesc := Trim(LRec.Descripcion);
        if (LFinalDesc = '') or SameText(LFinalDesc, 'CU') or (LFinalDesc = LRec.Ean) or (Length(LFinalDesc) <= 3) then
        begin
          if LArtDesc <> '' then
            LFinalDesc := LArtDesc;
        end;
        if (LFinalDesc = '') and (LArtDesc <> '') then
          LFinalDesc := LArtDesc;

        Qry.ParamByName('desc').AsString := LFinalDesc;

        Qry.ParamByName('cant').AsFloat := LRec.Cantidad;
        Qry.ParamByName('bultos').DataType := ftInteger;
        if LRec.Bultos > 0 then
          Qry.ParamByName('bultos').AsInteger := LRec.Bultos
        else
          Qry.ParamByName('bultos').Clear;

        Qry.ParamByName('precio').AsFloat := LRec.Precio;
        Qry.ParamByName('total').AsFloat := LRec.Total;
        Qry.ParamByName('ean').AsString := LRec.Ean;
        Qry.ParamByName('cant_orig').AsFloat := LRec.Cantidad;
        Qry.ParamByName('num_lin').DataType := ftInteger;
        if LRec.Posicion > 0 then
          Qry.ParamByName('num_lin').AsInteger := LRec.Posicion
        else
          Qry.ParamByName('num_lin').Clear;

        Qry.ExecSQL;

        Qry.SQL.Text := 'SELECT LAST_INSERT_ID()';
        Qry.Open;
        LLineId := Qry.Fields[0].AsInteger;
        Qry.Close;

        for LDisc in LRec.Descuentos do
        begin
          Qry.SQL.Text :=
            'INSERT INTO ge_pedidos_lineas_descuentos (id_pedido_linea, tipo, codigo_motivo, porcentaje, importe) ' +
            'VALUES (:id_pedido_linea, :tipo, :codigo_motivo, :porcentaje, :importe)';
          Qry.ParamByName('id_pedido_linea').AsInteger := LLineId;
          Qry.ParamByName('tipo').AsString := LDisc.Tipo;
          Qry.ParamByName('codigo_motivo').AsString := LDisc.CodigoMotivo;
          Qry.ParamByName('porcentaje').AsFloat := LDisc.Porcentaje;
          Qry.ParamByName('importe').AsFloat := LDisc.Importe;
          Qry.ExecSQL;
        end;
      end;

      Inc(LTotalCount);
    finally
      Qry.Free;
    end;
  end;

begin
  Result := 0;
  AErrorMsg := '';
  LTotalCount := 0;
  LInOrder := False;
  LHasCurLine := False;

  FS := TFormatSettings.Create('en-US');
  FS.DateSeparator := '-';
  FS.ShortDateFormat := 'yyyy-mm-dd';

  // Configuración de delimitadores EDIFACT por defecto
  LSegTerm := '''';
  LElemSep := '+';
  LCompSep := ':';
  LEscChar := '?';

  if Trim(AEdifactContent) = '' then
  begin
    AErrorMsg := 'El contenido EDIFACT está vacío.';
    Exit;
  end;

  // Detección de cabecera UNA personalizada
  if Trim(AEdifactContent).StartsWith('UNA') and (Length(Trim(AEdifactContent)) >= 9) then
  begin
    var LUNA := Trim(AEdifactContent);
    LCompSep := LUNA[4];
    LElemSep := LUNA[5];
    LEscChar := LUNA[7];
    LSegTerm := LUNA[9];
  end;

  LSegments := SplitEdifactSegments(AEdifactContent, LSegTerm, LEscChar);
  if Length(LSegments) = 0 then
  begin
    AErrorMsg := 'No se pudieron extraer segmentos válidos del contenido EDIFACT.';
    Exit;
  end;

  LOrderLines := TList<TOrderLineRec>.Create;
  LPartiesEans := TList<TPair<string, string>>.Create;
  try
    for I := 0 to High(LSegments) do
    begin
      LSeg := Trim(LSegments[I]);
      if LSeg = '' then Continue;

      LElems := SplitEdifactElements(LSeg, LElemSep, LEscChar);
      if Length(LElems) = 0 then Continue;

      LTag := UpperCase(Trim(LElems[0]));

      if LTag = 'UNH' then
      begin
        if LInOrder and (LOrderLines.Count > 0) then
        begin
          if LHasCurLine then
          begin
            if (LCurLineRec.CantidadBulto > 0) and (LCurLineRec.Cantidad > 0) then
              LCurLineRec.Bultos := Trunc(LCurLineRec.Cantidad / LCurLineRec.CantidadBulto);
            if LCurLineRec.Total = 0 then
              LCurLineRec.Total := LCurLineRec.Cantidad * LCurLineRec.Precio;
            LOrderLines.Add(LCurLineRec);
            LHasCurLine := False;
          end;
          SaveActiveEdifactOrder;
        end;

        LInOrder := True;
        LNumPedido := '';
        LFechaPedido := Date;
        LFechaEntrega := Date;
        LFechaEntregaLimite := Date;
        LHasEntregaLimite := False;
        LEdiOrderType := '220';
        LEdiIdenticket := '';
        LEdiIncoterm := '';
        LClienteId := 0;
        LDestinoId := 0;
        LCentro := '';
        LDepartamento := '';
        LOrderLines.Clear;
        LPartiesEans.Clear;
        SetLength(LHeaderDiscounts, 0);
        LHasCurLine := False;
      end
      else if LTag = 'BGM' then
      begin
        LInOrder := True;
        if Length(LElems) > 1 then
        begin
          LComps := SplitEdifactComponents(LElems[1], LCompSep, LEscChar);
          if Length(LComps) > 0 then
            LEdiOrderType := Trim(LComps[0]);
        end;
        if Length(LElems) > 2 then
        begin
          LComps := SplitEdifactComponents(LElems[2], LCompSep, LEscChar);
          if Length(LComps) > 0 then
            LNumPedido := Trim(LComps[0])
          else
            LNumPedido := Trim(LElems[2]);
        end;
      end
      else if LTag = 'DTM' then
      begin
        if Length(LElems) > 1 then
        begin
          LComps := SplitEdifactComponents(LElems[1], LCompSep, LEscChar);
          if Length(LComps) >= 2 then
          begin
            var LQual := Trim(LComps[0]);
            var LVal := Trim(LComps[1]);
            var LFmt := '';
            if Length(LComps) >= 3 then LFmt := Trim(LComps[2]);

            if LQual = '137' then
              LFechaPedido := ParseEdifactDate(LVal, LFmt)
            else if LQual = '2' then
              LFechaEntrega := ParseEdifactDate(LVal, LFmt)
            else if (LQual = '63') or (LQual = '64') then
            begin
              LFechaEntregaLimite := ParseEdifactDate(LVal, LFmt);
              LHasEntregaLimite := True;
            end;
          end;
        end;
      end
      else if LTag = 'RFF' then
      begin
        if Length(LElems) > 1 then
        begin
          LComps := SplitEdifactComponents(LElems[1], LCompSep, LEscChar);
          if Length(LComps) >= 2 then
          begin
            var LQual := UpperCase(Trim(LComps[0]));
            var LVal := Trim(LComps[1]);

            if LQual = 'API' then
              LDepartamento := LVal
            else if LQual = 'ZZZ' then
              LCentro := LVal
            else if (LQual = 'ED_IDENTICKET') or (LQual = 'CT') or (LQual = 'VN') then
              LEdiIdenticket := LVal;
          end;
        end;
      end
      else if LTag = 'NAD' then
      begin
        if Length(LElems) >= 3 then
        begin
          var LQual := UpperCase(Trim(LElems[1]));
          LComps := SplitEdifactComponents(LElems[2], LCompSep, LEscChar);
          if Length(LComps) > 0 then
          begin
            var LIdent := Trim(LComps[0]);
            if (LQual <> '') and (LIdent <> '') then
              LPartiesEans.Add(TPair<string, string>.Create(LQual, LIdent));
          end;
        end;
      end
      else if LTag = 'LIN' then
      begin
        if LHasCurLine then
        begin
          if (LCurLineRec.CantidadBulto > 0) and (LCurLineRec.Cantidad > 0) then
            LCurLineRec.Bultos := Trunc(LCurLineRec.Cantidad / LCurLineRec.CantidadBulto);
          if LCurLineRec.Total = 0 then
            LCurLineRec.Total := LCurLineRec.Cantidad * LCurLineRec.Precio;
          if LCurLineRec.Ean <> '' then
            LOrderLines.Add(LCurLineRec);
          LHasCurLine := False;
        end;

        LCurLineRec.Posicion := 0;
        LCurLineRec.Ean := '';
        LCurLineRec.Descripcion := '';
        LCurLineRec.Cantidad := 0.0;
        LCurLineRec.Bultos := 0;
        LCurLineRec.CantidadBulto := 0.0;
        LCurLineRec.Precio := 0.0;
        LCurLineRec.Total := 0.0;
        SetLength(LCurLineRec.Descuentos, 0);
        LHasCurLine := True;

        if Length(LElems) >= 2 then
          LCurLineRec.Posicion := StrToIntDef(Trim(LElems[1]), 0);

        if Length(LElems) >= 4 then
        begin
          LComps := SplitEdifactComponents(LElems[3], LCompSep, LEscChar);
          if Length(LComps) > 0 then
            LCurLineRec.Ean := Trim(LComps[0]);
        end;
      end
      else if LTag = 'PIA' then
      begin
        if Length(LElems) >= 3 then
        begin
          LComps := SplitEdifactComponents(LElems[2], LCompSep, LEscChar);
          if Length(LComps) > 0 then
          begin
            var LCode := Trim(LComps[0]);
            if (LCurLineRec.Ean = '') and (Length(LCode) in [13, 14]) and (StrToInt64Def(LCode, 0) > 0) then
              LCurLineRec.Ean := LCode;
          end;
        end;
      end
      else if LTag = 'IMD' then
      begin
        var LFormatType := '';
        if Length(LElems) >= 2 then LFormatType := UpperCase(Trim(LElems[1]));
        var LItemCode := '';
        if Length(LElems) >= 3 then LItemCode := UpperCase(Trim(LElems[2]));

        if Length(LElems) >= 4 then
        begin
          LComps := SplitEdifactComponents(LElems[3], LCompSep, LEscChar);
          var LDescText := '';
          var LStartIndex := 0;
          if Length(LComps) >= 4 then
            LStartIndex := 3
          else
            LStartIndex := 0;

          // Recomponer componentes de texto consecutivos (composite C273 de EDIFACT)
          for J := LStartIndex to High(LComps) do
          begin
            var LPart := Trim(LComps[J]);
            if LPart <> '' then
            begin
              if LDescText = '' then
                LDescText := LPart
              else
              begin
                var LPrevRaw := LComps[J - 1];
                // Si la parte previa tenía longitud 35 (límite de ancho de línea UN/EDIFACT en 7008)
                // o termina en espacio, o la parte actual empieza por espacio, concatenar sin espacio extra
                if (Length(LPrevRaw) = 35) or LPrevRaw.EndsWith(' ') or LPart.StartsWith(' ') then
                  LDescText := LDescText + LPart
                else
                  LDescText := LDescText + ' ' + LPart;
              end;
            end;
          end;

          // Si tras empezar desde LStartIndex no se obtuvo texto, recorrer todos los componentes
          if LDescText = '' then
          begin
            for J := 0 to High(LComps) do
            begin
              var LPart := Trim(LComps[J]);
              if LPart <> '' then
              begin
                if LDescText = '' then
                  LDescText := LPart
                else
                  LDescText := LDescText + ' ' + LPart;
              end;
            end;
          end;

          if LDescText <> '' then
          begin
            // 1. Si el código es 'DSC' (Description), esta ES la descripción oficial del artículo
            if (LItemCode = 'DSC') then
            begin
              LCurLineRec.Descripcion := LDescText;
            end
            // 2. Si todavía no tenemos descripción, y no es un código numérico / STE / CU
            else if (LCurLineRec.Descripcion = '') and (LItemCode <> 'STE') and not SameText(LDescText, 'CU') and (LDescText <> LCurLineRec.Ean) then
            begin
              LCurLineRec.Descripcion := LDescText;
            end;
          end;
        end;
      end
      else if LTag = 'QTY' then
      begin
        if Length(LElems) >= 2 then
        begin
          LComps := SplitEdifactComponents(LElems[1], LCompSep, LEscChar);
          if Length(LComps) >= 2 then
          begin
            var LQual := Trim(LComps[0]);
            var LQtyVal := StrToFloatDef(StringReplace(Trim(LComps[1]), ',', '.', [rfReplaceAll]), 0.0, FS);
            if LQual = '21' then
              LCurLineRec.Cantidad := LQtyVal
            else if LQual = '59' then
              LCurLineRec.CantidadBulto := LQtyVal; // Unidades por bulto / caja
          end;
        end;
      end
      else if LTag = 'PRI' then
      begin
        if Length(LElems) >= 2 then
        begin
          LComps := SplitEdifactComponents(LElems[1], LCompSep, LEscChar);
          if Length(LComps) >= 2 then
          begin
            var LQual := UpperCase(Trim(LComps[0]));
            var LPriceVal := StrToFloatDef(StringReplace(Trim(LComps[1]), ',', '.', [rfReplaceAll]), 0.0, FS);
            if LQual = 'AAA' then
              LCurLineRec.Precio := LPriceVal
            else if (LQual = 'AAB') and (LCurLineRec.Precio <= 0) then
              LCurLineRec.Precio := LPriceVal;
          end;
        end;
      end
      else if LTag = 'MOA' then
      begin
        if Length(LElems) >= 2 then
        begin
          LComps := SplitEdifactComponents(LElems[1], LCompSep, LEscChar);
          if Length(LComps) >= 2 then
          begin
            var LQual := Trim(LComps[0]);
            var LAmtVal := StrToFloatDef(StringReplace(Trim(LComps[1]), ',', '.', [rfReplaceAll]), 0.0, FS);
            if LQual = '203' then
              LCurLineRec.Total := LAmtVal;
          end;
        end;
      end
      else if (LTag = 'UNT') or (LTag = 'UNZ') then
      begin
        if LHasCurLine then
        begin
          if (LCurLineRec.CantidadBulto > 0) and (LCurLineRec.Cantidad > 0) then
            LCurLineRec.Bultos := Trunc(LCurLineRec.Cantidad / LCurLineRec.CantidadBulto);
          if LCurLineRec.Total = 0 then
            LCurLineRec.Total := LCurLineRec.Cantidad * LCurLineRec.Precio;
          if LCurLineRec.Ean <> '' then
            LOrderLines.Add(LCurLineRec);
          LHasCurLine := False;
        end;

        if LInOrder and (LOrderLines.Count > 0) then
        begin
          SaveActiveEdifactOrder;
          LInOrder := False;
        end;
      end;
    end;

    // Si terminó el bucle y quedó un pedido pendiente de guardar
    if LInOrder and (LOrderLines.Count > 0) then
    begin
      if LHasCurLine then
      begin
        if (LCurLineRec.CantidadBulto > 0) and (LCurLineRec.Cantidad > 0) then
          LCurLineRec.Bultos := Trunc(LCurLineRec.Cantidad / LCurLineRec.CantidadBulto);
        if LCurLineRec.Total = 0 then
          LCurLineRec.Total := LCurLineRec.Cantidad * LCurLineRec.Precio;
        if LCurLineRec.Ean <> '' then
          LOrderLines.Add(LCurLineRec);
      end;
      SaveActiveEdifactOrder;
    end;

    Result := LTotalCount;
  finally
    LOrderLines.Free;
    LPartiesEans.Free;
  end;
end;

class function TEdicomProcessor.ImportOrders(const AFilePath: string; AEmpresaId: Integer; var AErrorMsg: string): Integer;
var
  LContent, LTrim: string;
begin
  Result := 0;
  AErrorMsg := '';

  if not TFile.Exists(AFilePath) then
  begin
    AErrorMsg := 'El archivo especificado no existe: ' + AFilePath;
    Exit;
  end;

  try
    LContent := TFile.ReadAllText(AFilePath, TEncoding.UTF8);
  except
    try
      LContent := TFile.ReadAllText(AFilePath, TEncoding.ANSI);
    except
      on E: Exception do
      begin
        AErrorMsg := 'No se pudo leer el archivo: ' + E.Message;
        Exit;
      end;
    end;
  end;

  LTrim := Trim(LContent);
  if LTrim = '' then
  begin
    AErrorMsg := 'El archivo está vacío.';
    Exit;
  end;

  // 1. Detección de formato EDIFACT (.EDI / .TXT estándar UN/EDIFACT)
  if LTrim.StartsWith('UNA') or LTrim.StartsWith('UNB') or LTrim.StartsWith('UNH') or
     ((Pos('ORDERS', LTrim) > 0) and (Pos('BGM+', LTrim) > 0)) then
  begin
    Result := ImportOrdersFromEdifactContent(LContent, AEmpresaId, AErrorMsg);
  end
  // 2. Detección de formato XML (.XML)
  else if (Pos('<HEADER>', LTrim) > 0) or (Pos('<?xml', LTrim) > 0) or (Pos('<ORDERS>', LTrim) > 0) then
  begin
    Result := ImportOrdersFromXmlContent(LContent, AEmpresaId, AErrorMsg);
  end
  // 3. Detección de formato JSON
  else if LTrim.StartsWith('{') and (Pos('"order_number"', LTrim) > 0) then
  begin
    if ImportOrderFromJson(LContent, AEmpresaId, AErrorMsg) then
      Result := 1
    else
      Result := 0;
  end
  // 4. Fallback inteligente
  else
  begin
    if Pos('''', LContent) > 0 then
      Result := ImportOrdersFromEdifactContent(LContent, AEmpresaId, AErrorMsg)
    else
      Result := ImportOrdersFromXmlContent(LContent, AEmpresaId, AErrorMsg);
  end;
end;

class function TEdicomProcessor.ExtractXmlBlocksFromContent(const AContent: string; out AMessageIds: TArray<string>): TArray<string>;
var
  LPos, LStartHeaderPos, LEndHeaderPos, LXmlStartPos, LXmlDeclPos: Integer;
  LPartHeaderStr, LXmlBlock, LCleanContent: string;
  LMsgId: string;
  LMsgList: TList<string>;
  LXmlList: TList<string>;
  LJsonVal, LItemVal, LPayloadVal: TJSONValue;
  LJsonArr: TJSONArray;
  LJsonObj: TJSONObject;
  I: Integer;

  procedure DumpToLog(const AStr: string);
  var
    SL: TStringList;
  begin
    try
      SL := TStringList.Create;
      try
        SL.Text := AStr;
        SL.SaveToFile(GetEnvironmentVariable('TEMP') + '\Edicom_Last_Received_Orders.txt', TEncoding.UTF8);
      finally
        SL.Free;
      end;
    except
    end;
  end;

  function UnescapeJsonString(const AStr: string): string;
  begin
    Result := AStr;
    Result := StringReplace(Result, '\"', '"', [rfReplaceAll]);
    Result := StringReplace(Result, '\n', #10, [rfReplaceAll]);
    Result := StringReplace(Result, '\r', #13, [rfReplaceAll]);
    Result := StringReplace(Result, '\t', #9, [rfReplaceAll]);
    Result := StringReplace(Result, '\\', '\', [rfReplaceAll]);
  end;

  function DecodeIfBase64(const AStr: string): string;
  var
    LTrimmed: string;
  begin
    LTrimmed := Trim(AStr);
    if (Pos('PD94bW', LTrimmed) = 1) or (Pos('PD94', LTrimmed) = 1) or (Pos('PEhFQ', LTrimmed) = 1) then
    begin
      try
        Result := TNetEncoding.Base64.Decode(LTrimmed);
      except
        Result := AStr;
      end;
    end
    else
      Result := AStr;
  end;

  function ProcessSingleContent(const ARawStr: string): Boolean;
  var
    LSubContent: string;
    PStart, PEnd, PDecl: Integer;
  begin
    Result := False;
    LSubContent := DecodeIfBase64(UnescapeJsonString(ARawStr));
    
    PStart := PosEx('<HEADER', UpperCase(LSubContent), 1);
    if PStart > 0 then
    begin
      PEnd := PosEx('</HEADER>', UpperCase(LSubContent), PStart);
      if PEnd > 0 then
      begin
        Inc(PEnd, Length('</HEADER>') - 1);
        PDecl := PosEx('<?XML', UpperCase(LSubContent), 1);
        if (PDecl > 0) and (PDecl < PStart) then
          LXmlList.Add(Copy(LSubContent, PDecl, PEnd - PDecl + 1))
        else
          LXmlList.Add(Copy(LSubContent, PStart, PEnd - PStart + 1));
        Result := True;
      end;
    end;
  end;

  function ExtractMessageIdFromMimeHeader(const AHeaderStr: string): string;
  var
    SL: TStringList;
    Idx: Integer;
    Line, Key, Val: string;
    ColonPos: Integer;
  begin
    Result := '';
    SL := TStringList.Create;
    try
      SL.Text := AHeaderStr;
      for Idx := 0 to SL.Count - 1 do
      begin
        Line := Trim(SL[Idx]);
        ColonPos := Pos(':', Line);
        if ColonPos > 0 then
        begin
          Key := UpperCase(Trim(Copy(Line, 1, ColonPos - 1)));
          Val := Trim(Copy(Line, ColonPos + 1, MaxInt));
          if (Key = 'X-EIPAAS-MESSAGE-ID') or (Key = 'MESSAGE-ID') or (Key = 'X-MESSAGE-ID') then
          begin
            Val := StringReplace(Val, '<', '', [rfReplaceAll]);
            Val := StringReplace(Val, '>', '', [rfReplaceAll]);
            Result := Trim(Val);
            Exit;
          end;
        end;
      end;
    finally
      SL.Free;
    end;
  end;

begin
  SetLength(Result, 0);
  SetLength(AMessageIds, 0);
  LCleanContent := Trim(AContent);
  if LCleanContent = '' then Exit;

  DumpToLog(LCleanContent);

  // Eliminar UTF-8 BOM si está presente
  if (Length(LCleanContent) >= 3) and (Ord(LCleanContent[1]) = $EF) and (Ord(LCleanContent[2]) = $BB) and (Ord(LCleanContent[3]) = $BF) then
    Delete(LCleanContent, 1, 3);
  LCleanContent := Trim(LCleanContent);

  LXmlList := TList<string>.Create;
  LMsgList := TList<string>.Create;
  try
    // 1. Probar si la respuesta viene empaquetada en formato JSON (Array u Objeto)
    if (Pos('[', LCleanContent) = 1) or (Pos('{', LCleanContent) = 1) then
    begin
      try
        LJsonVal := TJSONObject.ParseJSONValue(LCleanContent);
        if Assigned(LJsonVal) then
        begin
          try
            if LJsonVal is TJSONArray then
            begin
              LJsonArr := TJSONArray(LJsonVal);
              for I := 0 to LJsonArr.Count - 1 do
              begin
                LItemVal := LJsonArr.Items[I];
                if LItemVal is TJSONObject then
                begin
                  LJsonObj := TJSONObject(LItemVal);
                  if LJsonObj.GetValue('id') <> nil then
                    LMsgList.Add(LJsonObj.GetValue('id').Value)
                  else if LJsonObj.GetValue('messageId') <> nil then
                    LMsgList.Add(LJsonObj.GetValue('messageId').Value);

                  LPayloadVal := nil;
                  if LJsonObj.GetValue('payload') <> nil then
                    LPayloadVal := LJsonObj.GetValue('payload')
                  else if LJsonObj.GetValue('content') <> nil then
                    LPayloadVal := LJsonObj.GetValue('content')
                  else if LJsonObj.GetValue('data') <> nil then
                    LPayloadVal := LJsonObj.GetValue('data')
                  else if LJsonObj.GetValue('document') <> nil then
                    LPayloadVal := LJsonObj.GetValue('document');

                  if Assigned(LPayloadVal) then
                    ProcessSingleContent(LPayloadVal.Value);
                end;
              end;
            end
            else if LJsonVal is TJSONObject then
            begin
              LJsonObj := TJSONObject(LJsonVal);
              if LJsonObj.GetValue('id') <> nil then
                LMsgList.Add(LJsonObj.GetValue('id').Value)
              else if LJsonObj.GetValue('messageId') <> nil then
                LMsgList.Add(LJsonObj.GetValue('messageId').Value);

              LPayloadVal := nil;
              if LJsonObj.GetValue('payload') <> nil then
                LPayloadVal := LJsonObj.GetValue('payload')
              else if LJsonObj.GetValue('content') <> nil then
                LPayloadVal := LJsonObj.GetValue('content')
              else if LJsonObj.GetValue('data') <> nil then
                LPayloadVal := LJsonObj.GetValue('data')
              else if LJsonObj.GetValue('document') <> nil then
                LPayloadVal := LJsonObj.GetValue('document');

              if Assigned(LPayloadVal) then
                ProcessSingleContent(LPayloadVal.Value);
            end;
          finally
            LJsonVal.Free;
          end;
        end;
      except
      end;
    end;

    // 2. Si la respuesta es MIME Multipart (empieza por '--')
    if (LXmlList.Count = 0) and (Pos('--', LCleanContent) = 1) then
    begin
      var LBoundaryEnd := Pos(#10, LCleanContent);
      if LBoundaryEnd > 0 then
      begin
        var LBoundary := Trim(Copy(LCleanContent, 1, LBoundaryEnd));
        if SameText(Copy(LBoundary, Length(LBoundary) - 1, 2), '--') then
          LBoundary := Copy(LBoundary, 1, Length(LBoundary) - 2);

        var LBoundaryIdx := 1;
        while True do
        begin
          var LNextBoundary := PosEx(LBoundary, LCleanContent, LBoundaryIdx + Length(LBoundary));
          if LNextBoundary = 0 then Break;

          var LPartStr := Copy(LCleanContent, LBoundaryIdx + Length(LBoundary), LNextBoundary - (LBoundaryIdx + Length(LBoundary)));
          LBoundaryIdx := LNextBoundary;

          var LHeadBodySep := Pos(#13#10#13#10, LPartStr);
          var LSepLen := 4;
          if LHeadBodySep = 0 then
          begin
            LHeadBodySep := Pos(#10#10, LPartStr);
            LSepLen := 2;
          end;

          var LPartHeaders, LPartBody: string;
          if LHeadBodySep > 0 then
          begin
            LPartHeaders := Copy(LPartStr, 1, LHeadBodySep - 1);
            LPartBody := Trim(Copy(LPartStr, LHeadBodySep + LSepLen, MaxInt));
          end
          else
          begin
            LPartHeaders := '';
            LPartBody := Trim(LPartStr);
          end;

          var LDecodedBody := DecodeIfBase64(LPartBody);
          var LTrimmedDec := Trim(LDecodedBody);
          
          if (Pos('{', LTrimmedDec) = 1) or (Pos('[', LTrimmedDec) = 1) then
          begin
            try
              var LPartJson := TJSONObject.ParseJSONValue(LTrimmedDec);
              if Assigned(LPartJson) then
              begin
                try
                  if LPartJson is TJSONObject then
                  begin
                    var LObj := TJSONObject(LPartJson);
                    if LObj.GetValue('id') <> nil then
                    begin
                      if LMsgList.IndexOf(LObj.GetValue('id').Value) < 0 then
                        LMsgList.Add(LObj.GetValue('id').Value);
                    end
                    else if LObj.GetValue('messageId') <> nil then
                    begin
                      if LMsgList.IndexOf(LObj.GetValue('messageId').Value) < 0 then
                        LMsgList.Add(LObj.GetValue('messageId').Value);
                    end;
                  end;
                finally
                  LPartJson.Free;
                end;
              end;
            except
            end;
          end
          else
          begin
            ProcessSingleContent(LDecodedBody);
          end;

          var LMimeId := ExtractMessageIdFromMimeHeader(LPartHeaders);
          if (LMimeId <> '') and (LMsgList.IndexOf(LMimeId) < 0) then
            LMsgList.Add(LMimeId);
        end;
      end;
    end;

    // 3. Si sigue sin encontrar bloques XML, buscar marcas de <HEADER>...</HEADER> directo
    if LXmlList.Count = 0 then
    begin
      LPos := 1;
      while True do
      begin
        LStartHeaderPos := PosEx('<HEADER', UpperCase(LCleanContent), LPos);
        if LStartHeaderPos = 0 then Break;

        LEndHeaderPos := PosEx('</HEADER>', UpperCase(LCleanContent), LStartHeaderPos);
        if LEndHeaderPos = 0 then Break;
        Inc(LEndHeaderPos, Length('</HEADER>') - 1);

        LPartHeaderStr := Copy(LCleanContent, LPos, LStartHeaderPos - LPos);
        LMsgId := ExtractMessageIdFromMimeHeader(LPartHeaderStr);
        if (LMsgId <> '') and (LMsgList.IndexOf(LMsgId) < 0) then
          LMsgList.Add(LMsgId);

        LXmlStartPos := LStartHeaderPos;
        LXmlDeclPos := PosEx('<?XML', UpperCase(LCleanContent), LPos);
        if (LXmlDeclPos > 0) and (LXmlDeclPos < LStartHeaderPos) then
          LXmlStartPos := LXmlDeclPos;

        LXmlBlock := Copy(LCleanContent, LXmlStartPos, LEndHeaderPos - LXmlStartPos + 1);
        LXmlList.Add(Trim(LXmlBlock));

        LPos := LEndHeaderPos + 1;
      end;
    end;

    // 4. Fallback directo si el contenido empieza por '<'
    if (LXmlList.Count = 0) and (Pos('<', LCleanContent) = 1) and (Pos('--', LCleanContent) <> 1) then
      LXmlList.Add(LCleanContent);

    Result := LXmlList.ToArray;
    AMessageIds := LMsgList.ToArray;
  finally
    LXmlList.Free;
    LMsgList.Free;
  end;
end;

class function TEdicomProcessor.ImportOrdersFromXmlContent(const AXmlContent: string; AEmpresaId: Integer; var AErrorMsg: string): Integer;
var
  LMessageIds: TArray<string>;
begin
  Result := ImportOrdersFromXmlContent(AXmlContent, AEmpresaId, AErrorMsg, LMessageIds);
end;

class function TEdicomProcessor.ImportOrdersFromXmlContent(const AXmlContent: string; AEmpresaId: Integer; var AErrorMsg: string; out AMessageIds: TArray<string>): Integer;
var
  LXmlBlocks: TArray<string>;
  LSingleXml: string;
  LTotalCount, LSubCount: Integer;
  LSubError: string;
begin
  Result := 0;
  AErrorMsg := '';
  SetLength(AMessageIds, 0);

  if Trim(AXmlContent) = '' then
  begin
    AErrorMsg := 'El contenido XML está vacío.';
    Exit;
  end;

  LXmlBlocks := ExtractXmlBlocksFromContent(AXmlContent, AMessageIds);
  if Length(LXmlBlocks) = 0 then
  begin
    var LSnippet := Copy(Trim(AXmlContent), 1, 300);
    AErrorMsg := 'No se encontró ningún bloque de pedido EDI válido (<HEADER>) en la respuesta recibida.' + sLineBreak + sLineBreak +
                 'Respuesta recibida del servidor EDICOM:' + sLineBreak + LSnippet;
    Exit;
  end;

  LTotalCount := 0;
  for LSingleXml in LXmlBlocks do
  begin
    LSubError := '';
    LSubCount := ImportSingleOrderFromXmlBlock(LSingleXml, AEmpresaId, LSubError);
    if LSubCount > 0 then
      Inc(LTotalCount, LSubCount)
    else if LSubError <> '' then
    begin
      if AErrorMsg <> '' then AErrorMsg := AErrorMsg + sLineBreak + sLineBreak;
      AErrorMsg := AErrorMsg + LSubError;
    end;
  end;

  Result := LTotalCount;
end;

class function TEdicomProcessor.ImportSingleOrderFromXmlBlock(const AXmlBlock: string; AEmpresaId: Integer; var AErrorMsg: string): Integer;
var
  LXML: IXMLDocument;
  LHeaderNode: IXMLNode;
  LNode: IXMLNode;
  I, J, K: Integer;
  
  LNumPedido: string;
  LFechaPedido: TDateTime;
  LFechaEntrega: TDateTime;
  LFechaEntregaLimite: TDateTime;
  LHasEntregaLimite: Boolean;
  LEdiOrderType: string;
  LEdiIdenticket: string;
  LEdiIncoterm: string;
  LClienteId: Integer;
  LDestinoId: Integer;
  LClienteSolicitanteId: Integer;
  LCount: Integer;
  LOrderLines: TList<TOrderLineRec>;
  LineRec: TOrderLineRec;
  FS: TFormatSettings;
  LValStr: string;
  LTempId, LTempDest: Integer;

  LHeaderDiscounts: TArray<TDiscountRec>;
  LPartiesEans: TList<TPair<string, string>>;
  
  procedure SaveActiveOrder;
  var
    Qry: TFDQuery;
    LOrderId: Integer;
    LBase, LIva, LTotal: Double;
    LRec: TOrderLineRec;
    LDisc: TDiscountRec;
    LLineId: Integer;
    LEanInfo: string;
    LPair: TPair<string, string>;
    LArtId: Integer;
    LArtDesc: string;
  begin
    if LClienteId = 0 then
    begin
      LEanInfo := '';
      for LPair in LPartiesEans do
        LEanInfo := LEanInfo + Format('  [%s] %s', [LPair.Key, LPair.Value]) + sLineBreak;
      if LEanInfo = '' then
        LEanInfo := '  (ningún nodo PARTIES encontrado en el XML)';
      AErrorMsg := Format(
        'No se encontró ningún cliente en la base de datos que coincida con los EAN del pedido %s.' + sLineBreak +
        'Códigos EAN intentados:' + sLineBreak + '%s' +
        'Registre primero el EAN en la ficha del cliente (ge_clientes.ean) o en su destino (ge_clientes_destinos.ean_cliente, ean_receptor o ean_emisor).',
        [LNumPedido, LEanInfo]);
      Exit;
    end;
    if LOrderLines.Count = 0 then
    begin
      AErrorMsg := Format('El pedido %s no contiene líneas de artículos válidas (nodos LINES con EAN).', [LNumPedido]);
      Exit;
    end;
    
    // Fallback: si alguna línea viene sin precio (o 0) y el artículo está en BD, usar su precio por defecto (pvp1)
    for var LIdx := 0 to LOrderLines.Count - 1 do
    begin
      var LLineItem := LOrderLines[LIdx];
      if (LLineItem.Precio <= 0.0) and (LLineItem.Ean <> '') then
      begin
        var LTempArtId: Integer := 0;
        var LTempArtDesc: string := '';
        if FindArticleByEan(dmgMain.dbConn, LLineItem.Ean, LTempArtId, LTempArtDesc) and (LTempArtId > 0) then
        begin
          var QryPrice := TFDQuery.Create(nil);
          try
            QryPrice.Connection := dmgMain.dbConn;
            QryPrice.SQL.Text := 'SELECT pvp1 FROM ge_articulos WHERE id = :id LIMIT 1';
            QryPrice.ParamByName('id').AsInteger := LTempArtId;
            QryPrice.Open;
            if not QryPrice.IsEmpty and (QryPrice.Fields[0].AsFloat > 0) then
            begin
              LLineItem.Precio := QryPrice.Fields[0].AsFloat;
              LLineItem.Total := LLineItem.Cantidad * LLineItem.Precio;
              LOrderLines[LIdx] := LLineItem;
            end;
          finally
            QryPrice.Free;
          end;
        end;
      end;
    end;

    LBase := 0;
    for LRec in LOrderLines do
      LBase := LBase + LRec.Total;
    LIva := LBase * 0.10;
    LTotal := LBase + LIva;
    
    Qry := TFDQuery.Create(nil);
    try
      Qry.Connection := dmgMain.dbConn;
      Qry.SQL.Text := 
        'INSERT INTO ge_pedidos (empresa_id, cliente_id, cliente_solicitante_id, clientedestino_id, numero_pedido, ' +
        '  fecha_pedido, fecha_entrega, fecha_entrega_limite, estado, base_imponible, total, iva, observaciones, ' +
        '  edi_order_type, edi_identicket, edi_incoterm, id_user_creator, id_user_update) ' +
        'VALUES (:empresa_id, :cliente_id, :cliente_solicitante_id, :clientedestino_id, :numero_pedido, ' +
        '  :fecha_pedido, :fecha_entrega, :fecha_entrega_limite, ''PENDIENTE'', :base_imponible, :total, :iva, :obs, ' +
        '  :edi_order_type, :edi_identicket, :edi_incoterm, 1, 1)';
      Qry.ParamByName('empresa_id').AsInteger := AEmpresaId;
      Qry.ParamByName('cliente_id').AsInteger := LClienteId;
      Qry.ParamByName('cliente_solicitante_id').DataType := ftInteger;
      if LClienteSolicitanteId > 0 then
        Qry.ParamByName('cliente_solicitante_id').AsInteger := LClienteSolicitanteId
      else
        Qry.ParamByName('cliente_solicitante_id').Clear;
      Qry.ParamByName('clientedestino_id').DataType := ftInteger;
      if LDestinoId > 0 then
        Qry.ParamByName('clientedestino_id').AsInteger := LDestinoId
      else
        Qry.ParamByName('clientedestino_id').Clear;
      Qry.ParamByName('numero_pedido').AsString := LNumPedido;
      Qry.ParamByName('fecha_pedido').AsDateTime := LFechaPedido;
      Qry.ParamByName('fecha_entrega').AsDateTime := LFechaEntrega;
      
      Qry.ParamByName('fecha_entrega_limite').DataType := ftDate;
      if LHasEntregaLimite then
        Qry.ParamByName('fecha_entrega_limite').AsDate := LFechaEntregaLimite
      else
        Qry.ParamByName('fecha_entrega_limite').Clear;
        
      Qry.ParamByName('base_imponible').AsFloat := LBase;
      Qry.ParamByName('total').AsFloat := LTotal;
      Qry.ParamByName('iva').AsFloat := LIva;
      Qry.ParamByName('obs').AsString := 'Importado desde EDICOM WebService';
      
      Qry.ParamByName('edi_order_type').DataType := ftString;
      if LEdiOrderType <> '' then Qry.ParamByName('edi_order_type').AsString := LEdiOrderType else Qry.ParamByName('edi_order_type').Clear;

      Qry.ParamByName('edi_identicket').DataType := ftString;
      if LEdiIdenticket <> '' then Qry.ParamByName('edi_identicket').AsString := LEdiIdenticket else Qry.ParamByName('edi_identicket').Clear;

      Qry.ParamByName('edi_incoterm').DataType := ftString;
      if LEdiIncoterm <> '' then Qry.ParamByName('edi_incoterm').AsString := LEdiIncoterm else Qry.ParamByName('edi_incoterm').Clear;
      
      Qry.ExecSQL;
      
      Qry.SQL.Text := 'SELECT LAST_INSERT_ID()';
      Qry.Open;
      LOrderId := Qry.Fields[0].AsInteger;
      Qry.Close;
      
      for LDisc in LHeaderDiscounts do
      begin
        Qry.SQL.Text :=
          'INSERT INTO ge_pedidos_descuentos_cargos (id_pedido, tipo, codigo_motivo, porcentaje, importe) ' +
          'VALUES (:id_pedido, :tipo, :codigo_motivo, :porcentaje, :importe)';
        Qry.ParamByName('id_pedido').AsInteger := LOrderId;
        Qry.ParamByName('tipo').AsString := LDisc.Tipo;
        Qry.ParamByName('codigo_motivo').AsString := LDisc.CodigoMotivo;
        Qry.ParamByName('porcentaje').AsFloat := LDisc.Porcentaje;
        Qry.ParamByName('importe').AsFloat := LDisc.Importe;
        Qry.ExecSQL;
      end;
      
      for LRec in LOrderLines do
      begin
        Qry.SQL.Text := 
          'INSERT INTO ge_pedidos_lineas (' +
          '  id_pedido, id_articulo, descripcion, cantidad, bultos, precio, total, ' +
          '  ean_producto, cantidad_pedida_original, numero_linea_cliente' +
          ') VALUES (' +
          '  :id_pedido, :id_articulo, :desc, :cant, :bultos, :precio, :total, ' +
          '  :ean, :cant_orig, :num_lin' +
          ')';
        Qry.ParamByName('id_pedido').AsInteger := LOrderId;
        
        if FindArticleByEan(dmgMain.dbConn, LRec.Ean, LArtId, LArtDesc) then
          Qry.ParamByName('id_articulo').AsInteger := LArtId
        else
          Qry.ParamByName('id_articulo').AsInteger := 1;
          
        var LFinalDesc := Trim(LRec.Descripcion);
        if (LFinalDesc = '') or SameText(LFinalDesc, 'CU') or (LFinalDesc = LRec.Ean) or (Length(LFinalDesc) <= 3) then
        begin
          if LArtDesc <> '' then
            LFinalDesc := LArtDesc;
        end;
        if (LFinalDesc = '') and (LArtDesc <> '') then
          LFinalDesc := LArtDesc;

        Qry.ParamByName('desc').AsString := LFinalDesc;
          
        Qry.ParamByName('cant').AsFloat := LRec.Cantidad;
        Qry.ParamByName('bultos').DataType := ftInteger;
        if LRec.Bultos > 0 then
          Qry.ParamByName('bultos').AsInteger := LRec.Bultos
        else
          Qry.ParamByName('bultos').Clear;

        Qry.ParamByName('precio').AsFloat := LRec.Precio;
        Qry.ParamByName('total').AsFloat := LRec.Total;
        Qry.ParamByName('ean').AsString := LRec.Ean;
        Qry.ParamByName('cant_orig').AsFloat := LRec.Cantidad;
        Qry.ParamByName('num_lin').DataType := ftInteger;
        if LRec.Posicion > 0 then
          Qry.ParamByName('num_lin').AsInteger := LRec.Posicion
        else
          Qry.ParamByName('num_lin').Clear;

        Qry.ExecSQL;
        
        Qry.SQL.Text := 'SELECT LAST_INSERT_ID()';
        Qry.Open;
        LLineId := Qry.Fields[0].AsInteger;
        Qry.Close;
        
        for LDisc in LRec.Descuentos do
        begin
          Qry.SQL.Text :=
            'INSERT INTO ge_pedidos_lineas_descuentos (id_pedido_linea, tipo, codigo_motivo, porcentaje, importe) ' +
            'VALUES (:id_pedido_linea, :tipo, :codigo_motivo, :porcentaje, :importe)';
          Qry.ParamByName('id_pedido_linea').AsInteger := LLineId;
          Qry.ParamByName('tipo').AsString := LDisc.Tipo;
          Qry.ParamByName('codigo_motivo').AsString := LDisc.CodigoMotivo;
          Qry.ParamByName('porcentaje').AsFloat := LDisc.Porcentaje;
          Qry.ParamByName('importe').AsFloat := LDisc.Importe;
          Qry.ExecSQL;
        end;
      end;
      
      Inc(LCount);
    finally
      Qry.Free;
    end;
  end;

begin
  Result := 0;
  LCount := 0;
  LClienteId := 0;
  LDestinoId := 0;
  LNumPedido := '';
  LFechaPedido := Date;
  LFechaEntrega := Date;
  LFechaEntregaLimite := Date;
  LHasEntregaLimite := False;
  LEdiOrderType := '';
  LEdiIdenticket := '';
  LEdiIncoterm := '';
  SetLength(LHeaderDiscounts, 0);
  
  FS := TFormatSettings.Create('en-US');
  FS.DateSeparator := '-';
  FS.ShortDateFormat := 'yyyy-mm-dd';
  
  if Trim(AXmlBlock) = '' then
  begin
    AErrorMsg := 'El bloque XML está vacío.';
    Exit;
  end;
  
  LOrderLines := TList<TOrderLineRec>.Create;
  LPartiesEans := TList<TPair<string, string>>.Create;
  try
    try
      LXML := LoadXMLData(AXmlBlock);
      LHeaderNode := LXML.DocumentElement;
      
      if not SameText(LHeaderNode.NodeName, 'HEADER') then
      begin
        AErrorMsg := 'El elemento raíz del contenido XML debe ser <HEADER>.';
        Exit;
      end;
      
      for I := 0 to LHeaderNode.ChildNodes.Count - 1 do
      begin
        LNode := LHeaderNode.ChildNodes[I];
        
        if SameText(LNode.NodeName, 'Order_number') then
          LNumPedido := Trim(LNode.Text)
        else if SameText(LNode.NodeName, 'Type') then
          LEdiOrderType := Trim(LNode.Text)
        else if SameText(LNode.NodeName, 'Date') then
          TryStrToDate(Trim(Copy(LNode.Text, 1, 10)), LFechaPedido, FS)
        else if SameText(LNode.NodeName, 'Actual_delivery_date') then
          TryStrToDate(Trim(Copy(LNode.Text, 1, 10)), LFechaEntrega, FS)
        else if SameText(LNode.NodeName, 'Delivery_end_date') then
          LHasEntregaLimite := TryStrToDate(Trim(Copy(LNode.Text, 1, 10)), LFechaEntregaLimite, FS)
        else if SameText(LNode.NodeName, 'SUPPORTING_DOCS') then
        begin
          var LDocType: string := '';
          var LDocIdent: string := '';
          for J := 0 to LNode.ChildNodes.Count - 1 do
          begin
            if SameText(LNode.ChildNodes[J].NodeName, 'Type') then
              LDocType := Trim(LNode.ChildNodes[J].Text)
            else if SameText(LNode.ChildNodes[J].NodeName, 'Identifier') then
              LDocIdent := Trim(LNode.ChildNodes[J].Text);
          end;
          if SameText(LDocType, 'ED_Identicket') then
            LEdiIdenticket := LDocIdent;
        end
        else if SameText(LNode.NodeName, 'TRANSPORT_DETAILS') then
        begin
          for J := 0 to LNode.ChildNodes.Count - 1 do
          begin
            if SameText(LNode.ChildNodes[J].NodeName, 'Identification') then
              LEdiIncoterm := Trim(LNode.ChildNodes[J].Text);
          end;
        end
        else if SameText(LNode.NodeName, 'ALLOWANCES_CHARGES') then
        begin
          var LDiscRec: TDiscountRec;
          LDiscRec.Tipo := 'A';
          LDiscRec.CodigoMotivo := '';
          LDiscRec.Porcentaje := 0;
          LDiscRec.Importe := 0;
          
          for J := 0 to LNode.ChildNodes.Count - 1 do
          begin
            var LProp: IXMLNode := LNode.ChildNodes[J];
            if SameText(LProp.NodeName, 'Type') then
              LDiscRec.Tipo := Trim(LProp.Text)
            else if SameText(LProp.NodeName, 'Reason_code') then
              LDiscRec.CodigoMotivo := Trim(LProp.Text)
            else if SameText(LProp.NodeName, 'Percentage') then
            begin
              LValStr := StringReplace(LProp.Text, ',', '.', [rfReplaceAll]);
              LDiscRec.Porcentaje := StrToFloatDef(LValStr, 0.0, FS);
            end
            else if SameText(LProp.NodeName, 'Amount') then
            begin
              LValStr := StringReplace(LProp.Text, ',', '.', [rfReplaceAll]);
              LDiscRec.Importe := StrToFloatDef(LValStr, 0.0, FS);
            end;
          end;
          
          SetLength(LHeaderDiscounts, Length(LHeaderDiscounts) + 1);
          LHeaderDiscounts[Length(LHeaderDiscounts) - 1] := LDiscRec;
        end
        else if SameText(LNode.NodeName, 'PARTIES') then
        begin
          var LQualifier: string := '';
          var LIdentifier: string := '';
          for J := 0 to LNode.ChildNodes.Count - 1 do
          begin
            if SameText(LNode.ChildNodes[J].NodeName, 'Qualifier') then
              LQualifier := Trim(LNode.ChildNodes[J].Text)
            else if SameText(LNode.ChildNodes[J].NodeName, 'Identifier') then
              LIdentifier := Trim(LNode.ChildNodes[J].Text);
          end;

          if (LQualifier <> '') and (LIdentifier <> '') then
            LPartiesEans.Add(TPair<string, string>.Create(LQualifier, LIdentifier));
        end
        else if SameText(LNode.NodeName, 'LINES') then
        begin
          LineRec.Posicion := LOrderLines.Count + 1;
          LineRec.Ean := '';
          LineRec.Descripcion := '';
          LineRec.Cantidad := 0.0;
          LineRec.Bultos := 0;
          LineRec.CantidadBulto := 0.0;
          LineRec.Precio := 0.0;
          LineRec.Total := 0.0;
          SetLength(LineRec.Descuentos, 0);
          
          for J := 0 to LNode.ChildNodes.Count - 1 do
          begin
            var LLineProp: IXMLNode := LNode.ChildNodes[J];
            if SameText(LLineProp.NodeName, 'Identifier') then
              LineRec.Ean := Trim(LLineProp.Text)
            else if SameText(LLineProp.NodeName, 'Line_number') or SameText(LLineProp.NodeName, 'Position') then
              LineRec.Posicion := StrToIntDef(Trim(LLineProp.Text), LineRec.Posicion)
            else if SameText(LLineProp.NodeName, 'Description') then
              LineRec.Descripcion := Trim(LLineProp.Text)
            else if SameText(LLineProp.NodeName, 'Quantity') or SameText(LLineProp.NodeName, 'Qty') then
            begin
              LValStr := StringReplace(LLineProp.Text, ',', '.', [rfReplaceAll]);
              LineRec.Cantidad := StrToFloatDef(LValStr, 0.0, FS);
            end
            else if SameText(LLineProp.NodeName, 'Unit_net_price') or SameText(LLineProp.NodeName, 'Net_price') then
            begin
              LValStr := StringReplace(LLineProp.Text, ',', '.', [rfReplaceAll]);
              LineRec.Precio := StrToFloatDef(LValStr, 0.0, FS);
            end
            else if SameText(LLineProp.NodeName, 'Unit_gross_price') or SameText(LLineProp.NodeName, 'Gross_price') or
                    SameText(LLineProp.NodeName, 'Price') or SameText(LLineProp.NodeName, 'Unit_price') then
            begin
              LValStr := StringReplace(LLineProp.Text, ',', '.', [rfReplaceAll]);
              if LineRec.Precio <= 0.0 then
                LineRec.Precio := StrToFloatDef(LValStr, 0.0, FS);
            end
            else if SameText(LLineProp.NodeName, 'Package_quantity') or SameText(LLineProp.NodeName, 'Number_of_packages') or
                    SameText(LLineProp.NodeName, 'Packages') or SameText(LLineProp.NodeName, 'Bultos') then
            begin
              LValStr := StringReplace(LLineProp.Text, ',', '.', [rfReplaceAll]);
              LineRec.CantidadBulto := StrToFloatDef(LValStr, 0.0, FS);
            end
            else if SameText(LLineProp.NodeName, 'TOTALS_LINES') then
            begin
              for K := 0 to LLineProp.ChildNodes.Count - 1 do
              begin
                var LTotalProp: IXMLNode := LLineProp.ChildNodes[K];
                if SameText(LTotalProp.NodeName, 'Net_amount') or SameText(LTotalProp.NodeName, 'Total_amount') then
                begin
                  LValStr := StringReplace(LTotalProp.Text, ',', '.', [rfReplaceAll]);
                  LineRec.Total := StrToFloatDef(LValStr, 0.0, FS);
                end;
              end;
            end
            else if SameText(LLineProp.NodeName, 'ALLOWANCES_CHARGES_LINES') then
            begin
              var LLineDisc: TDiscountRec;
              LLineDisc.Tipo := 'A';
              LLineDisc.CodigoMotivo := '';
              LLineDisc.Porcentaje := 0.0;
              LLineDisc.Importe := 0.0;
              
              for K := 0 to LLineProp.ChildNodes.Count - 1 do
              begin
                var LSubProp: IXMLNode := LLineProp.ChildNodes[K];
                if SameText(LSubProp.NodeName, 'Type') then
                  LLineDisc.Tipo := Trim(LSubProp.Text)
                else if SameText(LSubProp.NodeName, 'Reason_code') then
                  LLineDisc.CodigoMotivo := Trim(LSubProp.Text)
                else if SameText(LSubProp.NodeName, 'Percentage') then
                begin
                  LValStr := StringReplace(LSubProp.Text, ',', '.', [rfReplaceAll]);
                  LLineDisc.Porcentaje := StrToFloatDef(LValStr, 0.0, FS);
                end
                else if SameText(LSubProp.NodeName, 'Amount') then
                begin
                  LValStr := StringReplace(LSubProp.Text, ',', '.', [rfReplaceAll]);
                  LLineDisc.Importe := StrToFloatDef(LValStr, 0.0, FS);
                end;
              end;
              
              SetLength(LineRec.Descuentos, Length(LineRec.Descuentos) + 1);
              LineRec.Descuentos[Length(LineRec.Descuentos) - 1] := LLineDisc;
            end;
          end;
          
          if LineRec.Total <= 0.0 then
            LineRec.Total := LineRec.Cantidad * LineRec.Precio;

          if (LineRec.Precio <= 0.0) and (LineRec.Cantidad > 0) and (LineRec.Total > 0) then
            LineRec.Precio := LineRec.Total / LineRec.Cantidad;

          if (LineRec.CantidadBulto > 0) and (LineRec.Cantidad > 0) then
          begin
            if LineRec.Cantidad >= LineRec.CantidadBulto then
              LineRec.Bultos := Trunc(LineRec.Cantidad / LineRec.CantidadBulto)
            else
              LineRec.Bultos := 1;
          end;
          if LineRec.Bultos <= 0 then
            LineRec.Bultos := 1;

          if LineRec.Ean <> '' then
            LOrderLines.Add(LineRec);
        end;
      end;

      // 1. Extraer identificadores según rol en PARTIES
      var LEanDeliveryPoint: string := '';
      var LEanBuyer: string := '';
      var LEanIssuer: string := '';

      for var LPair in LPartiesEans do
      begin
        if (SameText(LPair.Key, 'DP') or SameText(LPair.Key, 'UD') or SameText(LPair.Key, 'CN')) and (LEanDeliveryPoint = '') then
          LEanDeliveryPoint := Trim(LPair.Value)
        else if (SameText(LPair.Key, 'BY') or SameText(LPair.Key, 'IV') or SameText(LPair.Key, 'MR')) and (LEanBuyer = '') then
          LEanBuyer := Trim(LPair.Value)
        else if SameText(LPair.Key, 'MS') and (LEanIssuer = '') then
          LEanIssuer := Trim(LPair.Value);
      end;

      // 2. Resolver Cliente principal (priorizando BY / Comprador)
      if LEanBuyer <> '' then
      begin
        if FindClientByEan(dmgMain.dbConn, LEanBuyer, LTempId, LTempDest) then
          LClienteId := LTempId;
      end;

      // 3. Resolver Destino específico del cliente a partir del punto de entrega (DP / UD / CN)
      if LEanDeliveryPoint <> '' then
      begin
        var LDestinoCliId: Integer := 0;
        if FindDestinoByEan(dmgMain.dbConn, LClienteId, LEanDeliveryPoint, LDestinoId, LDestinoCliId) then
        begin
          if LClienteId = 0 then
            LClienteId := LDestinoCliId;
        end;
      end;

      // 3.3. Resolver Destino Solicitante (cliente_solicitante_id)
      LClienteSolicitanteId := 0;
      if (LEanBuyer <> '') or (LEanIssuer <> '') then
      begin
        var LEanToSearch := LEanBuyer;
        if LEanToSearch = '' then LEanToSearch := LEanIssuer;
        var LSolicitanteCliId: Integer := 0;
        FindDestinoSolicitanteByEan(dmgMain.dbConn, LClienteId, LEanToSearch, LClienteSolicitanteId, LSolicitanteCliId);
      end;
      if LClienteSolicitanteId = 0 then
        LClienteSolicitanteId := LDestinoId;

      // 4. Si aún no tenemos cliente, buscar secuencialmente en el resto de qualificadores
      if LClienteId = 0 then
      begin
        for var LPriority in TArray<string>.Create('BY', 'IV', 'MR', 'MS', 'DP', 'UD', 'SU') do
        begin
          if LClienteId > 0 then Break;
          for var LPair in LPartiesEans do
          begin
            if SameText(LPair.Key, LPriority) and (LPair.Value <> '') then
            begin
              if FindClientByEan(dmgMain.dbConn, LPair.Value, LTempId, LTempDest) then
              begin
                LClienteId := LTempId;
                if (LDestinoId = 0) and (LTempDest > 0) then
                  LDestinoId := LTempDest;
                Break;
              end;
            end;
          end;
        end;
      end;

      SaveActiveOrder;
      Result := LCount;
    except
      on E: Exception do
      begin
        AErrorMsg := E.Message;
        Result := 0;
      end;
    end;
  finally
    LOrderLines.Free;
    LPartiesEans.Free;
  end;
end;

class function TEdicomProcessor.ImportOrderFromJson(const AJson: string; AEmpresaId: Integer; var AErrorMsg: string): Boolean;
var
  LJSONObj, LLineObj: TJSONObject;
  LVal: TJSONValue;
  LLinesArr: TJSONArray;
  I: Integer;
  LNumPedido: string;
  LFechaPedidoStr: string;
  LFechaEntregaStr: string;
  LFechaPedido: TDateTime;
  LFechaEntrega: TDateTime;
  LClienteEan: string;
  LEmpresa, LCentro, LDepartamento: string;
  LClienteId, LDestinoId: Integer;
  LOrderLines: TList<TOrderLineRec>;
  LRec: TOrderLineRec;
  FS: TFormatSettings;
  LBase, LIva, LTotal: Double;
  Qry: TFDQuery;
  LOrderId: Integer;
  LArtId: Integer;
  LArtDesc: string;
begin
  Result := False;
  AErrorMsg := '';
  LClienteId := 0;
  LDestinoId := 0;
  LEmpresa := '';
  LCentro := '';
  LDepartamento := '';
  
  LJSONObj := TJSONObject.ParseJSONValue(AJson) as TJSONObject;
  if not Assigned(LJSONObj) then
  begin
    AErrorMsg := 'El contenido devuelto no es un JSON válido.';
    Exit;
  end;
  
  try
    // 1. Extraer y validar campos de cabecera usando TJSONValue tradicionales (Delphi XE+)
    LVal := LJSONObj.GetValue('order_number');
    if Assigned(LVal) then LNumPedido := LVal.Value else LNumPedido := '';
    
    if LNumPedido = '' then
    begin
      AErrorMsg := 'Falta el número de pedido (order_number) en el JSON.';
      Exit;
    end;
    
    LVal := LJSONObj.GetValue('date');
    if Assigned(LVal) then LFechaPedidoStr := LVal.Value else LFechaPedidoStr := '';
    
    LVal := LJSONObj.GetValue('delivery_date');
    if Assigned(LVal) then LFechaEntregaStr := LVal.Value else LFechaEntregaStr := '';
    
    LVal := LJSONObj.GetValue('client_ean');
    if Assigned(LVal) then LClienteEan := LVal.Value else LClienteEan := '';
    
    LVal := LJSONObj.GetValue('empresa');
    if Assigned(LVal) then LEmpresa := LVal.Value;
    LVal := LJSONObj.GetValue('centro');
    if Assigned(LVal) then LCentro := LVal.Value;
    LVal := LJSONObj.GetValue('departamento');
    if Assigned(LVal) then LDepartamento := LVal.Value;
    
    // Rellenar con ceros a la izquierda hasta 4 dígitos si aplica
    if (LCentro <> '') and (Length(LCentro) < 4) then
      LCentro := StringOfChar('0', 4 - Length(LCentro)) + LCentro;
    if (LDepartamento <> '') and (Length(LDepartamento) < 4) then
      LDepartamento := StringOfChar('0', 4 - Length(LDepartamento)) + LDepartamento;
      
    // 2. Intentar buscar por Centro y Departamento en ge_clientes_destinos
    if (LCentro <> '') and (LDepartamento <> '') then
    begin
      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.SQL.Text := 
          'SELECT id_cliente, id, ean_cliente, ean_receptor FROM ge_clientes_destinos ' +
          'WHERE (LPAD(TRIM(centro), 4, ''0'') = :centro OR TRIM(centro) = :centro_raw) ' +
          '  AND (LPAD(TRIM(departamento), 4, ''0'') = :depto OR TRIM(departamento) = :depto_raw) ' +
          '  AND activo = 1 LIMIT 1';
        Qry.ParamByName('centro').AsString := LCentro;
        Qry.ParamByName('centro_raw').AsString := LCentro;
        Qry.ParamByName('depto').AsString := LDepartamento;
        Qry.ParamByName('depto_raw').AsString := LDepartamento;
        Qry.Open;
        
        if not Qry.IsEmpty then
        begin
          LClienteId := Qry.FieldByName('id_cliente').AsInteger;
          LDestinoId := Qry.FieldByName('id').AsInteger;
          if LClienteEan = '' then
            LClienteEan := Qry.FieldByName('ean_cliente').AsString;
          if LClienteEan = '' then
            LClienteEan := Qry.FieldByName('ean_receptor').AsString;
        end;
      finally
        Qry.Free;
      end;
    end;
    
    FS := TFormatSettings.Create('en-US');
    FS.DateSeparator := '-';
    FS.ShortDateFormat := 'yyyy-mm-dd';
    
    if not TryStrToDate(LFechaPedidoStr, LFechaPedido, FS) then
      LFechaPedido := Date;
      
    if not TryStrToDate(LFechaEntregaStr, LFechaEntrega, FS) then
      LFechaEntrega := LFechaPedido + 7;
      
    // Si no se encontró por centro/depto, buscar por EAN
    if (LClienteId = 0) and (LClienteEan <> '') then
      FindClientByEan(dmgMain.dbConn, LClienteEan, LClienteId, LDestinoId);
      
    if LClienteId = 0 then
    begin
      AErrorMsg := 'No se encontró ningún cliente en la base de datos coincidente por Centro/Depto (' + 
                   LCentro + '/' + LDepartamento + ') ni por EAN (' + LClienteEan + ').';
      Exit;
    end;
    
    LVal := LJSONObj.GetValue('lines');
    if (LVal = nil) or not (LVal is TJSONArray) then
    begin
      AErrorMsg := 'El pedido no contiene una lista de líneas de artículos (lines).';
      Exit;
    end;
    
    LLinesArr := LVal as TJSONArray;
    if LLinesArr.Count = 0 then
    begin
      AErrorMsg := 'La lista de líneas de artículos (lines) está vacía.';
      Exit;
    end;
    
    LOrderLines := TList<TOrderLineRec>.Create;
    try
      for I := 0 to LLinesArr.Count - 1 do
      begin
        LLineObj := LLinesArr.Items[I] as TJSONObject;
        LRec.Ean := '';
        LRec.Descripcion := '';
        LRec.Cantidad := 0.0;
        LRec.Precio := 0.0;
        
        LVal := LLineObj.GetValue('ean');
        if Assigned(LVal) then LRec.Ean := LVal.Value;
        
        LVal := LLineObj.GetValue('description');
        if Assigned(LVal) then LRec.Descripcion := LVal.Value;
        
        LVal := LLineObj.GetValue('quantity');
        if Assigned(LVal) then LRec.Cantidad := StrToFloatDef(StringReplace(LVal.Value, ',', '.', [rfReplaceAll]), 0.0, FS);
        
        LVal := LLineObj.GetValue('price');
        if Assigned(LVal) then LRec.Precio := StrToFloatDef(StringReplace(LVal.Value, ',', '.', [rfReplaceAll]), 0.0, FS);
        
        LRec.Total := LRec.Cantidad * LRec.Precio;
        SetLength(LRec.Descuentos, 0);
        
        if LRec.Ean <> '' then
          LOrderLines.Add(LRec);
      end;
      
      if LOrderLines.Count = 0 then
      begin
        AErrorMsg := 'No se pudieron parsear líneas válidas del pedido.';
        Exit;
      end;
      
      // Calcular totales
      LBase := 0;
      for LRec in LOrderLines do
        LBase := LBase + LRec.Total;
      LIva := LBase * 0.10; // IVA general al 10%
      LTotal := LBase + LIva;
      
      Qry := TFDQuery.Create(nil);
      try
        Qry.Connection := dmgMain.dbConn;
        Qry.SQL.Text := 
          'INSERT INTO ge_pedidos (empresa_id, cliente_id, cliente_solicitante_id, clientedestino_id, numero_pedido, ' +
          '  fecha_pedido, fecha_entrega, estado, base_imponible, total, iva, observaciones, ' +
          '  id_user_creator, id_user_update) ' +
          'VALUES (:empresa_id, :cliente_id, :cliente_solicitante_id, :clientedestino_id, :numero_pedido, ' +
          '  :fecha_pedido, :fecha_entrega, ''PENDIENTE'', :base_imponible, :total, :iva, :obs, ' +
          '  1, 1)';
        Qry.ParamByName('empresa_id').AsInteger := AEmpresaId;
        Qry.ParamByName('cliente_id').AsInteger := LClienteId;
        Qry.ParamByName('cliente_solicitante_id').DataType := ftInteger;
        if LDestinoId > 0 then
          Qry.ParamByName('cliente_solicitante_id').AsInteger := LDestinoId
        else
          Qry.ParamByName('cliente_solicitante_id').Clear;
        Qry.ParamByName('clientedestino_id').DataType := ftInteger;
        if LDestinoId > 0 then
          Qry.ParamByName('clientedestino_id').AsInteger := LDestinoId
        else
          Qry.ParamByName('clientedestino_id').Clear;
        Qry.ParamByName('numero_pedido').AsString := LNumPedido;
        Qry.ParamByName('fecha_pedido').AsDateTime := LFechaPedido;
        Qry.ParamByName('fecha_entrega').AsDateTime := LFechaEntrega;
        Qry.ParamByName('base_imponible').AsFloat := LBase;
        Qry.ParamByName('total').AsFloat := LTotal;
        Qry.ParamByName('iva').AsFloat := LIva;
        Qry.ParamByName('obs').AsString := 'Importado desde PDF mediante IA';
        Qry.ExecSQL;
        
        // Obtener el ID insertado
        Qry.SQL.Text := 'SELECT LAST_INSERT_ID()';
        Qry.Open;
        LOrderId := Qry.Fields[0].AsInteger;
        Qry.Close;
        
        // Insertar líneas
        for LRec in LOrderLines do
        begin
          Qry.SQL.Text := 
            'INSERT INTO ge_pedidos_lineas (id_pedido, id_articulo, descripcion, cantidad, precio, total) ' +
            'VALUES (:id_pedido, :id_articulo, :desc, :cant, :precio, :total)';
          Qry.ParamByName('id_pedido').AsInteger := LOrderId;
          
          if FindArticleByEan(dmgMain.dbConn, LRec.Ean, LArtId, LArtDesc) then
            Qry.ParamByName('id_articulo').AsInteger := LArtId
          else
            Qry.ParamByName('id_articulo').AsInteger := 1; // Fallback
            
          if LRec.Descripcion <> '' then
            Qry.ParamByName('desc').AsString := LRec.Descripcion
          else
            Qry.ParamByName('desc').AsString := LArtDesc;
            
          Qry.ParamByName('cant').AsFloat := LRec.Cantidad;
          Qry.ParamByName('precio').AsFloat := LRec.Precio;
          Qry.ParamByName('total').AsFloat := LRec.Total;
          Qry.ExecSQL;
        end;
        
        Result := True;
      finally
        Qry.Free;
      end;
    finally
      LOrderLines.Free;
    end;
  except
    on E: Exception do
    begin
      AErrorMsg := E.Message;
      Result := False;
    end;
  end;
  
  LJSONObj.Free;
end;

class function TEdicomProcessor.ExportInvoice(const AInvoiceId: string; const AOutputPath: string; var AErrorMsg: string): Boolean;
var
  Qry: TFDQuery;
  LFileContent: TStringList;
  LInvoiceNum, LFecha, LRefCliente: string;
  LCompanyEan, LClientEan, LDestinoEan, LReceptorEan: string;
  LBase, LIva, LTotal: Double;
  LPorcIva: Double;
  LLineId, LCountLines: Integer;
  LIdTypeEmp, LIdTypeCli, LIdTypeRec: string;
begin
  Result := False;
  if AInvoiceId = '' then Exit;
  
  Qry := TFDQuery.Create(nil);
  LFileContent := TStringList.Create;
  try
    Qry.Connection := dmgMain.dbConn;
    
    // Obtener datos de cabecera de la factura de forma dinámica desde BD
    Qry.SQL.Text := 
      'SELECT f.numero, f.fecha, f.base_imponible, f.iva, f.total, f.referencia, f.empresa_id, f.cliente_id, f.clientedestino_id, ' +
      '  e.gln AS empresa_ean, ed.gln_empresa AS edicom_gln, e.cif_nif AS empresa_cif, ' +
      '  d.ean_facturacion AS destino_ean_facturacion, d.ean_cliente AS destino_ean_cliente, ' +
      '  d.ean_emisor AS destino_ean_emisor, d.ean_receptor AS destino_ean_receptor, ' +
      '  d.cp AS destino_cp, d.pais AS destino_pais, d.departamento AS destino_departamento, ' +
      '  c.nif AS cliente_cif, c.nif AS cliente_nif, c.ean AS cliente_gln, ' +
      '  c.codigo_postal AS cliente_cp, c.codigo_pais AS cliente_pais ' +
      'FROM ge_facturas f ' +
      'JOIN ge_empresas e ON f.empresa_id = e.id ' +
      'LEFT JOIN ge_edicom_config ed ON ed.id_empresa = e.id AND ed.activo = 1 ' +
      'JOIN ge_clientes c ON f.cliente_id = c.id ' +
      'LEFT JOIN ge_clientes_destinos d ON d.id = COALESCE(f.clientedestino_id, (SELECT alb.clientedestino_id FROM ge_albaranes alb WHERE alb.id_factura = f.id AND alb.clientedestino_id IS NOT NULL LIMIT 1)) ' +
      'WHERE f.id = :id LIMIT 1';
    Qry.ParamByName('id').AsString := AInvoiceId;
    Qry.Open;
    
    if Qry.IsEmpty then
    begin
      AErrorMsg := 'No se encontró la factura en la base de datos.';
      Exit;
    end;
    
    LInvoiceNum := Qry.FieldByName('numero').AsString;
    LFecha := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss', Qry.FieldByName('fecha').AsDateTime);
    LRefCliente := Qry.FieldByName('referencia').AsString;
    LBase := Qry.FieldByName('base_imponible').AsFloat;
    LIva := Qry.FieldByName('iva').AsFloat;
    LTotal := Qry.FieldByName('total').AsFloat;
    
    var LEmpCif: string := Trim(Qry.FieldByName('empresa_cif').AsString);
    var LCliCif: string := Trim(Qry.FieldByName('cliente_cif').AsString);
    if LCliCif = '' then LCliCif := Trim(Qry.FieldByName('cliente_nif').AsString);

    // 1. GLN/NIF del Emisor (dinámico desde BD: ge_edicom_config -> ge_empresas)
    LCompanyEan := Trim(Qry.FieldByName('edicom_gln').AsString);
    if LCompanyEan = '' then LCompanyEan := Trim(Qry.FieldByName('empresa_ean').AsString);
    if LCompanyEan = '' then LCompanyEan := LEmpCif;

    if (Length(LCompanyEan) = 13) and (StrToInt64Def(LCompanyEan, 0) > 0) then
      LIdTypeEmp := 'GLN'
    else
      LIdTypeEmp := 'VAT';

    // 2. GLN/NIF del Cliente Comprador BY (dinámico desde BD: ge_clientes_destinos -> ge_clientes)
    // El punto operacional de compra es ean_emisor
    LClientEan := Trim(Qry.FieldByName('destino_ean_emisor').AsString);
    if LClientEan = '' then LClientEan := Trim(Qry.FieldByName('destino_ean_cliente').AsString);
    if LClientEan = '' then LClientEan := Trim(Qry.FieldByName('cliente_gln').AsString);
    if LClientEan = '' then LClientEan := LCliCif;

    // Si contiene múltiples EANs separados por guión, tomar el primero para el XML
    if Pos('-', LClientEan) > 0 then
      LClientEan := Trim(Copy(LClientEan, 1, Pos('-', LClientEan) - 1));

    if (Length(LClientEan) = 13) and (StrToInt64Def(LClientEan, 0) > 0) then
      LIdTypeCli := 'GLN'
    else
      LIdTypeCli := 'VAT';

    // 3. GLN del Lugar de Entrega DP
    LDestinoEan := Trim(Qry.FieldByName('destino_ean_receptor').AsString);
    if LDestinoEan = '' then LDestinoEan := LClientEan;
    if Pos('-', LDestinoEan) > 0 then
      LDestinoEan := Trim(Copy(LDestinoEan, 1, Pos('-', LDestinoEan) - 1));

    // 4. Resolución inteligente de GLN Receptor (IV) según la estructura oficial de Gran Consumo
    var LCp: string := Trim(Qry.FieldByName('destino_cp').AsString);
    if LCp = '' then LCp := Trim(Qry.FieldByName('cliente_cp').AsString);

    var LPais: string := UpperCase(Trim(Qry.FieldByName('destino_pais').AsString));
    if LPais = '' then LPais := UpperCase(Trim(Qry.FieldByName('cliente_pais').AsString));

    // Priorizar ean_facturacion explícito del destino
    LReceptorEan := Trim(Qry.FieldByName('destino_ean_facturacion').AsString);
    if Pos('-', LReceptorEan) > 0 then
      LReceptorEan := Trim(Copy(LReceptorEan, 1, Pos('-', LReceptorEan) - 1));

    // Normalizar corrección para El Corte Inglés: '8422416000508' o '8422416000504'
    if (LReceptorEan = '8422416000508') or (LReceptorEan = '8422416000504') then
      LReceptorEan := '';

    if LReceptorEan = '' then
    begin
      if (UpperCase(LCliCif) = 'A28017895') or
         (Pos('8422416', LClientEan) = 1) or
         (Pos('8422416', LDestinoEan) = 1) or
         (Pos('5600000', LClientEan) = 1) or
         (Pos('5600000', LDestinoEan) = 1) then
      begin
        if (LPais = 'PT') or (LPais = 'PORTUGAL') or (Pos('5600000', LClientEan) = 1) or (Pos('5600000', LDestinoEan) = 1) then
          LReceptorEan := '5600000025895'
        else if (Copy(LCp, 1, 2) = '35') or (Copy(LCp, 1, 2) = '38') then
          LReceptorEan := '8422416200720'
        else
          LReceptorEan := '8422416200508';
      end
      else
      begin
        LReceptorEan := Trim(Qry.FieldByName('destino_ean_receptor').AsString);
        if LReceptorEan = '' then
          LReceptorEan := LClientEan;
      end;
    end;

    if (Length(LReceptorEan) = 13) and (StrToInt64Def(LReceptorEan, 0) > 0) then
      LIdTypeRec := 'GLN'
    else
      LIdTypeRec := 'VAT';

    Qry.Close;
    
    // Contar el número de líneas
    Qry.SQL.Text := 'SELECT COUNT(*) FROM ge_facturas_lineas WHERE factura_id = :id';
    Qry.ParamByName('id').AsString := AInvoiceId;
    Qry.Open;
    LCountLines := Qry.Fields[0].AsInteger;
    Qry.Close;

    LFileContent.Add('<?xml version="1.0" encoding="UTF-8"?>');
    LFileContent.Add('<HEADER>');
    LFileContent.Add(Format('    <Invoice_number>%s</Invoice_number>', [LInvoiceNum]));
    LFileContent.Add('    <Project>ES_ECI_OUT</Project>');
    LFileContent.Add('    <Type>380</Type>');
    LFileContent.Add(Format('    <Date>%s</Date>', [LFecha]));
    LFileContent.Add('    <Currency_code>EUR</Currency_code>');
    
    // PARTIES SU - Vendedor (Dinámico de BD)
    LFileContent.Add('    <PARTIES>');
    LFileContent.Add('        <Qualifier>SU</Qualifier>');
    LFileContent.Add(Format('        <Identifier_type>%s</Identifier_type>', [LIdTypeEmp]));
    LFileContent.Add(Format('        <Identifier>%s</Identifier>', [LCompanyEan]));
    if LEmpCif <> '' then
    begin
      LFileContent.Add(Format('        <Tax_id>%s</Tax_id>', [LEmpCif]));
      LFileContent.Add('        <Tax_id_type>Vat_number</Tax_id_type>');
    end;
    LFileContent.Add('    </PARTIES>');
    
    // PARTIES IV - Receptor / Facturado (Dinámico de BD)
    LFileContent.Add('    <PARTIES>');
    LFileContent.Add('        <Qualifier>IV</Qualifier>');
    LFileContent.Add(Format('        <Identifier_type>%s</Identifier_type>', [LIdTypeRec]));
    LFileContent.Add(Format('        <Identifier>%s</Identifier>', [LReceptorEan]));
    if LCliCif <> '' then
    begin
      LFileContent.Add(Format('        <Tax_id>%s</Tax_id>', [LCliCif]));
      LFileContent.Add('        <Tax_id_type>Vat_number</Tax_id_type>');
    end;
    LFileContent.Add('    </PARTIES>');
    
    // PARTIES BY - Comprador (Dinámico de BD)
    LFileContent.Add('    <PARTIES>');
    LFileContent.Add('        <Qualifier>BY</Qualifier>');
    LFileContent.Add(Format('        <Identifier_type>%s</Identifier_type>', [LIdTypeCli]));
    LFileContent.Add(Format('        <Identifier>%s</Identifier>', [LClientEan]));
    if LCliCif <> '' then
    begin
      LFileContent.Add(Format('        <Tax_id>%s</Tax_id>', [LCliCif]));
      LFileContent.Add('        <Tax_id_type>Vat_number</Tax_id_type>');
    end;
    var LDepto := NormalizeDepartmentCode(Qry.FieldByName('destino_departamento').AsString);
    if LDepto <> '' then
    begin
      LFileContent.Add('        <ADDITIONAL_GROUP_PARTIES>');
      LFileContent.Add('            <Name>GENERAL</Name>');
      LFileContent.Add('            <ADDITIONAL_GROUP_PROPERTY_PARTIES>');
      LFileContent.Add('                <Name>Buyer_department_id</Name>');
      LFileContent.Add(Format('                <Value>%s</Value>', [LDepto]));
      LFileContent.Add('            </ADDITIONAL_GROUP_PROPERTY_PARTIES>');
      LFileContent.Add('        </ADDITIONAL_GROUP_PARTIES>');
    end;
    LFileContent.Add('    </PARTIES>');
    
    // PARTIES DP - Lugar de Entrega / Destino de la Mercancía (Quién recibe - NAD+DP)
    if LDestinoEan <> '' then
    begin
      LFileContent.Add('    <PARTIES>');
      LFileContent.Add('        <Qualifier>DP</Qualifier>');
      if (Length(LDestinoEan) = 13) and (StrToInt64Def(LDestinoEan, 0) > 0) then
        LFileContent.Add('        <Identifier_type>GLN</Identifier_type>')
      else
        LFileContent.Add(Format('        <Identifier_type>%s</Identifier_type>', [LIdTypeCli]));
      LFileContent.Add(Format('        <Identifier>%s</Identifier>', [LDestinoEan]));
      if LCliCif <> '' then
      begin
        LFileContent.Add(Format('        <Tax_id>%s</Tax_id>', [LCliCif]));
        LFileContent.Add('        <Tax_id_type>Vat_number</Tax_id_type>');
      end;
      LFileContent.Add('    </PARTIES>');
    end;
    
    // SUPPORTING_DOCS (Pedido referenciado)
    if LRefCliente <> '' then
    begin
      LFileContent.Add('    <SUPPORTING_DOCS>');
      LFileContent.Add('        <Type>220</Type>');
      LFileContent.Add(Format('        <Identifier>%s</Identifier>', [LRefCliente]));
      LFileContent.Add('    </SUPPORTING_DOCS>');
    end;

    // TAXES
    LPorcIva := 10.0;
    LFileContent.Add('    <TAXES>');
    LFileContent.Add('        <Type>VAT</Type>');
    LFileContent.Add(Format('        <Rate>%s</Rate>', [FloatToStr(LPorcIva).Replace(',', '.')]));
    LFileContent.Add(Format('        <Amount>%s</Amount>', [FloatToStr(LIva).Replace(',', '.')]));
    LFileContent.Add('    </TAXES>');
    
    // TOTALS
    LFileContent.Add('    <TOTALS>');
    LFileContent.Add(Format('        <Total_amount>%s</Total_amount>', [FloatToStr(LTotal).Replace(',', '.')]));
    LFileContent.Add(Format('        <Net_amount>%s</Net_amount>', [FloatToStr(LBase).Replace(',', '.')]));
    LFileContent.Add(Format('        <Gross_amount>%s</Gross_amount>', [FloatToStr(LBase).Replace(',', '.')]));
    LFileContent.Add(Format('        <Taxable_amount>%s</Taxable_amount>', [FloatToStr(LBase).Replace(',', '.')]));
    LFileContent.Add('    </TOTALS>');

    // NOTES
    LFileContent.Add('    <NOTES>');
    LFileContent.Add('        <Type>AAI</Type>');
    LFileContent.Add('        <Text>Factura generada desde GESTION--RAUL ASENCIO</Text>');
    LFileContent.Add('    </NOTES>');

    // ADDITIONAL_GROUP_HEADER (Ubicado exactamente entre <NOTES> y <LINES>)
    LFileContent.Add('    <ADDITIONAL_GROUP_HEADER>');
    LFileContent.Add('        <Name>GENERAL</Name>');
    LFileContent.Add('        <ADDITIONAL_GROUP_PROPERTY_HEADER>');
    LFileContent.Add('            <Name>Total_lines</Name>');
    LFileContent.Add(Format('            <Value>%d</Value>', [LCountLines]));
    LFileContent.Add('        </ADDITIONAL_GROUP_PROPERTY_HEADER>');
    LFileContent.Add('    </ADDITIONAL_GROUP_HEADER>');

    // Obtener líneas de detalle
    Qry.SQL.Text := 
      'SELECT l.descripcion, l.cantidad, l.precio, l.total, l.articulo_id, a.id AS articulo_codigo, a.ean, COALESCE(l.tipo_iva, t.tipo, 10.0) as porc_iva ' +
      'FROM ge_facturas_lineas l ' +
      'LEFT JOIN ge_articulos a ON l.articulo_id = a.id ' +
      'LEFT JOIN ge_tipos_iva t ON a.id_tipo_iva = t.id ' +
      'WHERE l.factura_id = :id ' +
      'ORDER BY l.posicion, l.id';
    Qry.ParamByName('id').AsString := AInvoiceId;
    Qry.Open;
    
    LLineId := 1;
    
    while not Qry.Eof do
    begin
      var LEan: string := Trim(Qry.FieldByName('ean').AsString);
      if LEan = '' then
        LEan := Trim(Qry.FieldByName('articulo_codigo').AsString);

      var LIdTypeLine: string := 'EAN';
      if Length(LEan) = 12 then
        LIdTypeLine := 'UPC';

      var LDesc: string := Qry.FieldByName('descripcion').AsString;
      var LCant: Double := Qry.FieldByName('cantidad').AsFloat;
      var LPrec: Double := Qry.FieldByName('precio').AsFloat;
      var LLinTotal: Double := Qry.FieldByName('total').AsFloat;
      var LLinBase: Double := 0.0;
      
      if not Qry.FieldByName('porc_iva').IsNull then
        LPorcIva := Qry.FieldByName('porc_iva').AsFloat;

      if (LCant > 0) and (LPrec > 0) then
        LLinBase := LCant * LPrec
      else if LLinTotal > 0 then
      begin
        if LPorcIva > 0 then
          LLinBase := LLinTotal / (1.0 + (LPorcIva / 100.0))
        else
          LLinBase := LLinTotal;
      end;
        
      LFileContent.Add('    <LINES>');
      LFileContent.Add(Format('        <Line_ID>%d</Line_ID>', [LLineId]));
      LFileContent.Add(Format('        <Identifier_type>%s</Identifier_type>', [LIdTypeLine]));
      LFileContent.Add(Format('        <Identifier>%s</Identifier>', [LEan]));
      LFileContent.Add(Format('        <Description>%s</Description>', [LDesc]));
      LFileContent.Add(Format('        <Quantity>%s</Quantity>', [FloatToStr(LCant).Replace(',', '.')]));
      LFileContent.Add(Format('        <Unit_gross_price>%s</Unit_gross_price>', [FloatToStr(LPrec).Replace(',', '.')]));
      LFileContent.Add('        <TOTALS_LINES>');
      LFileContent.Add(Format('            <Net_amount>%s</Net_amount>', [FloatToStr(LLinBase).Replace(',', '.')]));
      LFileContent.Add('        </TOTALS_LINES>');
      LFileContent.Add('    </LINES>');
      
      Inc(LLineId);
      Qry.Next;
    end;
    
    LFileContent.Add('</HEADER>');
    
    LFileContent.SaveToFile(AOutputPath, TEncoding.UTF8);
    Result := True;
  finally
    LFileContent.Free;
    Qry.Free;
  end;
end;

end.
