Skip to content

Commit 91f5e78

Browse files
committed
chore: right amount
1 parent 31c0d2c commit 91f5e78

File tree

5 files changed

+378
-7
lines changed

5 files changed

+378
-7
lines changed

accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/IndexerTransactionTransformer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ private TransformedTransactionItem transformItem(TransactionItemEntity item,
106106

107107
return TransformedTransactionItem.builder()
108108
.id(item.getId())
109-
.amountFcy(amountFcy)
110-
.amountLcy(item.getAmountLcy())
109+
.amountFcy(item.getOperationType().equals(OperationType.DEBIT) ? amountFcy : amountFcy.negate())
110+
.amountLcy(item.getOperationType().equals(OperationType.DEBIT) ? item.getAmountLcy() : item.getAmountLcy().negate())
111111
.fxRate(fxRate)
112112
.accountEvent(accountEvent)
113113
.project(project)

accounting_reporting_core/src/main/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/OnChainIndexerReconcilationService.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OnChainTransactionDto;
2020
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OnChainTransactionItemDto;
21-
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.OperationType;
2221
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.reconcilation.ReconcilationCode;
2322
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.TransactionEntity;
2423
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.IndexerTransactionTransformer.TransformedTransaction;
@@ -302,10 +301,8 @@ private void compareItemFields(StringBuilder mismatches,
302301
private void compareAmountFcy(StringBuilder mismatches, String itemId,
303302
TransformedTransactionItem dbItem,
304303
OnChainTransactionItemDto indexerItem) {
305-
BigDecimal amountToCompare = dbItem.getOperationType().equals(OperationType.DEBIT) ? indexerItem.amountFcy() : indexerItem.amountFcy().negate();
306-
307-
if (dbItem.getAmountFcy() != null && dbItem.getAmountFcy().compareTo(amountToCompare) != 0) {
308-
appendItemMismatch(mismatches, itemId, "amountFcy", dbItem.getAmountFcy(), amountToCompare);
304+
if (dbItem.getAmountFcy() != null && dbItem.getAmountFcy().compareTo(indexerItem.amountFcy()) != 0) {
305+
appendItemMismatch(mismatches, itemId, "amountFcy", dbItem.getAmountFcy(), indexerItem.amountFcy());
309306
}
310307
}
311308

accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/IndexerTransactionTransformerTest.java

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,127 @@ private TransactionEntity createTransaction(String id, String internalNumber, Tr
297297
return tx;
298298
}
299299

300+
@Test
301+
void transformForIndexerComparison_shouldHandleNullCostCenterCode() {
302+
// Given - cost center exists but has null customerCode
303+
TransactionEntity tx = createTransaction("tx-1", "INT-001", TransactionType.VendorPayment, "2024-01-15", "batch-1");
304+
TransactionItemEntity item = createItem("item-1", BigDecimal.valueOf(1000), BigDecimal.valueOf(1000), BigDecimal.ONE);
305+
// Set cost center with null customerCode
306+
item.setCostCenter(Optional.of(CostCenter.builder().customerCode(null).build()));
307+
item.setTransaction(tx);
308+
tx.setAllItems(Set.of(item));
309+
310+
// When
311+
TransformedTransaction result = transformer.transformForIndexerComparison(tx);
312+
313+
// Then - cost center should be null when customerCode is null
314+
TransformedTransactionItem transformedItem = result.getItems().get(0);
315+
assertThat(transformedItem.getCostCenterCustomerCode()).isNull();
316+
}
317+
318+
@Test
319+
void transformForIndexerComparison_shouldHandleDocumentWithCounterparty() {
320+
// Given
321+
TransactionEntity tx = createTransaction("tx-1", "INT-001", TransactionType.VendorPayment, "2024-01-15", "batch-1");
322+
TransactionItemEntity item = createItem("item-1", BigDecimal.valueOf(1000), BigDecimal.valueOf(1000), BigDecimal.ONE);
323+
324+
org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.Counterparty counterparty =
325+
org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.Counterparty.builder()
326+
.customerCode("VENDOR-001")
327+
.type(org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.Counterparty.Type.VENDOR)
328+
.build();
329+
330+
Document doc = Document.builder()
331+
.num("DOC-001")
332+
.currency(Currency.builder().customerCode("CHF").build())
333+
.counterparty(counterparty)
334+
.build();
335+
item.setDocument(Optional.of(doc));
336+
item.setTransaction(tx);
337+
tx.setAllItems(Set.of(item));
338+
339+
// When
340+
TransformedTransaction result = transformer.transformForIndexerComparison(tx);
341+
342+
// Then - counterparty should be preserved in transformation
343+
TransformedTransactionItem transformedItem = result.getItems().get(0);
344+
assertThat(transformedItem.getDocumentNumber()).isEqualTo("DOC-001");
345+
}
346+
347+
@Test
348+
void transformForIndexerComparison_shouldHandleItemWithoutAccountEvent() {
349+
// Given
350+
TransactionEntity tx = createTransaction("tx-1", "INT-001", TransactionType.VendorPayment, "2024-01-15", "batch-1");
351+
TransactionItemEntity item = createItem("item-1", BigDecimal.valueOf(1000), BigDecimal.valueOf(1000), BigDecimal.ONE);
352+
// Account event is already empty in createItem()
353+
item.setTransaction(tx);
354+
tx.setAllItems(Set.of(item));
355+
356+
// When
357+
TransformedTransaction result = transformer.transformForIndexerComparison(tx);
358+
359+
// Then - event code should be null
360+
TransformedTransactionItem transformedItem = result.getItems().get(0);
361+
assertThat(transformedItem.getEventCode()).isNull();
362+
}
363+
364+
@Test
365+
void transformForIndexerComparison_shouldHandleItemWithoutProject() {
366+
// Given
367+
TransactionEntity tx = createTransaction("tx-1", "INT-001", TransactionType.VendorPayment, "2024-01-15", "batch-1");
368+
TransactionItemEntity item = createItem("item-1", BigDecimal.valueOf(1000), BigDecimal.valueOf(1000), BigDecimal.ONE);
369+
// Project is already empty in createItem()
370+
item.setTransaction(tx);
371+
tx.setAllItems(Set.of(item));
372+
373+
// When
374+
TransformedTransaction result = transformer.transformForIndexerComparison(tx);
375+
376+
// Then - project code should be null
377+
TransformedTransactionItem transformedItem = result.getItems().get(0);
378+
assertThat(transformedItem.getProjectCustomerCode()).isNull();
379+
}
380+
381+
@Test
382+
void transformForIndexerComparison_shouldHandleItemWithoutDocument() {
383+
// Given
384+
TransactionEntity tx = createTransaction("tx-1", "INT-001", TransactionType.VendorPayment, "2024-01-15", "batch-1");
385+
TransactionItemEntity item = createItem("item-1", BigDecimal.valueOf(1000), BigDecimal.valueOf(1000), BigDecimal.ONE);
386+
// Document is already empty in createItem()
387+
item.setTransaction(tx);
388+
tx.setAllItems(Set.of(item));
389+
390+
// When
391+
TransformedTransaction result = transformer.transformForIndexerComparison(tx);
392+
393+
// Then - document fields should be null
394+
TransformedTransactionItem transformedItem = result.getItems().get(0);
395+
assertThat(transformedItem.getDocumentNumber()).isNull();
396+
assertThat(transformedItem.getCurrencyCustomerCode()).isNull();
397+
assertThat(transformedItem.getVatCustomerCode()).isNull();
398+
}
399+
400+
@Test
401+
void transformForIndexerComparison_shouldUseCostCenterCodeWhenNotFoundInOrg() {
402+
// Given - cost center not found in organisation API should use the code from the item
403+
TransactionEntity tx = createTransaction("tx-1", "INT-001", TransactionType.VendorPayment, "2024-01-15", "batch-1");
404+
TransactionItemEntity item = createItem("item-1", BigDecimal.valueOf(1000), BigDecimal.valueOf(1000), BigDecimal.ONE);
405+
item.setCostCenter(Optional.of(CostCenter.builder().customerCode("UNKNOWN-CC").build()));
406+
item.setTransaction(tx);
407+
tx.setAllItems(Set.of(item));
408+
409+
// Mock cost center not found
410+
when(organisationPublicApi.findCostCenter(ORG_ID, "UNKNOWN-CC"))
411+
.thenReturn(Optional.empty());
412+
413+
// When
414+
TransformedTransaction result = transformer.transformForIndexerComparison(tx);
415+
416+
// Then - should use the original code since not found in org
417+
TransformedTransactionItem transformedItem = result.getItems().get(0);
418+
assertThat(transformedItem.getCostCenterCustomerCode()).isEqualTo("UNKNOWN-CC");
419+
}
420+
300421
private TransactionItemEntity createItem(String id, BigDecimal amountFcy, BigDecimal amountLcy, BigDecimal fxRate) {
301422
TransactionItemEntity item = new TransactionItemEntity();
302423
item.setId(id);

accounting_reporting_core/src/test/java/org/cardanofoundation/lob/app/accounting_reporting_core/service/internal/OnChainIndexerReconcilationServiceTest.java

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ void reconcileWithIndexer_shouldReturnOK_whenTransactionsMatch() {
7676
new IndexerTransactionTransformer.VatHolder("VAT0", BigDecimal.ZERO),
7777
null))
7878
.accountEvent(new IndexerTransactionTransformer.AccountEventHolder("7820T000", "Test Event"))
79+
.operationType(OperationType.DEBIT)
7980
.build();
8081

8182
TransformedTransaction transformedTx = TransformedTransaction.builder()
@@ -328,6 +329,7 @@ void reconcileWithIndexer_shouldDetectAmountMismatch() {
328329
new IndexerTransactionTransformer.VatHolder("VAT0", BigDecimal.ZERO),
329330
null))
330331
.accountEvent(new IndexerTransactionTransformer.AccountEventHolder("7820T000", "Test Event"))
332+
.operationType(OperationType.DEBIT)
331333
.build();
332334

333335
TransformedTransaction transformedTx = TransformedTransaction.builder()
@@ -462,6 +464,205 @@ private TransactionEntity createDbTransaction(String id, String internalNumber,
462464
return tx;
463465
}
464466

467+
@Test
468+
void reconcileWithIndexer_shouldHandleCreditOperationType() {
469+
// Given - CREDIT operations should negate the indexer amount for comparison
470+
String organisationId = "test-org";
471+
LocalDate dateFrom = LocalDate.of(2024, 1, 1);
472+
LocalDate dateTo = LocalDate.of(2024, 1, 31);
473+
474+
TransactionEntity dbTx = createDbTransaction("tx-1", "VENDPYMT-001", "VendorPayment", "2024-01-15", "batch-1");
475+
TransactionItemEntity dbItem = createDbTransactionItem("item-1", BigDecimal.valueOf(-1000), "1.0", "9000", "PRJ001");
476+
dbItem.setOperationType(OperationType.CREDIT); // Set CREDIT operation type
477+
dbItem.setTransaction(dbTx);
478+
dbTx.setAllItems(Set.of(dbItem));
479+
480+
// Mock transformer to return CREDIT item with negative amount
481+
TransformedTransactionItem transformedItem = TransformedTransactionItem.builder()
482+
.id("item-1")
483+
.amountFcy(BigDecimal.valueOf(-1000)) // Negative for CREDIT
484+
.fxRate(BigDecimal.ONE)
485+
.costCenter(new IndexerTransactionTransformer.CostCenterHolder("9000", "Internal"))
486+
.project(new IndexerTransactionTransformer.ProjectHolder("PRJ001", "Test Project"))
487+
.document(new IndexerTransactionTransformer.DocumentHolder("doc-001",
488+
new IndexerTransactionTransformer.CurrencyHolder("ISO_4217:CHF", "CHF"),
489+
new IndexerTransactionTransformer.VatHolder("VAT0", BigDecimal.ZERO),
490+
null))
491+
.accountEvent(new IndexerTransactionTransformer.AccountEventHolder("7820T000", "Test Event"))
492+
.operationType(OperationType.CREDIT)
493+
.build();
494+
495+
TransformedTransaction transformedTx = TransformedTransaction.builder()
496+
.id("tx-1")
497+
.internalNumber("VENDPYMT-001")
498+
.transactionType("VendorPayment")
499+
.entryDate("2024-01-15")
500+
.batchId("batch-1")
501+
.organisationId(organisationId)
502+
.items(List.of(transformedItem))
503+
.build();
504+
505+
when(indexerTransactionTransformer.transformForIndexerComparison(any(TransactionEntity.class)))
506+
.thenReturn(transformedTx);
507+
508+
// Indexer stores positive amount, CREDIT should negate it for comparison
509+
OnChainTransactionItemDto indexerItem = new OnChainTransactionItemDto(
510+
"item-1", BigDecimal.valueOf(1000), "1", "doc-001", "ISO_4217:CHF",
511+
"Internal", "9000", "0", "VAT0", "7820T000", "Test Event", "PRJ001", "Test Project"
512+
);
513+
OnChainTransactionDto indexerTx = new OnChainTransactionDto(
514+
"tx-1", "hash-1", "VENDPYMT-001", "2024-01", "batch-1",
515+
"VendorPayment", "2024-01-15", organisationId, List.of(indexerItem)
516+
);
517+
518+
when(indexerService.retrieveTransactionsByDateRange(organisationId, dateFrom, dateTo))
519+
.thenReturn(Either.right(List.of(indexerTx)));
520+
521+
// When
522+
Either<Problem, Map<String, IndexerReconcilationResult>> result =
523+
service.reconcileWithIndexer(organisationId, dateFrom, dateTo, Set.of(dbTx));
524+
525+
// Then - CREDIT with -1000 should match indexer +1000 after negation
526+
assertThat(result.isRight()).isTrue();
527+
Map<String, IndexerReconcilationResult> results = result.get();
528+
assertThat(results.get("tx-1").status()).isEqualTo(ReconcilationCode.OK);
529+
}
530+
531+
@Test
532+
void reconcileWithIndexer_shouldDetectFxRateMismatch() {
533+
// Given
534+
String organisationId = "test-org";
535+
LocalDate dateFrom = LocalDate.of(2024, 1, 1);
536+
LocalDate dateTo = LocalDate.of(2024, 1, 31);
537+
538+
TransactionEntity dbTx = createDbTransaction("tx-1", "VENDPYMT-001", "VendorPayment", "2024-01-15", "batch-1");
539+
TransactionItemEntity dbItem = createDbTransactionItem("item-1", BigDecimal.valueOf(1000), "1.25", "9000", "PRJ001");
540+
dbItem.setTransaction(dbTx);
541+
dbTx.setAllItems(Set.of(dbItem));
542+
543+
// Mock transformer to return item with different fxRate - note: content key uses fxRate
544+
TransformedTransactionItem transformedItem = TransformedTransactionItem.builder()
545+
.id("item-1")
546+
.amountFcy(BigDecimal.valueOf(1000))
547+
.fxRate(new BigDecimal("1.25")) // Different from indexer
548+
.costCenter(new IndexerTransactionTransformer.CostCenterHolder("9000", "Internal"))
549+
.project(new IndexerTransactionTransformer.ProjectHolder("PRJ001", "Test Project"))
550+
.document(new IndexerTransactionTransformer.DocumentHolder("doc-001",
551+
new IndexerTransactionTransformer.CurrencyHolder("ISO_4217:CHF", "CHF"),
552+
new IndexerTransactionTransformer.VatHolder("VAT0", BigDecimal.ZERO),
553+
null))
554+
.accountEvent(new IndexerTransactionTransformer.AccountEventHolder("7820T000", "Test Event"))
555+
.operationType(OperationType.DEBIT)
556+
.build();
557+
558+
TransformedTransaction transformedTx = TransformedTransaction.builder()
559+
.id("tx-1")
560+
.internalNumber("VENDPYMT-001")
561+
.transactionType("VendorPayment")
562+
.entryDate("2024-01-15")
563+
.batchId("batch-1")
564+
.organisationId(organisationId)
565+
.items(List.of(transformedItem))
566+
.build();
567+
568+
when(indexerTransactionTransformer.transformForIndexerComparison(any(TransactionEntity.class)))
569+
.thenReturn(transformedTx);
570+
571+
// Indexer has different fxRate - this will cause a mismatch because items won't match by content key
572+
OnChainTransactionItemDto indexerItem = new OnChainTransactionItemDto(
573+
"item-1", BigDecimal.valueOf(1000), "1.50", "doc-001", "ISO_4217:CHF",
574+
"Internal", "9000", "0", "VAT0", "7820T000", "Test Event", "PRJ001", "Test Project"
575+
);
576+
OnChainTransactionDto indexerTx = new OnChainTransactionDto(
577+
"tx-1", "hash-1", "VENDPYMT-001", "2024-01", "batch-1",
578+
"VendorPayment", "2024-01-15", organisationId, List.of(indexerItem)
579+
);
580+
581+
when(indexerService.retrieveTransactionsByDateRange(organisationId, dateFrom, dateTo))
582+
.thenReturn(Either.right(List.of(indexerTx)));
583+
584+
// When
585+
Either<Problem, Map<String, IndexerReconcilationResult>> result =
586+
service.reconcileWithIndexer(organisationId, dateFrom, dateTo, Set.of(dbTx));
587+
588+
// Then - fxRate mismatch should cause item not found
589+
assertThat(result.isRight()).isTrue();
590+
Map<String, IndexerReconcilationResult> results = result.get();
591+
assertThat(results.get("tx-1").status()).isEqualTo(ReconcilationCode.NOK);
592+
assertThat(results.get("tx-1").mismatchReason()).contains("not found in indexer");
593+
}
594+
595+
@Test
596+
void reconcileWithIndexer_shouldHandleInternalNumberMismatch() {
597+
// Given
598+
String organisationId = "test-org";
599+
LocalDate dateFrom = LocalDate.of(2024, 1, 1);
600+
LocalDate dateTo = LocalDate.of(2024, 1, 31);
601+
602+
TransactionEntity dbTx = createDbTransaction("tx-1", "VENDPYMT-001", "VendorPayment", "2024-01-15", "batch-1");
603+
604+
// Mock transformer
605+
TransformedTransaction transformedTx = TransformedTransaction.builder()
606+
.id("tx-1")
607+
.internalNumber("VENDPYMT-001")
608+
.transactionType("VendorPayment")
609+
.entryDate("2024-01-15")
610+
.batchId("batch-1")
611+
.organisationId(organisationId)
612+
.items(List.of())
613+
.build();
614+
615+
when(indexerTransactionTransformer.transformForIndexerComparison(any(TransactionEntity.class)))
616+
.thenReturn(transformedTx);
617+
618+
// Indexer has different internal number
619+
OnChainTransactionDto indexerTx = new OnChainTransactionDto(
620+
"tx-1", "hash-1", "VENDPYMT-999", "2024-01", "batch-1",
621+
"VendorPayment", "2024-01-15", organisationId, List.of()
622+
);
623+
624+
when(indexerService.retrieveTransactionsByDateRange(organisationId, dateFrom, dateTo))
625+
.thenReturn(Either.right(List.of(indexerTx)));
626+
627+
// When
628+
Either<Problem, Map<String, IndexerReconcilationResult>> result =
629+
service.reconcileWithIndexer(organisationId, dateFrom, dateTo, Set.of(dbTx));
630+
631+
// Then
632+
assertThat(result.isRight()).isTrue();
633+
Map<String, IndexerReconcilationResult> results = result.get();
634+
assertThat(results.get("tx-1").status()).isEqualTo(ReconcilationCode.NOK);
635+
assertThat(results.get("tx-1").mismatchReason()).contains("Internal number mismatch");
636+
}
637+
638+
@Test
639+
void reconcileWithIndexer_shouldHandleEmptyDbTransactions() {
640+
// Given
641+
String organisationId = "test-org";
642+
LocalDate dateFrom = LocalDate.of(2024, 1, 1);
643+
LocalDate dateTo = LocalDate.of(2024, 1, 31);
644+
645+
// Indexer has transactions but DB doesn't
646+
OnChainTransactionDto indexerTx = new OnChainTransactionDto(
647+
"tx-1", "hash-1", "VENDPYMT-001", "2024-01", "batch-1",
648+
"VendorPayment", "2024-01-15", organisationId, List.of()
649+
);
650+
651+
when(indexerService.retrieveTransactionsByDateRange(organisationId, dateFrom, dateTo))
652+
.thenReturn(Either.right(List.of(indexerTx)));
653+
654+
// When
655+
Either<Problem, Map<String, IndexerReconcilationResult>> result =
656+
service.reconcileWithIndexer(organisationId, dateFrom, dateTo, Set.of());
657+
658+
// Then - should detect orphaned indexer transaction
659+
assertThat(result.isRight()).isTrue();
660+
Map<String, IndexerReconcilationResult> results = result.get();
661+
assertThat(results).hasSize(1);
662+
assertThat(results.get("tx-1").status()).isEqualTo(ReconcilationCode.NOK);
663+
assertThat(results.get("tx-1").mismatchReason()).contains("found in indexer but not in database");
664+
}
665+
465666
private TransactionItemEntity createDbTransactionItem(String id, BigDecimal amount, String fxRate, String costCenterCode, String projectCode) {
466667
TransactionItemEntity item = new TransactionItemEntity();
467668
item.setId(id);

0 commit comments

Comments
 (0)