Skip to content

Release 1.0.0 #266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 57 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
be72d52
fix: unlock transactions which won't be published now (#264)
Kammerlo May 20, 2025
e559b56
fix: journal enrichment must adjust the operation type as well (#269)
Kammerlo May 20, 2025
4b9a3e5
chore: adjusting build job
Kammerlo May 20, 2025
ab342b1
feat: moving journalenrichmentstask and fixing totalLCY amount
Kammerlo May 20, 2025
f1ee400
Fix: Parent cost center missing in extraction
M4rc0Russ0 May 20, 2025
efac869
chore: fixing test
Kammerlo May 21, 2025
98d1218
chore: renamed variable for debouncer configuration
Kammerlo May 21, 2025
5f3c846
Merge pull request #271 from cardano-foundation/fix/parent_cost_cente…
M4rc0Russ0 May 21, 2025
c69e6b1
fix: Missing currecny data should result invalid transaction
M4rc0Russ0 May 21, 2025
3949ed8
fix: transactional runner to not create new database transactions (#273)
Kammerlo May 22, 2025
07f1fd9
chore: spotless
Kammerlo May 22, 2025
2aa0f85
chore: adding transactional back to update transaction batch
Kammerlo May 22, 2025
87978c8
chore: spotless
Kammerlo May 22, 2025
ddb3766
Merge pull request #274 from cardano-foundation/fix/missing_currecny_…
M4rc0Russ0 May 22, 2025
3bef8b6
fix: fixing transactional runner
Kammerlo May 22, 2025
07f34b6
fix: optimizing internal transactions (#276)
Kammerlo May 23, 2025
d819ab6
fix: blockchain publisher didn't saved transaction when running in no…
Kammerlo May 23, 2025
6d70272
fix: batches tx counting
M4rc0Russ0 May 29, 2025
03935b2
Merge pull request #282 from cardano-foundation/fix/batches_tx_counting
M4rc0Russ0 Jun 3, 2025
3b02aa4
fix: CARDANO_MAX_TRANSACTION_SIZE_BYTES transaction size
M4rc0Russ0 May 28, 2025
8fd70b2
Merge pull request #281 from cardano-foundation/fix/CARDANO_MAX_TRANS…
M4rc0Russ0 Jun 5, 2025
a759f55
fix: extraction and public interface
M4rc0Russ0 Jun 6, 2025
5d5fe08
Merge pull request #283 from cardano-foundation/fix/extraction_and_pu…
M4rc0Russ0 Jun 6, 2025
3f3d6c0
fix: public service
M4rc0Russ0 Jun 6, 2025
4bd7f80
Merge pull request #284 from cardano-foundation/fix/public_service
M4rc0Russ0 Jun 6, 2025
c461e11
feat: Report is set to pending if values don't match
Kammerlo May 6, 2025
5dddcd4
Merge pull request #240 from cardano-foundation/feat/LOB-1193-Set-rep…
M4rc0Russ0 Jun 10, 2025
a87c13d
fix: account credit name
M4rc0Russ0 Jun 10, 2025
6bc1a22
Merge pull request #287 from cardano-foundation/fix/account_credit_name
M4rc0Russ0 Jun 10, 2025
ecaaf57
fix: revalt calculation fro CREDIT and DEBIT
M4rc0Russ0 Jun 11, 2025
7b51a2a
Merge pull request #290 from cardano-foundation/fix/fxrevalt_calculation
M4rc0Russ0 Jun 12, 2025
56b2c87
fix: ERASED_SUM_APPLIED using account values
M4rc0Russ0 Jun 12, 2025
ac1796d
Merge pull request #294 from cardano-foundation/fix/ERASED_SUM_APPLIE…
M4rc0Russ0 Jun 12, 2025
f663d79
fix: txitem_group_strategy_new
M4rc0Russ0 Jun 13, 2025
b98bef8
Merge pull request #297 from cardano-foundation/fix/txitem_group_stra…
M4rc0Russ0 Jun 13, 2025
8ab89e6
fix: saving report status in db (#296)
Kammerlo Jun 14, 2025
56fd4ef
Fix/save report status in db (#299)
Kammerlo Jun 14, 2025
02b0a3d
Fix/use always latest report for metrics (#304)
Kammerlo Jun 17, 2025
c02c1c7
chore: report version readability
M4rc0Russ0 Jun 17, 2025
f393953
fix: total expense values were wrongly mapped (#307)
Kammerlo Jun 18, 2025
572a942
Merge pull request #306 from cardano-foundation/chore/report_version_…
M4rc0Russ0 Jun 18, 2025
4d54947
Feat/aggregate on publisher (#308)
Kammerlo Jun 19, 2025
dd90c1e
Feat/aggregate Public Pages items (#310)
Kammerlo Jun 20, 2025
c97deaa
fix: change the amount reatment from Long to bigDecimal in the Public…
M4rc0Russ0 Jun 24, 2025
d5301d4
Merge pull request #311 from cardano-foundation/fix/change_long_to_bi…
M4rc0Russ0 Jun 24, 2025
30cd93f
fix: when TxType is FxReval
M4rc0Russ0 Jun 24, 2025
5cdb769
Merge pull request #312 from cardano-foundation/fix/fxreval
M4rc0Russ0 Jun 25, 2025
1346cb6
fix: rename the dummy account hard coded
M4rc0Russ0 Jun 25, 2025
73e2765
Merge pull request #314 from cardano-foundation/fix/rename_dummy_account
M4rc0Russ0 Jun 25, 2025
631ce7c
chore: removing exception throw for null objects and skipping them (#…
Kammerlo Jun 27, 2025
4b21734
fix: report validation
M4rc0Russ0 Jul 1, 2025
00a8bf3
Merge pull request #321 from cardano-foundation/fix/report_validation
M4rc0Russ0 Jul 2, 2025
a949d03
feat: adding API versioning (#320)
Kammerlo Jul 8, 2025
5e6d0e4
Update onChainFormat.md (#329)
Kammerlo Jul 8, 2025
bc9b06c
fix: avoiding to overwriting statuses (#326)
Kammerlo Jul 8, 2025
980d260
Merge branch 'main' into release/1.0.0
Kammerlo Jul 8, 2025
559d851
Update version in gradle.properties
Kammerlo Jul 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ name: Build

on:
push:
branches: [ main, develop ]
branches:
- main
- develop
- 'release/*'
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
pull_request:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

@RestController
@CrossOrigin(origins = "http://localhost:3000")
@RequestMapping("/api")
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
public class LedgerFollowerResource {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public BusinessRulesPipelineProcessor reprocessBusinessRulesProcessor() {
val pipelineTasks = new ArrayList<PipelineTask>();

pipelineTasks.add(conversionPipelineTask());
pipelineTasks.add(postCleansingPipelineTask());

return new DefaultBusinessRulesPipelineProcessor(pipelineTasks);
}
Expand All @@ -69,8 +70,7 @@ private PipelineTask sanityCheckPipelineTask() {

private PipelineTask preCleansingPipelineTask() {
return new DefaultPipelineTask(List.of(
new DiscardZeroBalanceTxItemsTaskItem(),
new JournalAccountCreditEnrichmentTaskItem(organisationPublicApi)
new DiscardZeroBalanceTxItemsTaskItem()
));
}

Expand All @@ -79,7 +79,8 @@ private PipelineTask preValidationPipelineTask() {
new AmountsFcyCheckTaskItem(),
new AmountsLcyCheckTaskItem(),
new AmountLcyBalanceZerosOutCheckTaskItem(),
new AmountFcyBalanceZerosOutCheckTaskItem()
new AmountFcyBalanceZerosOutCheckTaskItem(),
new JournalAccountCreditEnrichmentTaskItem(organisationPublicApi)
));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ public String getId() {
@Setter
private Boolean ledgerDispatchApproved = false;

@Column(name = "is_ready_to_publish", nullable = false)
@Getter
@Setter
private Boolean isReadyToPublish = false;

@Column(name = "ledger_dispatch_status", nullable = false)
@Enumerated(STRING)
@JdbcType(PostgreSQLEnumJdbcType.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package org.cardanofoundation.lob.app.accounting_reporting_core.job;

import java.time.LocalDate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import io.vavr.control.Either;
import org.zalando.problem.Problem;

import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.LedgerDispatchStatus;
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.core.TxStatusUpdate;
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.TransactionEntity;
import org.cardanofoundation.lob.app.accounting_reporting_core.domain.entity.report.ReportEntity;
import org.cardanofoundation.lob.app.accounting_reporting_core.repository.ReportRepository;
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.LedgerService;
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.ReportService;
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.TransactionBatchService;

@Service
@Slf4j
@RequiredArgsConstructor
@ConditionalOnProperty(value = "lob.accounting_reporting_core.enabled", havingValue = "true", matchIfMissing = true)
public class TxStatusUpdaterJob {

private final Map<String, TxStatusUpdate> txStatusUpdatesMap = new ConcurrentHashMap<>();
private final LedgerService ledgerService;
private final TransactionBatchService transactionBatchService;
private final ReportRepository reportRepository;
private final ReportService reportService;

@Value("${lob.blockchain.tx-status-updater.max-map-size:1000}")
private int maxMapSize;

// This Job collects all TxStatusUpdate events and updates the transactions in the database
@Scheduled(
fixedDelayString = "${ob.blockchain.tx-status-updater.fixed_delay:PT30S}",
initialDelayString = "${lob.blockchain.tx-status-updater.delay:PT30S}")
@Transactional
public void execute() {
Map<String, TxStatusUpdate> updates;
synchronized (txStatusUpdatesMap) {
updates = new HashMap<>(txStatusUpdatesMap);
}
if(updates.isEmpty()) {
log.debug("No TxStatusUpdate events to process");
return;
}
try {
log.info("Updating Status of {} transactions", updates.size());
List<TransactionEntity> transactionEntities = ledgerService.updateTransactionsWithNewStatuses(updates);
ledgerService.saveAllTransactionEntities(transactionEntities);

transactionBatchService.updateBatchesPerTransactions(updates);
updates.forEach(txStatusUpdatesMap::remove);

// Updating respective reports - Could be refactored to a separate method
Set<ReportEntity> reportEntitiesToBeUpdated = new HashSet<>();
for(TransactionEntity tx : transactionEntities) {
if (tx.getLedgerDispatchStatus() == LedgerDispatchStatus.FINALIZED) {
LocalDate date = tx.getEntryDate();
int year = date.getYear();
int month = date.getMonthValue();
int quarter = (month - 1) / 3 + 1;
reportEntitiesToBeUpdated.addAll(reportRepository.findNotPublishedByOrganisationIdAndContainingDate(tx.getOrganisation().getId(), year, quarter, month));
}
}

reportEntitiesToBeUpdated.forEach(report -> {
log.info("Checking if report {} is ready to publish", report.getId());
Either<Problem, Boolean> isReadyToPublish = reportService.canPublish(report);
if (isReadyToPublish.isLeft()) {
log.error("Report {} cannot be published: {}", report.getId(), isReadyToPublish.getLeft().getDetail());
return;
}
report.setIsReadyToPublish(isReadyToPublish.get());
reportRepository.save(report);
});

} catch (Exception e) {
log.error("Failed to process TxStatusUpdates - entries will be retained in the map", e);
}
}

public void addToStatusUpdateMap(Map<String, TxStatusUpdate> updateMap) {
synchronized (txStatusUpdatesMap) {
updateMap.forEach((key, value) -> {
txStatusUpdatesMap.merge(key, value, (oldValue, newValue) ->
newValue.getStatus().compareTo(oldValue.getStatus()) > 0 ? newValue : oldValue
);
});
}
if(txStatusUpdatesMap.size() > maxMapSize) {
log.warn("TxStatusUpdate map size exceeded the limit of {}. Current size: {}", maxMapSize, txStatusUpdatesMap.size());
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

public interface ReportRepository extends JpaRepository<ReportEntity, String> {

Optional<ReportEntity> findFirstByOrganisationIdAndReportId(@Param("organisationId") String organisationId, @Param("reportId") String reportId);

@Query("""
SELECT r FROM accounting_reporting_core.report.ReportEntity r
WHERE r.organisation.id = :organisationId
Expand Down Expand Up @@ -75,4 +77,16 @@ List<ReportEntity> getNewestReportsInRange(@Param("organisationId") String organ
AND r.ledgerDispatchStatus = 'FINALIZED'
""")
Set<ReportEntity> findByTypeAndWithinYearRange(@Param("organisationId") String organisationId, @Param("reportType") ReportType reportType, @Param("startYear") int startYear, @Param("endYear") int endYear);

@Query("""
SELECT r FROM accounting_reporting_core.report.ReportEntity r
WHERE
r.organisation.id = :organisationId
AND r.ledgerDispatchStatus = 'NOT_DISPATCHED'
AND
(r.intervalType = 'YEAR' AND r.year >= :year)
OR (r.intervalType = 'QUARTER' AND ((r.year = :year AND r.period >= :quarter) OR (r.year > :year)))
OR (r.intervalType = 'MONTH' AND ((r.year = :year AND r.period >= :month) OR (r.year > :year)))
""")
Set<ReportEntity> findNotPublishedByOrganisationIdAndContainingDate(@Param("organisationId") String organisationId, @Param("year") int year, @Param("quarter") int quarter, @Param("month") int month);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

@RestController
@CrossOrigin(origins = "http://localhost:3000")
@RequestMapping("/api")
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty(value = "lob.accounting_reporting_core.enabled", havingValue = "true", matchIfMissing = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

@RestController
@CrossOrigin(origins = "http://localhost:3000")
@RequestMapping("/api")
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty(value = "lob.accounting_reporting_core.enabled", havingValue = "true", matchIfMissing = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

@RestController
@CrossOrigin(origins = "http://localhost:3000")
@RequestMapping("/api")
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty(value = "lob.accounting_reporting_core.enabled", havingValue = "true", matchIfMissing = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.TransactionBatchService;

@RestController
@RequestMapping("/api/core")
@RequestMapping("/api/v1/core")
@Slf4j
@RequiredArgsConstructor
@Deprecated(forRemoval = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import org.cardanofoundation.lob.app.support.date.FlexibleDateParser;

@RestController
@RequestMapping("/api")
@RequestMapping("/api/v1")
@CrossOrigin(origins = "http://localhost:3000")
@RequiredArgsConstructor
@Slf4j
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.metrics.MetricService;

@RestController
@RequestMapping("/api/metrics")
@RequestMapping("/api/v1/metrics")
@CrossOrigin(origins = "http://localhost:3000")
@RequiredArgsConstructor
@Slf4j
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import org.cardanofoundation.lob.app.organisation.OrganisationPublicApi;

@RestController
@RequestMapping("/api/public")
@RequestMapping("/api/v1/public")
@RequiredArgsConstructor
@Slf4j
public class PublicInterfaceController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import jakarta.validation.Valid;
Expand All @@ -28,7 +29,7 @@
import org.cardanofoundation.lob.app.organisation.service.OrganisationCurrencyService;

@RestController
@RequestMapping("/api")
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty(value = "lob.accounting_reporting_core.enabled", havingValue = "true", matchIfMissing = true)
Expand All @@ -42,13 +43,23 @@ public class ReportController {
@PreAuthorize("hasRole(@securityConfig.getManagerRole()) or hasRole(@securityConfig.getAccountantRole())")
public ResponseEntity<ReportResponseView> reportGenerate(@Valid @RequestBody ReportGenerateRequest reportGenerateRequest) {
return reportService.reportGenerate(reportGenerateRequest).fold(
problem -> {
return ResponseEntity.status(problem.getStatus().getStatusCode()).body(ReportResponseView.createFail(problem));
}, success -> {
return ResponseEntity.ok().body(
ReportResponseView.createSuccess(List.of(reportViewService.responseView(success)))
);
}
problem ->
ResponseEntity.status(Objects.requireNonNull(problem.getStatus()).getStatusCode()).body(ReportResponseView.createFail(problem)),
success -> ResponseEntity.ok().body(
ReportResponseView.createSuccess(List.of(reportViewService.responseView(success)))
)
);
}

@Tag(name = "Reporting", description = "Reprocess Report")
@PostMapping(value = "/report-reprocess", produces = "application/json")
@PreAuthorize("hasRole(@securityConfig.getManagerRole()) or hasRole(@securityConfig.getAccountantRole())")
public ResponseEntity<ReportResponseView> reportReprocess(@Valid @RequestBody ReportReprocessRequest reportReprocessRequest) {
return reportService.reportReprocess(reportReprocessRequest).fold(
problem -> ResponseEntity.status(Objects.requireNonNull(problem.getStatus()).getStatusCode()).body(ReportResponseView.createFail(problem)),
success -> ResponseEntity.ok().body(
ReportResponseView.createSuccess(List.of(reportViewService.responseView(success)))
)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.cardanofoundation.lob.app.accounting_reporting_core.resource.views.*;
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.AccountingCoreService;
import org.cardanofoundation.lob.app.accounting_reporting_core.service.internal.TransactionRepositoryGateway;
import org.cardanofoundation.lob.app.organisation.OrganisationPublicApiIF;
import org.cardanofoundation.lob.app.organisation.domain.entity.OrganisationCostCenter;
import org.cardanofoundation.lob.app.organisation.domain.entity.OrganisationProject;
import org.cardanofoundation.lob.app.organisation.repository.CostCenterRepository;
Expand All @@ -57,6 +58,7 @@ public class AccountingCorePresentationViewService {
private final TransactionReconcilationRepository transactionReconcilationRepository;
private final CostCenterRepository costCenterRepository;
private final ProjectMappingRepository projectMappingRepository;
private final OrganisationPublicApiIF organisationPublicApiIF;

/**
* TODO: waiting for refactoring the layer to remove this
Expand Down Expand Up @@ -458,8 +460,8 @@ private Set<TransactionItemView> getTransactionItemView(TransactionEntity transa
item.getAccountCredit().map(Account::getCode).orElse(""),
item.getAccountCredit().flatMap(Account::getName).orElse(""),
item.getAccountCredit().flatMap(Account::getRefCode).orElse(""),
transaction.getTransactionType().equals(TransactionType.FxRevaluation) ? item.getAmountFcy() : item.getAmountFcy().abs(),
transaction.getTransactionType().equals(TransactionType.FxRevaluation) ? item.getAmountLcy() : item.getAmountLcy().abs(),
item.getOperationType().equals(OperationType.CREDIT) ? item.getAmountFcy().negate() : item.getAmountFcy(),
item.getOperationType().equals(OperationType.CREDIT) ? item.getAmountLcy().negate() : item.getAmountLcy(),
item.getFxRate(),
item.getCostCenter().map(CostCenter::getCustomerCode).orElse(""),
item.getCostCenter().flatMap(CostCenter::getExternalCustomerCode).orElse(""),
Expand Down Expand Up @@ -497,38 +499,39 @@ private Set<ViolationView> getViolations(TransactionEntity transaction) {

private TransactionReconciliationTransactionsView getReconciliationTransactionsSelector(Object[] violations) {
for (Object o : violations) {
if (Objects.isNull(o)) {
continue;
}
if (o instanceof TransactionEntity transactionEntity && transactionEntity.getLastReconcilation().isPresent()) {
return getTransactionReconciliationView(transactionEntity);
}
if (o instanceof ReconcilationViolation reconcilationViolation) {
return getTransactionReconciliationViolationView(reconcilationViolation);
}

try {
log.warn("Object type: {}", o.getClass());
} catch (Exception e) {
log.warn("\nempty object: {}\n", o);
}

}

return getTransactionReconciliationViolationView();
}

public BigDecimal getAmountLcyTotalForAllDebitItems(TransactionEntity tx) {
Set<TransactionItemEntity> items = tx.getItems();

if (tx.getTransactionType().equals(TransactionType.Journal)) {
items = tx.getItems().stream().filter(txItems -> txItems.getOperationType().equals(OperationType.DEBIT)).collect(toSet());
Optional<String> dummyAccount = organisationPublicApiIF.findByOrganisationId(tx.getOrganisation().getId()).orElse(new org.cardanofoundation.lob.app.organisation.domain.entity.Organisation()).getDummyAccount();
items = tx.getItems().stream().filter(txItems -> txItems.getAccountDebit().isPresent() && txItems.getAccountDebit().get().getCode().equals(dummyAccount.orElse(""))).collect(toSet());
}

if (tx.getTransactionType().equals(TransactionType.FxRevaluation)) {
items.stream()
BigDecimal totalCredit = items.stream()
.filter(item -> item.getOperationType().equals(OperationType.CREDIT))
.forEach(item -> {
item.setAmountLcy(item.getAmountLcy().negate());
item.setAmountFcy(item.getAmountFcy().negate());
});
.map(TransactionItemEntity::getAmountLcy)
.reduce(ZERO, BigDecimal::add); // Use ZERO as identity for sum

BigDecimal totalDebit = items.stream()
.filter(item -> item.getOperationType().equals(OperationType.DEBIT))
.map(TransactionItemEntity::getAmountLcy)
.reduce(ZERO, BigDecimal::add); // Use ZERO as identity for sum

return totalCredit.subtract(totalDebit).abs();
}

return items.stream()
Expand Down
Loading