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. ITBI / e-GOV versão 2.2.226.04


   

INSTRUÇÕES


Para emitir a guia de pagamento você deve informar acima a inscrição do seu imóvel.
Estes dados poderão ser obtidos nas guias ou carnês de ITBI ou IPTU de anos anteriores.
Veja abaixo um exemplo de carnê genérico indicando onde você pode obter esses dados.

Qualquer dúvida, entre em contato com o Departamento de Receita e Fiscalização Fazendária da Prefeitura.



Atenção: a impressão deve ser feita em impressora laser ou jato de tinta. Tire as margens do papel para imprimir.