@@ -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