Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions docs/WEB-1256-transaction-history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# WEB-1256 Base Teller transaction history

## Source-of-truth matrix

The read model emits one history row for one operational teller movement. Plugin workflow tables
are authoritative where they exist. A native `m_cashier_transactions` row is emitted only when it
is not linked to a plugin workflow, which prevents accounting the same movement twice.

| History source | Authoritative record | Operation | Teller-relative effect | Status/date |
|---|---|---|---|---|
| `SAVINGS_DEPOSIT` | `m_base_teller_deposit` | `SAVINGS_DEPOSIT` | inflow | workflow status / completed timestamp |
| `SAVINGS_OPENING` | `m_base_teller_savings_opening` | `SAVINGS_OPENING` | inflow | workflow status / completed timestamp |
| `RETURNED_CHECK_PAYMENT` | `m_returned_check_payment` | `RETURNED_CHECK_SETTLEMENT` | inflow | workflow status / business date |
| `SERVICE_PAYMENT` | `m_service_payment` | `PAY_SERVICE` | inflow | workflow status / business date |
| `CREDIT_PAYMENT` | `m_base_teller_credit_payment` | `CREDIT_PAYMENT` | inflow | workflow status / business date |
| `CASH_ALLOCATION` | `m_cash_allocation` | allocation operation type | source outflow or destination inflow | workflow status / business date |
| `CASHIER_CLOSING` | `m_cashier_reconciliation` | `CASHIER_CLOSING` | outflow | workflow status / business date |
| `CASH_OPERATION` | `m_cash_operation_transaction` | cash operation type | outflow | workflow status / business date |
| `CASHIER_TRANSACTION` | unlinked `m_cashier_transactions` | native transaction type | types 101/103 inflow; 102/104 outflow | completed / transaction date |

The returned-check detection event, catalog changes, inventory snapshots, journal entries, loan
transactions, savings transactions, receipts, and denomination rows are not independent history
movements. They are status, accounting, or supporting evidence and are therefore not summed.

## Stable identifiers and amounts

History IDs are opaque canonical strings in the form `<SOURCE_TYPE>:<SOURCE_ID>`. Detail,
denomination, and receipt endpoints accept this same identifier. IDs are not derived from a list
position or a mutable receipt number.

`inflow` and `outflow` are teller-relative and non-negative. `total` is always
`totalInflows - totalOutflows`; totals are calculated over the complete filtered data set, not only
the requested page, and are kept separate by currency. Cash received and change are shown only
when the authoritative workflow persisted them. Reference filtering searches business-facing
references and descriptions, not idempotency keys.

## Denominations and receipts

Denominations are returned only from persisted denomination tables. An empty list with a false
support flag means the source did not persist that kind of breakdown; the API does not infer it.
The current workflows do not persist change denominations, so `changeDenominationsSupported` is
false. A denomination type is null when the source did not record one.

Receipt payloads delegate to the existing workflow receipt/read service. Unsupported native or
cash-operation records explicitly return `receiptSupported=false`. The report endpoint returns the
full filtered, structured data set for Web App printing and does not claim to produce a PDF.

## Authorization and office scope

Every route and service entry point requires `READ_BASE_TELLER_TRANSACTION_HISTORY`. Office scope
is derived from the authenticated user's office hierarchy. A user with an active cashier
assignment is narrowed to that assignment unless the user also has the existing
`READ_GLOBAL_SETTLEMENT` authority. Client-provided office or teller IDs can only narrow the
server-computed scope; they cannot widen it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package org.apache.fineract.baseteller.api;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.baseteller.data.TransactionHistoryContextData;
import org.apache.fineract.baseteller.data.TransactionHistoryDenominationsData;
import org.apache.fineract.baseteller.data.TransactionHistoryDetailData;
import org.apache.fineract.baseteller.data.TransactionHistoryQuery;
import org.apache.fineract.baseteller.data.TransactionHistoryReceiptData;
import org.apache.fineract.baseteller.data.TransactionHistoryReportData;
import org.apache.fineract.baseteller.data.TransactionHistorySearchData;
import org.apache.fineract.baseteller.service.TransactionHistoryReadPlatformService;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.stereotype.Component;

