unit IntegracaoSicrediBoletos; interface uses Classes, SysUtils, DB, Variants, Types, Windows, // FireDAC Units uADStanIntf, uADStanOption, uADStanParam, uADStanError, uADDatSManager, uADPhysIntf, uADDAptIntf, uADStanAsync, uADDAptManager, uADGUIxIntf, uADStanDef, uADStanPool, uADPhysManager, uADCompClient, uADCompDataSet, // SuperObject for JSON superobject, litePDF, // The Sicredi API wrapper unit SicrediCobrancaAPI; // Assumes the unit name from your .pas file type EIntegracaoSicrediException = class(Exception); TIntegracaoSicrediBoletos = class private FDBConnection: TADConnection; FSicrediApi: TSicrediCobrancaAPI; FSequencia: Integer; FCodigoBeneficiarioPayload: string; FQuery: TADQuery; // Reusable query object FQueryCarne: TADQuery; // Reusable query object procedure LogError(const AMessage: string); function GetNextValForSequence(const ASequenceName: string): Integer; function ExtractDigits(const S: string): string; function InternalConstruirPayloadBoleto(const ARegistroData: ISuperObject; const ANossoNumeroParaEnvio: string): ISuperObject; function InternalGetBeneficiarioFinalApiData(const ARegistroBoleto: ISuperObject): ISuperObject; public constructor Create(ADBConnection: TADConnection; const AConfig: ISuperObject); destructor Destroy; override; function MergePDFsToString(const APdfContents: TStringDynArray): string; // New merging function function CadastrarParcela(const AModulo: string; ARefParcela: Integer): string; // Returns NossoNumero or empty function CadastrarParcelas(const AModulo: string; const AListaRefParcelas: TIntegerDynArray): Boolean; function CadastrarCarne(const AModuloCarne: string; ARefCarne: Integer): Boolean; function ImprimirParcela(const AModulo: string; ARefParcela: Integer): string; // Returns PDF content or empty function ImprimirParcelas(const AModulo: string; const AListaRefParcelas: TIntegerDynArray; AMergePDFs: Boolean = False): TStringDynArray; // Returns array of PDF contents function ImprimirCarne(const AModuloCarne: string; ARefCarne: Integer; AMergePDFs: Boolean = False): TStringDynArray; end; implementation uses DateUtils; // For FormatDateTime, StrToDate, IncDay, Now function IfThen(Cond:Boolean; cTrue,cFalse:String):String; begin if Cond then Result:=cTrue else Result:=cFalse; end; function SplitString(const S, Delimiters: string): TStringDynArray; var List: TStringList; ResultArray: TStringDynArray; i: Integer; CurrentPos, PrevPos: Integer; begin List := TStringList.Create; try PrevPos := 1; CurrentPos := 1; while CurrentPos <= Length(S) do begin if Pos(S[CurrentPos], Delimiters) > 0 then begin List.Add(Copy(S, PrevPos, CurrentPos - PrevPos)); PrevPos := CurrentPos + 1; end; Inc(CurrentPos); end; List.Add(Copy(S, PrevPos, Length(S) - PrevPos + 1)); SetLength(ResultArray, List.Count); for i := 0 to List.Count - 1 do begin ResultArray[i] := List[i]; end; Result := ResultArray; finally List.Free; end; end; { TIntegracaoSicrediBoletos } constructor TIntegracaoSicrediBoletos.Create(ADBConnection: TADConnection; const AConfig: ISuperObject); var LApiConfig: ISuperObject; begin inherited Create; if not Assigned(ADBConnection) then raise EIntegracaoSicrediException.Create('Conexão com banco de dados (TADConnection) não fornecida.'); if not Assigned(AConfig) then raise EIntegracaoSicrediException.Create('Objeto de configuração (ISuperObject) não fornecido.'); FDBConnection := ADBConnection; // Extract settings for this class if AConfig.asObject.Exists('sequencia') then FSequencia := AConfig.I['sequencia'] else raise EIntegracaoSicrediException.Create('Configuração "sequencia" não encontrada.'); if AConfig.asObject.Exists('codigoBeneficiarioPayload') then FCodigoBeneficiarioPayload := AConfig.S['codigoBeneficiarioPayload'] else raise EIntegracaoSicrediException.Create('Configuração "codigoBeneficiarioPayload" não encontrada.'); // Prepare config for SicrediCobrancaAPI // Assuming AConfig contains all necessary fields for TSicrediCobrancaAPI constructor // Adjust if your AConfig structure is different FSicrediApi := TSicrediCobrancaAPI.Create( AConfig.S['developerApiKey'], AConfig.S['authUsername'], AConfig.S['authPassword'], AConfig.S['cooperativa'], AConfig.S['posto'], AConfig.S['codigoBeneficiarioApi'], // Maps to ACodigoBeneficiario in TSicrediCobrancaAPI AConfig.S['environment'] ); FQuery := TADQuery.Create(nil); FQuery.Connection := FDBConnection; FQueryCarne := TADQuery.Create(nil); FQueryCarne.Connection := FDBConnection; end; destructor TIntegracaoSicrediBoletos.Destroy; begin FreeAndNil(FSicrediApi); FreeAndNil(FQueryCarne); FreeAndNil(FQuery); inherited Destroy; end; procedure TIntegracaoSicrediBoletos.LogError(const AMessage: string); begin OutputDebugString(PChar('IntegracaoSicrediBoletos Error: ' + AMessage)); // Consider a more robust logging mechanism for production. end; function TIntegracaoSicrediBoletos.ExtractDigits(const S: string): string; var I: Integer; begin Result := ''; for I := 1 to Length(S) do begin if S[I] in ['0'..'9'] then Result := Result + S[I]; end; end; function TIntegracaoSicrediBoletos.GetNextValForSequence(const ASequenceName: string): Integer; begin Result := -1; FQuery.SQL.Clear; FQuery.SQL.Add('SELECT nextval(:seqName) AS novo_numero'); FQuery.ParamByName('seqName').AsString := ASequenceName; try FQuery.Open; if not FQuery.IsEmpty then Result := FQuery.FieldByName('novo_numero').AsInteger else LogError('Erro ao obter próximo valor da sequence ' + ASequenceName + ': consulta vazia.'); except on E: Exception do begin LogError('Exceção ao obter próximo valor da sequence ' + ASequenceName + ': ' + E.Message); raise; // Re-raise the exception end; end; FQuery.Close; end; function TIntegracaoSicrediBoletos.InternalGetBeneficiarioFinalApiData(const ARegistroBoleto: ISuperObject): ISuperObject; var LClienteRow: ISuperObject; LMunicipioRow: ISuperObject; LCodigoMunicipio, LCodigoMunicipioIBGE, LCidadeNome, LTipoPessoa, LDocumento: string; begin Result := SO; // Create an empty JSON object // Simplified logic from PHP - adapt for real-world use FQuery.SQL.Clear; FQuery.SQL.Add('SELECT nome, cgc, endereco, bairro, uf, cep, codigo_municipio_ibge, codigo_municipio FROM aac_clientes LIMIT 1'); // TODO: Add proper WHERE clause to find the correct cliente based on ARegistroBoleto or other context try FQuery.Open; if FQuery.IsEmpty then begin LogError('Beneficiário final (cliente) não encontrado na view aac_clientes.'); FQuery.Close; Exit; // Return empty object end; LClienteRow := SO; // Using SuperObject to hold row data for easier access LClienteRow.S['nome'] := FQuery.FieldByName('nome').AsString; LClienteRow.S['cgc'] := FQuery.FieldByName('cgc').AsString; LClienteRow.S['endereco'] := FQuery.FieldByName('endereco').AsString; LClienteRow.S['bairro'] := FQuery.FieldByName('bairro').AsString; LClienteRow.S['uf'] := FQuery.FieldByName('uf').AsString; LClienteRow.S['cep'] := FQuery.FieldByName('cep').AsString; LClienteRow.S['codigo_municipio_ibge'] := FQuery.FieldByName('codigo_municipio_ibge').AsString; LClienteRow.S['codigo_municipio'] := FQuery.FieldByName('codigo_municipio').AsString; FQuery.Close; LDocumento := ExtractDigits(LClienteRow.S['cgc']); if Length(LDocumento) = 11 then LTipoPessoa := 'PESSOA_FISICA' else if Length(LDocumento) = 14 then LTipoPessoa := 'PESSOA_JURIDICA' else LTipoPessoa := 'PESSOA_JURIDICA'; // Default or handle error Result.S['tipoPessoa'] := LTipoPessoa; Result.S['documento'] := LDocumento; Result.S['nome'] := Copy(Trim(LClienteRow.S['nome']), 1, 40); if Trim(LClienteRow.S['endereco']) <> '' then Result.S['logradouro'] := Copy(Trim(LClienteRow.S['endereco']), 1, 40); LCidadeNome := 'CIDADE INDEFINIDA'; LCodigoMunicipio := Trim(LClienteRow.S['codigo_municipio']); LCodigoMunicipioIBGE := Trim(LClienteRow.S['codigo_municipio_ibge']); if LCodigoMunicipio <> '' then begin FQuery.SQL.Clear; FQuery.SQL.Add('SELECT nome_municipio FROM aac_municipios WHERE codigo_municipio = :codMun'); FQuery.ParamByName('codMun').AsString := LCodigoMunicipio; FQuery.Open; if not FQuery.IsEmpty then LCidadeNome := Trim(FQuery.FieldByName('nome_municipio').AsString) else LogError('Nome do município não encontrado para codigo_municipio: ' + LCodigoMunicipio); FQuery.Close; end else if LCodigoMunicipioIBGE <> '' then begin FQuery.SQL.Clear; FQuery.SQL.Add('SELECT nome_municipio FROM aac_municipios WHERE codigo_municipio_ibge = :codMunIBGE'); FQuery.ParamByName('codMunIBGE').AsString := LCodigoMunicipioIBGE; FQuery.Open; if not FQuery.IsEmpty then LCidadeNome := Trim(FQuery.FieldByName('nome_municipio').AsString) else LogError('Nome do município não encontrado para codigo_municipio_ibge: ' + LCodigoMunicipioIBGE); FQuery.Close; end; Result.S['cidade'] := Copy(LCidadeNome, 1, 25); if Trim(LClienteRow.S['uf']) <> '' then Result.S['uf'] := Copy(Trim(LClienteRow.S['uf']), 1, 2); if Trim(LClienteRow.S['cep']) <> '' then Result.S['cep'] := ExtractDigits(LClienteRow.S['cep']); // Clean up empty fields from result - SuperObject doesn't add if value is variant null by default // but good practice if fields could be added with empty strings explicitly. except on E: Exception do begin FQuery.Close; // Ensure query is closed on error LogError('Erro ao buscar dados do beneficiário final: ' + E.Message); Result := SO; // Return empty on error end; end; end; function TIntegracaoSicrediBoletos.InternalConstruirPayloadBoleto(const ARegistroData: ISuperObject; const ANossoNumeroParaEnvio: string): ISuperObject; var LPayload, LPagador, LBeneficiarioFinalData: ISuperObject; LDocumentoPagador, LTipoPessoaPagador, LDataVencStr, LDataDescStr : string; LDataVenc, LDataDesc: TDateTime; begin Result := nil; // Default to nil if critical data is missing if not Assigned(ARegistroData) then begin LogError('Dados do registro (ARegistroData) não fornecidos para construir payload.'); Exit; end; LPayload := SO; LPayload.S['codigoBeneficiario'] := FCodigoBeneficiarioPayload; LPayload.S['tipoCobranca'] := 'HIBRIDO'; LPagador := SO; LDocumentoPagador := ExtractDigits(ARegistroData.S['documento']); if Length(LDocumentoPagador) = 11 then LTipoPessoaPagador := 'PESSOA_FISICA' else if Length(LDocumentoPagador) = 14 then LTipoPessoaPagador := 'PESSOA_JURIDICA' else begin LogError('Documento do pagador inválido: ' + ARegistroData.S['documento']); Exit; // Cannot proceed without valid pagador document type end; LPagador.S['tipoPessoa'] := LTipoPessoaPagador; LPagador.S['documento'] := LDocumentoPagador; LPagador.S['nome'] := Copy(Trim(ARegistroData.S['sacado']), 1, 40); LPagador.S['endereco'] := Copy(Trim(ARegistroData.S['logradouro'] + IfThen(Trim(ARegistroData.S['numerolog']) <> '', ', ' + Trim(ARegistroData.S['numerolog']), '') + IfThen(Trim(ARegistroData.S['complemento']) <> '', ' - ' + Trim(ARegistroData.S['complemento']), '') ), 1, 40); LPagador.S['cidade'] := Copy(Trim(ARegistroData.S['cidade']), 1, 40); // API says 25, PHP example says 40, check docs. LPagador.S['uf'] := Copy(Trim(ARegistroData.S['uf']), 1, 2); LPagador.S['cep'] := ExtractDigits(ARegistroData.S['cep']); LPayload.O['pagador'] := LPagador; // Beneficiario Final LBeneficiarioFinalData := InternalGetBeneficiarioFinalApiData(ARegistroData); // Pass ARegistroData for context if needed if Assigned(LBeneficiarioFinalData) and (LBeneficiarioFinalData.AsObject.Count > 0) then // Check if object has properties begin LPayload.O['beneficiarioFinal'] := LBeneficiarioFinalData; end; LPayload.S['especieDocumento'] := 'DUPLICATA_MERCANTIL_INDICACAO'; LPayload.S['nossoNumero'] := ANossoNumeroParaEnvio; LPayload.S['seuNumero'] := ANossoNumeroParaEnvio; // Typically same as nossoNumero LDataVencStr := Trim(ARegistroData.S['vencimento']); if LDataVencStr = '' then begin LogError('Data de vencimento não fornecida no registro.'); Exit; end; try // Assuming YYYY-MM-DD or a format StrToDate understands with current settings // If it's DD/MM/YYYY from DB, adjust parsing or settings LDataVenc := StrToDate(LDataVencStr); // Or ParseDateTime if format is fixed LPayload.S['dataVencimento'] := FormatDateTime('yyyy-mm-dd', LDataVenc); except on E: Exception do begin LogError('Data de vencimento inválida no registro: ' + LDataVencStr + '. Erro: ' + E.Message); Exit; end; end; LPayload.D['valor'] := ARegistroData.D['valor']; // Use AsCurrency or AsFloat if ARegistroData.asObject.Exists('vlrdesconto') and (ARegistroData.D['vlrdesconto'] > 0) and ARegistroData.asObject.Exists('dtdesconto') and (Trim(ARegistroData.S['dtdesconto']) <> '') then begin LDataDescStr := Trim(ARegistroData.S['dtdesconto']); try LDataDesc := StrToDate(LDataDescStr); LPayload.S['tipoDesconto'] := 'VALOR'; // Or "PERCENTUAL_DATA_FIXA" etc. LPayload.D['valorDesconto1'] := ARegistroData.D['vlrdesconto']; LPayload.S['dataDesconto1'] := FormatDateTime('yyyy-mm-dd', LDataDesc); except on E: Exception do begin LogError('Data de desconto inválida: ' + LDataDescStr + '. Erro: ' + E.Message + '. Desconto não será aplicado.'); // Do not add discount fields if date is invalid end; end; end; if ARegistroData.asObject.Exists('indjuros') and (ARegistroData.D['indjuros'] > 0) then begin LPayload.S['tipoJuros'] := 'PERCENTUAL'; // Check API docs for types: PERCENTUAL, VALOR_DIA_CORRIDO, ISENTO etc. LPayload.D['juros'] := ARegistroData.D['indjuros']; end; if ARegistroData.asObject.Exists('indmulta') and (ARegistroData.D['indmulta'] > 0) then begin LPayload.D['multa'] := ARegistroData.D['indmulta']; end; // Informativos e Mensagens (Exemplo - Adicionar se presentes em ARegistroData) // if ARegistroData.Exists('informativos') and (ARegistroData.S['informativos'] <> '') then // begin // LInformativos := TStringList.Create; // try // LInformativos.CommaText := ARegistroData.S['informativos']; // Assuming comma-separated // LPayload.A['informativos'] := SO(LInformativos); // Convert StringList to JSON array // finally // LInformativos.Free; // end; // end; Result := LPayload; end; function TIntegracaoSicrediBoletos.CadastrarParcela(const AModulo: string; ARefParcela: Integer): string; var LNumeroOriginalTriNumeroStr: string; LNumeroOriginalTriNumero: Integer; LRegistroData: ISuperObject; LDadosBoletoApi, LApiResponse: ISuperObject; LApiNossoNumero, LTxid, LQrCode, LLinhaDigitavel, LCodigoBarras: string; LSequenceName: string; LWasProcessed: Boolean; begin Result := ''; // Default to empty string for failure/not processed LNumeroOriginalTriNumero := -1; LWasProcessed := False; FDBConnection.StartTransaction; // Start transaction try // 1. Obter/Criar o registro em tri_numero FQuery.SQL.Clear; FQuery.SQL.Add('SELECT numero FROM tri_numero WHERE sequencia = :seq AND modulo = :mod AND ref_parcela = :ref'); FQuery.ParamByName('seq').AsInteger := FSequencia; FQuery.ParamByName('mod').AsString := AModulo; FQuery.ParamByName('ref').AsInteger := ARefParcela; FQuery.Open; if FQuery.IsEmpty then begin FQuery.Close; LSequenceName := 'tri_numero_g' + IntToStr(FSequencia); LNumeroOriginalTriNumero := GetNextValForSequence(LSequenceName); if LNumeroOriginalTriNumero = -1 then raise EIntegracaoSicrediException.Create('Falha ao obter próximo número da sequence ' + LSequenceName); FQuery.SQL.Clear; FQuery.SQL.Add('INSERT INTO tri_numero (sequencia, numero, modulo, ref_parcela) VALUES (:seq, :num, :mod, :ref)'); FQuery.ParamByName('seq').AsInteger := FSequencia; FQuery.ParamByName('num').AsInteger := LNumeroOriginalTriNumero; FQuery.ParamByName('mod').AsString := AModulo; FQuery.ParamByName('ref').AsInteger := ARefParcela; FQuery.ExecSQL; end else begin LNumeroOriginalTriNumero := FQuery.FieldByName('numero').AsInteger; FQuery.Close; end; LNumeroOriginalTriNumeroStr := IntToStr(LNumeroOriginalTriNumero); // 2. Pegar detalhes da tabela tri_numero_registro FQuery.SQL.Clear; // Select all fields needed for _construirPayloadBoleto and status check FQuery.SQL.Add('SELECT status, linhadigitavel, txid, qrcode, codigobarras, ' + 'documento, sacado, logradouro, numerolog, complemento, cidade, uf, cep, ' + 'vencimento, valor, vlrdesconto, dtdesconto, indjuros, indmulta ' + 'FROM tri_numero_registro WHERE sequencia = :seq AND numero = :num'); FQuery.ParamByName('seq').AsInteger := FSequencia; FQuery.ParamByName('num').AsInteger := LNumeroOriginalTriNumero; FQuery.Open; if FQuery.IsEmpty then begin FQuery.Close; LogError('Registro não encontrado em tri_numero_registro para sequencia ' + IntToStr(FSequencia) + ', numero ' + LNumeroOriginalTriNumeroStr + '. Crie este registro primeiro com os dados do boleto.'); FDBConnection.Rollback; // Rollback on error Exit; end; LRegistroData := SO; // Populate SuperObject from query for easier use LRegistroData.I['status'] := FQuery.FieldByName('status').AsInteger; LRegistroData.S['linhadigitavel'] := FQuery.FieldByName('linhadigitavel').AsString; LRegistroData.S['txid'] := FQuery.FieldByName('txid').AsString; // Handle nulls if necessary LRegistroData.S['qrcode'] := FQuery.FieldByName('qrcode').AsString; LRegistroData.S['codigobarras'] := FQuery.FieldByName('codigobarras').AsString; LRegistroData.S['documento'] := FQuery.FieldByName('documento').AsString; LRegistroData.S['sacado'] := FQuery.FieldByName('sacado').AsString; LRegistroData.S['logradouro'] := FQuery.FieldByName('logradouro').AsString; LRegistroData.S['numerolog'] := FQuery.FieldByName('numerolog').AsString; LRegistroData.S['complemento'] := FQuery.FieldByName('complemento').AsString; LRegistroData.S['cidade'] := FQuery.FieldByName('cidade').AsString; LRegistroData.S['uf'] := FQuery.FieldByName('uf').AsString; LRegistroData.S['cep'] := FQuery.FieldByName('cep').AsString; LRegistroData.S['vencimento'] := FormatDateTime('yyyy-mm-dd', FQuery.FieldByName('vencimento').AsDateTime); // Store as string LRegistroData.D['valor'] := FQuery.FieldByName('valor').AsCurrency; // Store as numeric type if not FQuery.FieldByName('vlrdesconto').IsNull then LRegistroData.D['vlrdesconto'] := FQuery.FieldByName('vlrdesconto').AsCurrency else LRegistroData.D['vlrdesconto'] := 0.0; if not FQuery.FieldByName('dtdesconto').IsNull then LRegistroData.S['dtdesconto'] := FormatDateTime('yyyy-mm-dd', FQuery.FieldByName('dtdesconto').AsDateTime) else LRegistroData.S['dtdesconto'] := ''; if not FQuery.FieldByName('indjuros').IsNull then LRegistroData.D['indjuros'] := FQuery.FieldByName('indjuros').AsCurrency else LRegistroData.D['indjuros'] := 0.0; if not FQuery.FieldByName('indmulta').IsNull then LRegistroData.D['indmulta'] := FQuery.FieldByName('indmulta').AsCurrency else LRegistroData.D['indmulta'] := 0.0; FQuery.Close; // Se linhadigitavel existe e status é 3, já foi enviado com sucesso. if (Trim(LRegistroData.S['linhadigitavel']) <> '') and (LRegistroData.I['status'] = 3) then begin // LogError: Boleto já processado. LWasProcessed := True; Result := LNumeroOriginalTriNumeroStr; // Retorna o nossoNumero usado pela API (que é o numero_original) FDBConnection.Commit; // Commit as it's a successful read of existing data Exit; end; // 3. Montar array para dadosBoleto LDadosBoletoApi := InternalConstruirPayloadBoleto(LRegistroData, LNumeroOriginalTriNumeroStr); if not Assigned(LDadosBoletoApi) then begin LogError('Não foi possível construir o payload do boleto para o número original: ' + LNumeroOriginalTriNumeroStr); FDBConnection.Rollback; Exit; end; // 4. Tentar enviar pela API try LApiResponse := FSicrediApi.CadastrarBoleto(LDadosBoletoApi); if Assigned(LApiResponse) and LApiResponse.asObject.Exists('nossoNumero') and LApiResponse.asObject.Exists('linhaDigitavel') then begin LApiNossoNumero := LApiResponse.S['nossoNumero']; // Este é o nossoNumero confirmado/gerado pela API LTxid := LApiResponse.S['txid']; LQrCode := LApiResponse.S['qrCode']; LLinhaDigitavel := LApiResponse.S['linhaDigitavel']; LCodigoBarras := LApiResponse.S['codigoBarras']; // Atualizar tri_numero_registro com status = 3 (sucesso) FQuery.SQL.Clear; FQuery.SQL.Add('UPDATE tri_numero_registro SET ' + 'txid = :txid, qrcode = :qrcode, linhadigitavel = :ld, codigobarras = :cb, status = 3 ' + 'WHERE sequencia = :seq AND numero = :num'); FQuery.ParamByName('txid').AsString := LTxid; FQuery.ParamByName('qrcode').AsString := LQrCode; FQuery.ParamByName('ld').AsString := LLinhaDigitavel; FQuery.ParamByName('cb').AsString := LCodigoBarras; FQuery.ParamByName('seq').AsInteger := FSequencia; FQuery.ParamByName('num').AsInteger := LNumeroOriginalTriNumero; FQuery.ExecSQL; Result := LApiNossoNumero; // Retorna o nossoNumero da API LWasProcessed := True; end else begin LogError('Resposta da API Sicredi inválida ou incompleta após cadastro: ' + IfThen(Assigned(LApiResponse), LApiResponse.AsJSON, 'NULO')); // Não modificar o status em caso de falha na API, conforme solicitado. FDBConnection.Rollback; // Rollback on API error Exit; end; except on E: Exception do begin LogError('Erro ao cadastrar boleto via SicrediCobrancaAPI: ' + E.Message); // Não modificar o status em caso de falha na API, conforme solicitado. FDBConnection.Rollback; // Rollback on exception Exit; end; end; FDBConnection.Commit; // Commit transaction if all successful except on E: Exception do begin FDBConnection.Rollback; // Rollback on any other exception LogError('Exceção geral em CadastrarParcela: ' + E.Message); raise; // Re-raise to inform caller end; end; end; function TIntegracaoSicrediBoletos.CadastrarParcelas(const AModulo: string; const AListaRefParcelas: TIntegerDynArray): Boolean; var I: Integer; LRefParcela: Integer; LResultadoCadastro: string; begin Result := True; // Assume success for I := Low(AListaRefParcelas) to High(AListaRefParcelas) do begin LRefParcela := AListaRefParcelas[I]; LResultadoCadastro := CadastrarParcela(AModulo, LRefParcela); if LResultadoCadastro = '' then // Failure if empty string is returned begin LogError('Falha ao cadastrar parcela ' + AModulo + '/' + IntToStr(LRefParcela) + ' dentro de CadastrarParcelas. Abortando.'); Result := False; Break; // Exit loop on first failure end; end; end; function TIntegracaoSicrediBoletos.CadastrarCarne(const AModuloCarne: string; ARefCarne: Integer): Boolean; var LTabelaParcelas, LNomeBaseTabela, LModuloAplicacao: string; LRefParcelaCarne: Integer; LResultadoCadastro: string; I: Integer; PathParts: TStringDynArray; begin Result := True; // Assume success LNomeBaseTabela := AModuloCarne; if Pos('.', AModuloCarne) > 0 then // Equivalent to PHP strpos !== false begin // Simple split logic, assumes last part after '.' is base name // For more complex paths, a dedicated function might be needed PathParts := SplitString(AModuloCarne, '.'); // Delphi XE+, for D7 use manual split or StringReplace if Length(PathParts) > 0 then LNomeBaseTabela := PathParts[High(PathParts)]; end; LTabelaParcelas := AModuloCarne + '_parcelas'; // Example: 'iptu_parcelas' or 'schema.iptu_parcelas' // Basic validation for table name to prevent SQL injection // This is very basic, ensure AModuloCarne is from a trusted source or sanitize more thoroughly for I := 1 to Length(LTabelaParcelas) do if not (LTabelaParcelas[I] in ['a'..'z', 'A'..'Z', '0'..'9', '_', '.']) then raise EIntegracaoSicrediException.Create('Nome de módulo inválido para construção da tabela de parcelas: ' + LTabelaParcelas); FQueryCarne.SQL.Clear; FQueryCarne.SQL.Add('SELECT referencial FROM ' + LTabelaParcelas + ' WHERE referencial_carne = :refCarne AND status = ''0'''); FQueryCarne.ParamByName('refCarne').AsInteger := ARefCarne; try FQueryCarne.Open; if FQueryCarne.IsEmpty then begin // LogError: Nenhuma parcela pendente. FQueryCarne.Close; Exit; // No pending parcels, considered success. end; LModuloAplicacao := LNomeBaseTabela; while not FQueryCarne.Eof do begin LRefParcelaCarne := FQueryCarne.FieldByName('referencial').AsInteger; LResultadoCadastro := CadastrarParcela(LModuloAplicacao, LRefParcelaCarne); if LResultadoCadastro = '' then begin LogError('Falha ao cadastrar parcela ' + LModuloAplicacao + '/' + IntToStr(LRefParcelaCarne) + ' do carnê ' + IntToStr(ARefCarne) + '. Abortando cadastro do carnê.'); Result := False; Break; end; FQueryCarne.Next; end; FQueryCarne.Close; except on E: Exception do begin FQueryCarne.Close; LogError('Erro ao buscar parcelas do carnê ' + IntToStr(ARefCarne) + ' em ' + LTabelaParcelas + ': ' + E.Message); Result := False; end; end; end; function TIntegracaoSicrediBoletos.ImprimirParcela(const AModulo: string; ARefParcela: Integer): string; var LResultadoCadastro: string; LNumeroOriginalTriNumero: Integer; LLinhaDigitavel, LPdfContent: string; begin Result := ''; // Default to empty string for failure // 1. Garantir que o boleto esteja registrado/atualizado. LResultadoCadastro := CadastrarParcela(AModulo, ARefParcela); if LResultadoCadastro = '' then // Failed to ensure registration begin LogError('Falha ao garantir o cadastro da parcela ' + AModulo + '/' + IntToStr(ARefParcela) + ' antes de imprimir.'); Exit; end; // 2. Obter numero_original_tri_numero para buscar a linha digitável correta. FQuery.SQL.Clear; FQuery.SQL.Add('SELECT numero FROM tri_numero WHERE sequencia = :seq AND modulo = :mod AND ref_parcela = :ref'); FQuery.ParamByName('seq').AsInteger := FSequencia; FQuery.ParamByName('mod').AsString := AModulo; FQuery.ParamByName('ref').AsInteger := ARefParcela; FQuery.Open; if FQuery.IsEmpty then begin FQuery.Close; LogError('Não foi possível encontrar o registro em tri_numero para ' + AModulo + '/' + IntToStr(ARefParcela) + ' após a chamada de cadastro.'); Exit; end; LNumeroOriginalTriNumero := FQuery.FieldByName('numero').AsInteger; FQuery.Close; // 3. Buscar linhadigitavel em tri_numero_registro. FQuery.SQL.Clear; FQuery.SQL.Add('SELECT linhadigitavel FROM tri_numero_registro WHERE sequencia = :seq AND numero = :num AND status = 3'); FQuery.ParamByName('seq').AsInteger := FSequencia; FQuery.ParamByName('num').AsInteger := LNumeroOriginalTriNumero; FQuery.Open; if FQuery.IsEmpty or (Trim(FQuery.FieldByName('linhadigitavel').AsString) = '') then begin FQuery.Close; LogError('Linha digitável não encontrada ou inválida em tri_numero_registro para sequencia ' + IntToStr(FSequencia) + ', numero ' + IntToStr(LNumeroOriginalTriNumero) + ' (status=3).'); Exit; end; LLinhaDigitavel := Trim(FQuery.FieldByName('linhadigitavel').AsString); FQuery.Close; // 4. Chamar API para pegar PDF try LPdfContent := FSicrediApi.ImprimirBoletoPDF(LLinhaDigitavel); // Basic check for PDF content (Indy returns string, could be binary) // Delphi 7 strings are AnsiString. %PDF- is a common PDF header. if (Length(LPdfContent) > 4) and (Pos('%PDF-', LPdfContent) = 1) then // Pos is 1-based begin Result := LPdfContent; end else begin LogError('Conteúdo retornado pela API de impressão não parece ser um PDF válido para linha digitável: ' + LLinhaDigitavel + '. Tamanho: ' + IntToStr(Length(LPdfContent)) + '. Início: ' + Copy(LPdfContent, 1, 20)); end; except on E: Exception do begin LogError('Erro ao imprimir boleto via SicrediCobrancaAPI para linha digitável ' + LLinhaDigitavel + ': ' + E.Message); // Result remains empty end; end; end; function TIntegracaoSicrediBoletos.ImprimirParcelas(const AModulo: string; const AListaRefParcelas: TIntegerDynArray; AMergePDFs: Boolean): TStringDynArray; var I: Integer; LRefParcela: Integer; LPdfContent: string; LPdfs: TStringList; // Use TStringList for dynamic array of strings begin SetLength(Result, 0); // Initialize empty dynamic array LPdfs := TStringList.Create; try for I := Low(AListaRefParcelas) to High(AListaRefParcelas) do begin LRefParcela := AListaRefParcelas[I]; LPdfContent := ImprimirParcela(AModulo, LRefParcela); if LPdfContent = '' then begin LogError('Falha ao imprimir parcela ' + AModulo + '/' + IntToStr(LRefParcela) + ' dentro de ImprimirParcelas. Abortando.'); SetLength(Result, 0); // Clear result on failure Exit; // Return empty array on first failure end; LPdfs.Add(LPdfContent); end; if AMergePDFs then begin // LPdfContent := MergePDFsToString(LPdfsList); end; // Convert TStringList to TStringDynArray for the result SetLength(Result, LPdfs.Count); for I := 0 to LPdfs.Count - 1 do Result[I] := LPdfs[I]; finally LPdfs.Free; end; end; function TIntegracaoSicrediBoletos.ImprimirCarne(const AModuloCarne: string; ARefCarne: Integer; AMergePDFs: Boolean): TStringDynArray; var LTabelaParcelas, LNomeBaseTabela, LModuloAplicacao: string; LRefParcelaCarne: Integer; LPdfContent: string; LPdfsList: TStringList; I: Integer; PathParts: TStringDynArray; begin SetLength(Result, 0); // Initialize empty dynamic array LPdfsList := TStringList.Create; try try LNomeBaseTabela := AModuloCarne; if Pos('.', AModuloCarne) > 0 then begin PathParts := SplitString(AModuloCarne, '.'); if Length(PathParts) > 0 then LNomeBaseTabela := PathParts[High(PathParts)]; end; LTabelaParcelas := AModuloCarne + '_parcelas'; for I := 1 to Length(LTabelaParcelas) do if not (LTabelaParcelas[I] in ['a'..'z', 'A'..'Z', '0'..'9', '_', '.']) then raise EIntegracaoSicrediException.Create('Nome de módulo inválido para construção da tabela de parcelas: ' + LTabelaParcelas); FQueryCarne.SQL.Clear; // Consider if status='0' is always the criteria for printing, or if paid/processed slips might also be reprinted. FQueryCarne.SQL.Add('SELECT referencial FROM ' + LTabelaParcelas + ' WHERE referencial_carne = :refCarne AND status = ''0'''); FQueryCarne.ParamByName('refCarne').AsInteger := ARefCarne; FQueryCarne.Open; if FQueryCarne.IsEmpty then begin // LogError: Nenhuma parcela encontrada para impressão do carnê. FQueryCarne.Close; Exit; // Returns empty array, which is correct. end; LModuloAplicacao := LNomeBaseTabela; while not FQueryCarne.Eof do begin LRefParcelaCarne := FQueryCarne.FieldByName('referencial').AsInteger; LPdfContent := ImprimirParcela(LModuloAplicacao, LRefParcelaCarne); if LPdfContent = '' then begin LogError('Falha ao imprimir parcela ' + LModuloAplicacao + '/' + IntToStr(LRefParcelaCarne) + ' do carnê ' + IntToStr(ARefCarne) + '. Abortando impressão do carnê.'); SetLength(Result, 0); // Clear result on failure FQueryCarne.Close; Exit; end; LPdfsList.Add(LPdfContent); FQueryCarne.Next; end; FQueryCarne.Close; if AMergePDFs then LogError('Funcionalidade de mesclar PDFs não implementada.'); SetLength(Result, LPdfsList.Count); for I := 0 to LPdfsList.Count - 1 do Result[I] := LPdfsList[I]; except on E: Exception do begin FQueryCarne.Close; // Ensure query is closed on error LogError('Erro ao buscar parcelas do carnê ' + IntToStr(ARefCarne) + ' para impressão: ' + E.Message); SetLength(Result, 0); // Clear result on failure // Optionally re-raise E if it's critical end; end; finally LPdfsList.Free; end; end; function TIntegracaoSicrediBoletos.MergePDFsToString(const APdfContents: TStringDynArray): string; var MergedPDF: TLitePDF; SourcePDF: TLitePDF; I: Integer; PdfDataPtr: PByte; DataLength: LongWord; CurrentPdfContent: AnsiString; // Use AnsiString for raw byte data begin Result := ''; if Length(APdfContents) = 0 then begin LogError('MergePDFsToString: No PDF content to merge.'); Exit; end; if Length(APdfContents) = 1 then begin LogError('MergePDFsToString: Only one PDF, no merging needed.'); Result := APdfContents[0]; Exit; end; MergedPDF := nil; SourcePDF := nil; try MergedPDF := TLitePDF.Create; MergedPDF.CreateMemDocument; // Create the destination PDF in memory for I := 0 to Length(APdfContents) - 1 do begin CurrentPdfContent := AnsiString(APdfContents[I]); // Ensure it's AnsiString if Length(CurrentPdfContent) = 0 then begin LogError('MergePDFsToString: Skipping empty PDF content at index ' + IntToStr(I)); Continue; end; SourcePDF := TLitePDF.Create; try // Load the current PDF from its string content DataLength := Length(CurrentPdfContent); if DataLength > 0 then begin PdfDataPtr := PByte(PAnsiChar(CurrentPdfContent)); SourcePDF.LoadFromData(PdfDataPtr, DataLength, '', False); // Empty password, not for update // Add all pages from the source PDF to the merged PDF if SourcePDF.GetPageCount > 0 then begin MergedPDF.AddPagesFrom(SourcePDF, 0, SourcePDF.GetPageCount); end else begin LogError('MergePDFsToString: Source PDF at index ' + IntToStr(I) + ' has no pages.'); end; end; finally SourcePDF.Close; // Important: Close releases internal structures SourcePDF.Free; SourcePDF := nil; end; end; // Save the merged PDF to a string DataLength := 0; // First call to get the required buffer size if MergedPDF.SaveToData(nil, DataLength) then begin if DataLength > 0 then begin SetLength(CurrentPdfContent, DataLength); // Resize AnsiString to fit PdfDataPtr := PByte(PAnsiChar(CurrentPdfContent)); // Second call to get the actual data if MergedPDF.SaveToData(PdfDataPtr, DataLength) then begin Result := string(CurrentPdfContent); // Convert AnsiString to standard string if needed, though in D7 they are compatible end else begin LogError('MergePDFsToString: Failed to save merged PDF data (2nd call). Error: ' + MergedPDF.getLastErrorMessage); end; end else begin LogError('MergePDFsToString: Merged PDF has no content to save.'); end; end else begin LogError('MergePDFsToString: Failed to get merged PDF data size (1st call). Error: ' + MergedPDF.getLastErrorMessage); end; except on E: TLitePDFException do begin LogError('MergePDFsToString: LitePDFException - Code: ' + IntToStr(E.getCode) + ', Msg: ' + E.getMessage); Result := ''; // Ensure result is empty on error end; on E: Exception do begin LogError('MergePDFsToString: General Exception - ' + E.Message); Result := ''; // Ensure result is empty on error end; end; // Ensure MergedPDF is closed and freed if Assigned(MergedPDF) then begin MergedPDF.Close; MergedPDF.Free; end; end; // --- End of New PDF Merging Function --- initialization // Register TIntegerDynArray and TStringDynArray for Variants if needed for RTTI or COM, // but typically not required for direct usage like this. finalization end.