NotesWhat is notes.io?

Notes brand slogan

Notes - notes.io

package helpers;

import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.List;
import java.util.Collections;

public class SpannerQueryHelper {

private final SpannerUtil spanner;

public SpannerQueryHelper(SpannerUtil spanner) {
this.spanner = spanner;
}

/**
* Executa SELECT simples:
* spanner.query("TbLoanInformationStatic", { LoanNumber: "123", Status: "Cancelado" })
*/
public List<Map<String, Object>> query(String table, Map<String, Object> conditions) {

if (table == null || table.isBlank()) {
throw new RuntimeException("A tabela deve ser informada.");
}
if (conditions == null || conditions.isEmpty()) {
throw new RuntimeException("As condições devem ser informadas.");
}

String whereClause = conditions.entrySet().stream()
.map(e -> {
Object value = e.getValue();
if (value == null) {
return e.getKey() + " IS NULL";
}
// Trata números e booleanos diretamente, sem aspas
if (value instanceof Number || value instanceof Boolean) {
return e.getKey() + " = " + value;
}
// Trata outros tipos como string, escapando aspas simples
String strValue = value.toString();
String safeValue = strValue.replace("'", "''");
return e.getKey() + " = '" + safeValue + "'";
})
.collect(Collectors.joining(" AND "));

String sql = "SELECT * FROM " + table + " WHERE " + whereClause;

// Printa no console para visualizar no log do Karate (ajuda a rodar manualmente no Spanner)
System.out.println("n=======================================================");
System.out.println(" [SPANNER QUERY HELPER] EXECUTANDO QUERY:");
System.out.println(" " + sql);
System.out.println("=======================================================n");

return spanner.executeQuery(sql);
}

/**
* Retorna apenas UM registro (útil para chaves únicas)
*/
public Map<String, Object> queryOne(String table, Map<String, Object> conditions) {
var list = query(table, conditions);
if (list.isEmpty()) {
// Retorna null em vez de lançar exceção para ser tratado no re-try
return null;
}
return list.get(0);
}

/**
* Executa SELECT com retentativa em caso de o registro ainda não existir.
* Útil para aguardar a persistência de eventos Kafka assíncronos no banco de dados.
*
* @param table Tabela do Spanner
* @param conditions Condições do WHERE
* @param maxRetries Quantidade máxima de tentativas
* @param delayMillis Tempo de espera entre cada tentativa em milissegundos
*/
public List<Map<String, Object>> queryWithRetry(String table, Map<String, Object> conditions, int maxRetries, long delayMillis) {
for (int i = 0; i < maxRetries; i++) {
try {
var result = query(table, conditions);
if (result != null && !result.isEmpty()) {
System.out.println("Tentativa " + (i + 1) + ": Sucesso! Registros encontrados.");
return result;
}
System.out.println("Tentativa " + (i + 1) + "/" + maxRetries + ": Nenhum registro encontrado ainda. Aguardando " + delayMillis + "ms...");
} catch (Exception e) {
System.out.println("Tentativa " + (i + 1) + "/" + maxRetries + " falhou com erro: " + e.getMessage());
}

try {
Thread.sleep(delayMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrompida durante a retentativa de consulta ao Spanner.", e);
}
}

System.out.println("Esgotadas as " + maxRetries + " tentativas. Nenhum registro encontrado.");
return Collections.emptyList(); // Retorna lista vazia se não encontrar
}

/**
* Retorna apenas UM registro usando retentativas.
* Lança exceção se não encontrar após as tentativas esgotarem, falhando o teste do Karate imediatamente.
*/
public Map<String, Object> queryOneWithRetry(String table, Map<String, Object> conditions, int maxRetries, long delayMillis) {
var list = queryWithRetry(table, conditions, maxRetries, delayMillis);
if (list.isEmpty()) {
String whereClause = conditions.entrySet().stream().map(e -> e.getKey() + " = " + e.getValue()).collect(Collectors.joining(" AND "));
String sql = "SELECT * FROM " + table + " WHERE " + whereClause;

throw new RuntimeException(
String.format("Nenhum registro encontrado para a tabela '%s' após %d tentativas.nQUERY EXECUTADA: %snCONDIÇÕES: %s",
table, maxRetries, sql, conditions)
);
}
return list.get(0);
}

/**
* [NCA-652] Aguarda, com retentativas, ate o contrato atingir um status especifico
* (ex.: 'Encerrado' apos a liquidacao). Necessario porque a atualizacao do status
* em TbLoanInformationStatic e ASSINCRONA: a linha ja existe, mas o status so muda
* depois que o evento de liquidacao e processado. O queryOneWithRetry nao serve aqui
* porque ele para assim que encontra a linha (independente do status).
*
* @throws RuntimeException se o status esperado nao for atingido apos as tentativas.
*/
public Map<String, Object> aguardarStatusContrato(String loanNumber, String statusEsperado,
int maxRetries, long delayMillis) {
String sql = "SELECT * FROM TbLoanInformationStatic WHERE LoanNumber = '"
+ loanNumber.replace("'", "''") + "' ORDER BY LastUpdateTime DESC LIMIT 1";

Map<String, Object> ultimo = null;
for (int i = 0; i < maxRetries; i++) {
List<Map<String, Object>> res = spanner.executeQuery(sql);
if (res != null && !res.isEmpty()) {
ultimo = res.get(0);
Object status = ultimo.get("LoanInformationStaticStatus");
System.out.println("Tentativa " + (i + 1) + "/" + maxRetries
+ ": contrato " + loanNumber + " com status '" + status + "'.");
if (statusEsperado.equals(status)) {
return ultimo;
}
} else {
System.out.println("Tentativa " + (i + 1) + "/" + maxRetries + ": contrato ainda nao encontrado.");
}
try {
Thread.sleep(delayMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrompida ao aguardar status do contrato.", e);
}
}
throw new RuntimeException(String.format(
"[NCA-652] Status esperado '%s' nao atingido para o contrato %s apos %d tentativas. Ultima leitura: %s",
statusEsperado, loanNumber, maxRetries, ultimo));
}

/**
* [NCA-652] Seleciona um contrato D-1 ELEGÍVEL para liquidação por fraude, SEM implantar.
* Substitui o SELECT manual no Spanner.
*
* Elegibilidade:
* - LoanNumber inicia com o prefixo (ex.: "CONTRATO");
* - Contrato com status 'Aberto';
* - LoanOriginationDate < hoje (D-1) — não pode ser implantado/liquidado no mesmo dia;
* - Possui ao menos UMA parcela cuja VERSÃO ATUAL (maior RepaymentFinancialPositionDate)
* está 'Aberto' e IsActiveFlag = true.
*
* Retorna, pronto para o fluxo de baixa/liquidação:
* {
* LoanNumber, LoanInformationStaticId, TenantId,
* Repayments : todas as parcelas abertas [{ loanRepaymentId }],
* ParcelasParaBaixar : abertas EXCETO a última [{ loanRepaymentId }] (baixa normal),
* ParcelaLiquidacao : id da última parcela aberta (recebe a liquidação por fraude)
* }
*
* @throws RuntimeException se não houver contrato D-1 elegível.
*/
public Map<String, Object> findContratoD1ParaLiquidacaoFraude(String prefixoLoanNumber, String fusoHorario, String loanNumberFixo) {

String prefixo = (prefixoLoanNumber == null || prefixoLoanNumber.isBlank())
? "CONTRATO" : prefixoLoanNumber.trim();
String prefixoSeguro = prefixo.replace("'", "''");
String tz = (fusoHorario == null || fusoHorario.isBlank())
? "America/Sao_Paulo" : fusoHorario.trim();
boolean temFixo = loanNumberFixo != null && !loanNumberFixo.isBlank();

// Sub-condição: a parcela está ABERTA na sua VERSÃO MAIS RECENTE (maior data de posição).
// O presentDate do valor presente é essa data da posição (que costuma ser D-1).
String versaoAtualEAberta =
" AND r.IsActiveFlag = TRUE" +
" AND r.RepaymentStatus = 'Aberto'" +
" AND r.RepaymentFinancialPositionDate = (" +
" SELECT MAX(r2.RepaymentFinancialPositionDate)" +
" FROM TbLoanRepayment AS r2" +
" WHERE r2.LoanInformationStaticId = r.LoanInformationStaticId" +
" AND r2.RepaymentId = r.RepaymentId" +
" )";

// 1) Escolhe o contrato. Se loanNumberFixo veio, usa ele; senao busca o D-1 mais recente.
String whereContrato;
if (temFixo) {
whereContrato = " WHERE s.LoanNumber = '" + loanNumberFixo.replace("'", "''") + "'";
} else {
whereContrato =
" WHERE s.LoanNumber LIKE '" + prefixoSeguro + "%'" +
" AND s.LoanNumber NOT LIKE 'CONTRATO-ERR%'" +
" AND s.LoanNumber NOT LIKE 'RETROATIVO%'" +
" AND s.LoanNumber NOT LIKE 'CONTRATO-DB%'" +
" AND s.LoanNumber NOT LIKE 'CONTRATO-RESP%'" +
" AND s.LoanNumber NOT LIKE 'CONTRATO-PARC%'" +
" AND s.LoanInformationStaticStatus = 'Aberto'" +
" AND s.LoanOriginationDate < CURRENT_DATE('" + tz + "')";
}

String sqlContrato =
"SELECT s.LoanNumber AS LoanNumber, s.LoanInformationStaticId AS LoanInformationStaticId" +
" FROM TbLoanInformationStatic AS s" +
whereContrato +
" AND EXISTS (" +
" SELECT 1 FROM TbLoanRepayment AS r" +
" WHERE r.LoanInformationStaticId = s.LoanInformationStaticId" +
versaoAtualEAberta +
" )" +
" ORDER BY s.LoanOriginationDate DESC" +
" LIMIT 1";

System.out.println("[NCA-652] SELECIONANDO CONTRATO PARA LIQUIDACAO POR FRAUDE: " + sqlContrato);

List<Map<String, Object>> contratos = spanner.executeQuery(sqlContrato);
if (contratos == null || contratos.isEmpty()) {
throw new RuntimeException(
"[NCA-652] Nenhum contrato elegível encontrado (" + (temFixo ? "loanNumber=" + loanNumberFixo : "prefixo '" + prefixo + "%', status 'Aberto', originação < hoje")
+ " e com parcela aberta). QUERY: " + sqlContrato);
}

String loanNumber = (String) contratos.get(0).get("LoanNumber");
String staticId = (String) contratos.get(0).get("LoanInformationStaticId");

// 2) Lista TODAS as parcelas abertas (versão atual) do contrato, ordenadas.
String sqlParcelas =
"SELECT r.RepaymentId AS RepaymentId, r.TenantId AS TenantId," +
" r.RepaymentFinancialPositionDate AS PositionDate" +
" FROM TbLoanRepayment AS r" +
" WHERE r.LoanInformationStaticId = '" + staticId.replace("'", "''") + "'" +
versaoAtualEAberta +
" ORDER BY r.RepaymentId ASC";

List<Map<String, Object>> parcelas = spanner.executeQuery(sqlParcelas);
if (parcelas == null || parcelas.isEmpty()) {
throw new RuntimeException("[NCA-652] Contrato " + loanNumber + " sem parcelas abertas na versão atual.");
}

Object tenantId = parcelas.get(0).get("TenantId");
// Data da posição financeira (D-1) usada como presentDate no valor presente.
Object presentDate = parcelas.get(0).get("PositionDate");

List<Map<String, Object>> repayments = new ArrayList<>();
List<Map<String, Object>> parcelasParaBaixar = new ArrayList<>();
for (Map<String, Object> p : parcelas) {
Map<String, Object> item = new HashMap<>();
item.put("loanRepaymentId", p.get("RepaymentId"));
repayments.add(item);
}
// Todas menos a última vão para a baixa normal; a última recebe a liquidação por fraude.
for (int i = 0; i < repayments.size() - 1; i++) {
parcelasParaBaixar.add(repayments.get(i));
}
Object parcelaLiquidacao = parcelas.get(parcelas.size() - 1).get("RepaymentId");

Map<String, Object> resultado = new HashMap<>();
resultado.put("LoanNumber", loanNumber);
resultado.put("LoanInformationStaticId", staticId);
resultado.put("TenantId", tenantId);
resultado.put("PresentDate", presentDate);
resultado.put("Repayments", repayments);
resultado.put("ParcelasParaBaixar", parcelasParaBaixar);
resultado.put("ParcelaLiquidacao", parcelaLiquidacao);

System.out.println("[NCA-652] Contrato D-1 selecionado: " + loanNumber +
" | parcelas abertas: " + repayments.size() + " | liquidacao na parcela: " + parcelaLiquidacao);
return resultado;
}
}






# language: en
# encoding: UTF-8
@liquidacaoFraudeD1 @NCA-652 @fraude @high
Feature: NCA-652 - Liquidacao por FRAUDE de contrato D-1 (consulta no Spanner, NAO implanta)

# NAO implanta contrato. Seleciona no Spanner um contrato D-1 ja existente
# (prefixo CONTRATO%, originacao < hoje, status Aberto e com parcela aberta),
# baixa as parcelas abertas uma a uma (loop) e dispara a liquidacao por FRAUDE
# na ULTIMA parcela (COMP + agente de fraude). Restricao da API: nao pode sobrar
# parcela em aberto apos a liquidacao.
#
# Agente -> evento contabil esperado (validado no atomo do PRCO, nao aqui):
# 198 -> LFRD ; 199/218/219 -> LFRI
# Aqui (ACPF) validamos a consequencia na base: contrato ENCERRADO.

Background:
* def Utils = Java.type('helpers.Utils')
* def SA_JSON = Utils.getSpannerCredential(executionEnv)
* def SpannerUtil = Java.type('helpers.SpannerUtil')
* def SpannerQueryHelper = Java.type('helpers.SpannerQueryHelper')
* def spannerUtil = new SpannerUtil(spanner.projectId, spanner.instanceId, spanner.databaseId, SA_JSON)
* def queryHelper = new SpannerQueryHelper(spannerUtil)

Scenario Outline: Liquidar contrato D-1 por fraude com agente <agente> (esperado <evento>)

# --- ETAPA 1: SELECIONA CONTRATO D-1 (NAO IMPLANTA) ---
# p_loanNumber (opcional) fixa um contrato; senao busca o D-1 mais recente.
* def prefixo = karate.get('p_prefixoLoanNumber', 'CONTRATO')
* def fuso = karate.get('p_fusoHorario', 'America/Sao_Paulo')
* def contratoFixo = karate.get('p_loanNumber', null)
* def contratoD1 = queryHelper.findContratoD1ParaLiquidacaoFraude(prefixo, fuso, contratoFixo)
* def loanNumber = contratoD1.LoanNumber
* def tenantId = contratoD1.TenantId
* def repayments = contratoD1.Repayments
* def parcelasParaBaixar = contratoD1.ParcelasParaBaixar
* def parcelaLiquidacao = contratoD1.ParcelaLiquidacao
* def presentDate = contratoD1.PresentDate
* def activeSequence = karate.get('p_activeSequence', 1)
* print '>> [NCA-652] Contrato D-1:', loanNumber, '| presentDate:', presentDate, '| agente:', '<agente>'

# --- ETAPA 2: BAIXA NORMAL DAS PARCELAS ABERTAS (LOOP), EXCETO A ULTIMA ---
# Passa p_loanNumber para o @NCA-T13 NAO implantar (so baixa o contrato selecionado).
* eval
"""
karate.forEach(parcelasParaBaixar, function(p){
karate.log('>> [NCA-652] Baixando parcela normal', p.loanRepaymentId);
var a = {
p_loanNumber: loanNumber,
p_tenantId: tenantId,
p_activeSequence: activeSequence,
p_repayments: repayments,
p_parcelaId: p.loanRepaymentId,
p_presentDate: presentDate
};
var r = karate.call('classpath:features/baixa/baixaParcela.feature@NCA-T13', a);
if (r.response.responseStatus != 200) karate.fail('Falha na baixa normal da parcela ' + p.loanRepaymentId);
});
"""

# --- ETAPA 3: BAIXA DA ULTIMA PARCELA COMO FRAUDE (ENCERRA O CONTRATO) ---
* def hoje = java.time.LocalDate.now().toString()
* def argsFraude =
"""
{
"p_loanNumber": "#(loanNumber)",
"p_tenantId": "#(tenantId)",
"p_activeSequence": "#(activeSequence)",
"p_repayments": "#(repayments)",
"p_parcelaId": "#(parcelaLiquidacao)",
"p_presentDate": "#(presentDate)",
"p_loanStatusCode": "COMP",
"p_repaymentReceivingAgentNumber": "<agente>",
"p_loanSettlementFlag": true,
"p_loanEffectiveSettlementDate": "#(hoje)"
}
"""
* print '>> [NCA-652] LIQUIDACAO FRAUDE - parcela:', parcelaLiquidacao, '| agente:', '<agente>'
* def baixaFraude = karate.call('classpath:features/baixa/baixaParcela.feature@NCA-T13', argsFraude)
* print '>> [NCA-652] RESPOSTA DA LIQUIDACAO:', baixaFraude.response.response
Then match baixaFraude.response.responseStatus == 200

# --- ETAPA 4: VALIDA NA BASE QUE O CONTRATO FOI ENCERRADO ---
# Espera o status virar 'Encerrado' (a liquidacao via COMP e assincrona).
* def contratoBase = queryHelper.aguardarStatusContrato(loanNumber, 'Encerrado', 15, 4000)
* match contratoBase.LoanInformationStaticStatus == 'Encerrado'
* print '>> [NCA-652] Contrato', loanNumber, 'liquidado por fraude (agente <agente>, esperado <evento>) e ENCERRADO na base.'

Examples:
| agente | evento |
| 198 | LFRD |




     
 
what is notes.io
 

Notes is a web-based application for online taking notes. You can take your notes and share with others people. If you like taking long notes, notes.io is designed for you. To date, over 8,000,000,000+ notes created and continuing...

With notes.io;

  • * You can take a note from anywhere and any device with internet connection.
  • * You can share the notes in social platforms (YouTube, Facebook, Twitter, instagram etc.).
  • * You can quickly share your contents without website, blog and e-mail.
  • * You don't need to create any Account to share a note. As you wish you can use quick, easy and best shortened notes with sms, websites, e-mail, or messaging services (WhatsApp, iMessage, Telegram, Signal).
  • * Notes.io has fabulous infrastructure design for a short link and allows you to share the note as an easy and understandable link.

Fast: Notes.io is built for speed and performance. You can take a notes quickly and browse your archive.

Easy: Notes.io doesn’t require installation. Just write and share note!

Short: Notes.io’s url just 8 character. You’ll get shorten link of your note when you want to share. (Ex: notes.io/q )

Free: Notes.io works for 14 years and has been free since the day it was started.


You immediately create your first note and start sharing with the ones you wish. If you want to contact us, you can use the following communication channels;


Email: [email protected]

Twitter: http://twitter.com/notesio

Instagram: http://instagram.com/notes.io

Facebook: http://facebook.com/notesio



Regards;
Notes.io Team

     
 
Shortened Note Link
 
 
Looding Image
 
     
 
Long File
 
 

For written notes was greater than 18KB Unable to shorten.

To be smaller than 18KB, please organize your notes, or sign in.