@Path("/v2/base-teller/transaction-history")
@Component
@Produces(MediaType.APPLICATION_JSON)
@Tag(
name = "Base Teller Transaction History",
description =
"Authorized, source-backed history of Base Teller transactions. Requires READ_BASE_TELLER_TRANSACTION_HISTORY.")
@RequiredArgsConstructor
public class TransactionHistoryApiResource {

private static final String PERMISSION = "READ_BASE_TELLER_TRANSACTION_HISTORY";

private final PlatformSecurityContext securityContext;
private final TransactionHistoryReadPlatformService readPlatformService;

@GET
@Path("/context")
@Operation(summary = "Retrieve authorized transaction-history filter values")
public TransactionHistoryContextData context() {
authorize();
return readPlatformService.context();
}

@GET
@Operation(
summary = "Search Base Teller transaction history",
description =
"Returns one canonical row per logical transaction, database-side pagination, and totals over the complete filtered result.")
public TransactionHistorySearchData search(
@QueryParam("fromDate") final String fromDate,
@QueryParam("toDate") final String toDate,
@Parameter(description = "Cashier identifier returned by context") @QueryParam("tellerId")
final Long tellerId,
@QueryParam("currencyCode") final String currencyCode,
@QueryParam("status") final String status,
@QueryParam("type") final String type,
@QueryParam("operation") final String operation,
@QueryParam("concept") final String concept,
@QueryParam("reference") final String reference,
@QueryParam("offset") final Integer offset,
@QueryParam("limit") final Integer limit,
@QueryParam("sort") final String sort,
@QueryParam("order") final String order) {
authorize();
return readPlatformService.search(
query(
fromDate,
toDate,
tellerId,
currencyCode,
status,
type,
operation,
concept,
reference,
offset,
limit,
sort,
order));
}

@GET
@Path("/report")
@Operation(
summary = "Retrieve the complete filtered history report dataset",
description =
"Returns all matching rows in stable order plus per-currency totals for Web App printing.")
public TransactionHistoryReportData report(
@QueryParam("fromDate") final String fromDate,
@QueryParam("toDate") final String toDate,
@QueryParam("tellerId") final Long tellerId,
@QueryParam("currencyCode") final String currencyCode,
@QueryParam("status") final String status,
@QueryParam("type") final String type,
@QueryParam("operation") final String operation,
@QueryParam("concept") final String concept,
@QueryParam("reference") final String reference,
@QueryParam("sort") final String sort,
@QueryParam("order") final String order) {
authorize();
return readPlatformService.report(
query(
fromDate,
toDate,
tellerId,
currencyCode,
status,
type,
operation,
concept,
reference,
null,
null,
sort,
order));
}

@GET
@Path("/{historyId}")
@Operation(summary = "Retrieve source-authoritative transaction detail")
public TransactionHistoryDetailData detail(@PathParam("historyId") final String historyId) {
authorize();
return readPlatformService.detail(historyId);
}

@GET
@Path("/{historyId}/denominations")
@Operation(
summary = "Retrieve exact persisted denomination detail",
description = "Unsupported or uncaptured breakdowns are returned explicitly and never inferred.")
public TransactionHistoryDenominationsData denominations(
@PathParam("historyId") final String historyId) {
authorize();
return readPlatformService.denominations(historyId);
}

@GET
@Path("/{historyId}/receipt")
@Operation(
summary = "Retrieve or reprint the original transaction receipt",
description = "Delegates to the authoritative source receipt service without financial side effects.")
public TransactionHistoryReceiptData receipt(@PathParam("historyId") final String historyId) {
authorize();
return readPlatformService.receipt(historyId);
}

private void authorize() {
securityContext.authenticatedUser().validateHasPermissionTo(PERMISSION);
}

private static TransactionHistoryQuery query(
final String fromDate,
final String toDate,
final Long tellerId,
final String currencyCode,
final String status,
final String type,
final String operation,
final String concept,
final String reference,
final Integer offset,
final Integer limit,
final String sort,
final String order) {
return new TransactionHistoryQuery(
date(fromDate, "fromDate"),
date(toDate, "toDate"),
tellerId,
currencyCode,
status,
type,
operation,
concept,
reference,
offset,
limit,
sort,
order);
}

private static LocalDate date(final String value, final String field) {
if (StringUtils.isBlank(value)) {
return null;
}
try {
return LocalDate.parse(value);
} catch (RuntimeException exception) {
throw new GeneralPlatformDomainRuleException(
"error.msg.base.teller.transaction.history.date.invalid",
field + " must use ISO yyyy-MM-dd format.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.apache.fineract.baseteller.data;

import java.time.OffsetDateTime;

public record TransactionHistoryCancellationData(String reason, String user, OffsetDateTime date) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.apache.fineract.baseteller.data;

public record TransactionHistoryClientData(Long id, String displayName, String identification) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.apache.fineract.baseteller.data;

import java.util.List;

public record TransactionHistoryContextData(
List<TransactionHistoryTellerData> tellers,
List<TransactionHistoryCurrencyData> currencies,
List<TransactionHistoryOptionData> statuses,
List<TransactionHistoryOptionData> types,
List<TransactionHistoryOptionData> operations,
List<TransactionHistoryOptionData> concepts) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.apache.fineract.baseteller.data;

public record TransactionHistoryCurrencyData(String code, String name, Integer decimalPlaces) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.apache.fineract.baseteller.data;

import java.math.BigDecimal;

public record TransactionHistoryDenominationLineData(
String currencyCode,
String denominationId,
BigDecimal denominationValue,
Long quantity,
BigDecimal amount,
String denominationType) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.apache.fineract.baseteller.data;

import java.util.List;

public record TransactionHistoryDenominationsData(
String historyId,
boolean operationDenominationsSupported,
List<TransactionHistoryDenominationLineData> operationDenominations,
boolean changeDenominationsSupported,
List<TransactionHistoryDenominationLineData> changeDenominations) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.apache.fineract.baseteller.data;

import java.math.BigDecimal;
import java.time.OffsetDateTime;

public record TransactionHistoryDetailData(
String historyId,
OffsetDateTime transactionDate,
TransactionHistoryClientData client,
String operation,
String concept,
String reference,
TransactionHistoryTellerData teller,
String status,
String currencyCode,
Integer decimalPlaces,
BigDecimal cashReceived,
BigDecimal checksReceived,
BigDecimal change,
BigDecimal adjustment,
BigDecimal total,
TransactionHistoryCancellationData cancellation,
boolean receiptSupported) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.apache.fineract.baseteller.data;

import java.math.BigDecimal;
import java.time.OffsetDateTime;

public record TransactionHistoryItemData(
String historyId,
OffsetDateTime transactionDate,
String operation,
BigDecimal inflow,
BigDecimal outflow,
String currencyCode,
Integer decimalPlaces,
String concept,
String status,
String reference,
Long tellerId,
Long clientId) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.apache.fineract.baseteller.data;

public record TransactionHistoryOptionData(String code, String label) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.apache.fineract.baseteller.data;

import java.time.LocalDate;

public record TransactionHistoryQuery(
LocalDate fromDate,
LocalDate toDate,
Long tellerId,
String currencyCode,
String status,
String type,
String operation,
String concept,
String reference,
Integer offset,
Integer limit,
String sort,
String order) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.apache.fineract.baseteller.data;

public record TransactionHistoryReceiptData(
String historyId, String sourceType, Long sourceId, Object receipt) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.apache.fineract.baseteller.data;

import java.time.OffsetDateTime;
import java.util.List;

public record TransactionHistoryReportData(
OffsetDateTime generatedAt,
List<TransactionHistoryItemData> items,
long totalFilteredRecords,
List<TransactionHistoryTotalsData> totalsByCurrency) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.apache.fineract.baseteller.data;

import java.util.List;

public record TransactionHistorySearchData(
List<TransactionHistoryItemData> items,
long totalFilteredRecords,
int offset,
int limit,
List<TransactionHistoryTotalsData> totalsByCurrency) {}
Loading
Loading