bug fixes

This commit is contained in:
2026-06-25 00:39:58 +02:00
parent db1fe2672a
commit dc0c42d12e
17 changed files with 532 additions and 41 deletions

View File

@@ -36,6 +36,7 @@ public class SecurityConfig {
.requestMatchers("/nflow/**").permitAll()
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/public/**").permitAll()
.requestMatchers("/explorer/**").permitAll()
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider(userService))

View File

@@ -1,6 +1,12 @@
package zw.qantra.tm.domain.controllers;
import lombok.RequiredArgsConstructor;
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
import net.kaczmarzyk.spring.data.jpa.web.annotation.And;
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import zw.qantra.tm.domain.models.PaymentProcessor;
@@ -18,8 +24,12 @@ public class PaymentProcessorController {
private final PaymentProcessorService paymentProcessorService;
@GetMapping
public ResponseEntity<List<PaymentProcessor>> getAllPaymentProcessors() {
return ResponseEntity.ok(paymentProcessorService.getPaymentProcessorRepository().findAll());
public ResponseEntity getAllPaymentProcessors(
@And({
@Spec(path = "label", spec = Equal.class),
@Spec(path = "currency", spec = Equal.class),
})Specification<PaymentProcessor> specification, Pageable pageable) {
return ResponseEntity.ok(paymentProcessorService.getAllPaymentProcessors(specification, pageable));
}
@GetMapping("/{id}")

View File

@@ -0,0 +1,87 @@
package zw.qantra.tm.domain.controllers;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.services.TransactionReportService;
import zw.qantra.tm.domain.services.TransactionReportService.TransactionReportFileResult;
import zw.qantra.tm.domain.services.TransactionReportService.TransactionReportSummary;
import java.time.LocalDateTime;
import java.util.List;
@RestController
@RequestMapping("/public/reports/transactions")
@RequiredArgsConstructor
public class TransactionReportController {
private final TransactionReportService transactionReportService;
/**
* Generates a transaction report filtered by date range.
* Transactions are automatically filtered to only include
* partyType = INDIVIDUAL or GROUP.
*
* @param startDate start of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
* @param endDate end of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
* @param workspaceId optional workspace ID filter
* @return list of matching transactions
*/
@GetMapping
public ResponseEntity<List<Transaction>> getTransactionReport(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
@RequestParam(required = false) String workspaceId) {
List<Transaction> transactions = transactionReportService.generateReport(startDate, endDate, workspaceId);
return ResponseEntity.ok(transactions);
}
/**
* Generates a transaction report summary with counts and totals,
* filtered by date range and default party type filter.
*
* @param startDate start of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
* @param endDate end of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
* @param workspaceId optional workspace ID filter
* @return report summary with aggregated data
*/
@GetMapping("/summary")
public ResponseEntity<TransactionReportSummary> getTransactionReportSummary(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
@RequestParam(required = false) String workspaceId) {
TransactionReportSummary summary = transactionReportService.generateReportSummary(startDate, endDate, workspaceId);
return ResponseEntity.ok(summary);
}
/**
* Downloads a transaction report as an Excel file (.xlsx).
* The report is filtered by date range, with a default filter for
* partyType = INDIVIDUAL or GROUP.
*
* @param startDate start of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
* @param endDate end of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
* @param workspaceId optional workspace ID filter
* @return the generated Excel file as a downloadable resource
*/
@GetMapping("/download")
public ResponseEntity<Resource> downloadTransactionReport(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
@RequestParam(required = false) String workspaceId) {
TransactionReportFileResult reportFile = transactionReportService.generateReportFile(startDate, endDate, workspaceId);
ByteArrayResource resource = new ByteArrayResource(reportFile.fileBytes());
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + reportFile.filename() + "\"")
.body(resource);
}
}

View File

@@ -65,5 +65,6 @@ public class GroupBatchItem extends BaseEntity {
private String billProductName;
private String providerImage;
private String providerLabel;
private String creditAddress;
}

View File

@@ -3,6 +3,7 @@ package zw.qantra.tm.domain.models;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLRestriction;
import zw.qantra.tm.domain.enums.CurrencyType;
@Entity
@Table(name = "payment_processor", indexes = {
@@ -21,6 +22,8 @@ public class PaymentProcessor extends BaseEntity {
private String type;
@Column(columnDefinition = "TEXT")
private String image;
@Enumerated(EnumType.STRING)
private CurrencyType currency;
private String accountFieldName;
private String accountFieldLabel;
private String authType;

View File

@@ -37,6 +37,7 @@ public class Transaction extends BaseEntity {
private UUID workspaceId;
@Column(name = "user_id")
private String userId;
private String username;
private String trace;
private String region;
@Enumerated(EnumType.STRING)
@@ -87,6 +88,7 @@ public class Transaction extends BaseEntity {
private String providerImage;
private String paymentProcessorImage;
private UUID batchId;
private String batchDescription;
@Enumerated(EnumType.STRING)
private Status authorizationStatus;
@@ -105,7 +107,6 @@ public class Transaction extends BaseEntity {
@Column(name = "polling_status")
private Status pollingStatus;
private String orderId;
private String orderTrace;
private String orderTransactionTrace;

View File

@@ -1,12 +1,16 @@
package zw.qantra.tm.domain.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.models.PaymentProcessor;
import java.util.UUID;
@Repository
public interface PaymentProcessorRepository extends JpaRepository<PaymentProcessor, UUID> {
public interface PaymentProcessorRepository extends JpaRepository<PaymentProcessor, UUID>,
JpaSpecificationExecutor<PaymentProcessor> {
boolean existsByLabel(String label);
boolean existsByLabelAndCurrency(String label, CurrencyType currencyType);
}

View File

@@ -119,6 +119,7 @@ public class GroupBatchWorkflowService {
transaction.setPaymentProcessorName(batch.getPaymentProcessorName());
transaction.setPaymentProcessorImage(batch.getPaymentProcessorImage());
transaction.setBatchId(batch.getId());
transaction.setBatchDescription(batch.getDescription());
transaction.setBillName("Batch - " + batch.getDescription());
transaction = calculateChargesHandler.process(transaction);
@@ -225,6 +226,7 @@ public class GroupBatchWorkflowService {
transaction.setProviderLabel(item.getProviderLabel());
transaction.setUserId(batch.getUserId());
transaction.setUsername(user.getUsername());
transaction.setWorkspaceId(batch.getWorkspaceId());
transaction.setType(RequestType.CONFIRM);
transaction.setPartyType(PartyType.BATCH);
@@ -250,6 +252,8 @@ public class GroupBatchWorkflowService {
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.CONFIRMED);
mutableItem.setStatus(GroupBatchItemStatus.CONFIRMED);
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.PENDING);
if(result.getCreditAddress() != null)
mutableItem.setCreditAddress(result.getCreditAddress());
mutableItem.setConfirmError(null);
} else {
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);

View File

@@ -1,7 +1,11 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.models.PaymentProcessor;
import zw.qantra.tm.domain.repositories.PaymentProcessorRepository;
@Service
@@ -12,4 +16,8 @@ public class PaymentProcessorService {
public PaymentProcessorRepository getPaymentProcessorRepository() {
return paymentProcessorRepository;
}
public Page<PaymentProcessor> getAllPaymentProcessors(Specification<PaymentProcessor> specification, Pageable pageable) {
return paymentProcessorRepository.findAll(specification, pageable);
}
}

View File

@@ -240,7 +240,18 @@ public class RecipientGroupService {
item.setAmount(productDto.getDefaultAmount());
}
updatedItems.add(groupBatchItemRepository.save(item));
// Recalculate batch totals
BigDecimal newTotalValue = updatedItems.stream()
.map(GroupBatchItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
GroupBatch batch = groupBatchRepository.findById(batchId)
.orElseThrow(() -> new ApiException("Batch not found"));
batch.setValue(newTotalValue);
batch.setCount(updatedItems.size());
groupBatchRepository.save(batch);
} catch (Exception e) {
e.printStackTrace();
providerErrors.add(GroupCreateFromFileResult.RowError.builder()
.lineNumber(update.row.lineNumber)
.account(update.row.account)
@@ -250,19 +261,6 @@ public class RecipientGroupService {
}
}
// Recalculate batch totals
if (!updatedItems.isEmpty()) {
List<GroupBatchItem> allItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
BigDecimal newTotalValue = allItems.stream()
.map(GroupBatchItem::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
GroupBatch batch = groupBatchRepository.findById(batchId)
.orElseThrow(() -> new ApiException("Batch not found"));
batch.setValue(newTotalValue);
batch.setCount(allItems.size());
groupBatchRepository.save(batch);
}
return providerErrors;
}

View File

@@ -0,0 +1,347 @@
package zw.qantra.tm.domain.services;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.PartyType;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.repositories.TransactionRepository;
import jakarta.persistence.criteria.Predicate;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TransactionReportService {
private static final String REPORT_FILENAME_PREFIX = "transaction-report-";
private static final String REPORT_FILENAME_SUFFIX = ".xlsx";
private static final short DATA_FONT_SIZE = 12;
private static final short HEADER_FONT_SIZE = 14;
private static final short TITLE_FONT_SIZE = 16;
@Value("${app.reports.storage-path:./reports}")
private String reportsStoragePath;
private final TransactionRepository transactionRepository;
/**
* Generates a transaction report filtered by date range and party type.
* The default filter restricts results to transactions where partyType is
* either INDIVIDUAL or GROUP (excluding BATCH).
*
* @param startDate the start of the date range (inclusive)
* @param endDate the end of the date range (inclusive)
* @return list of transactions matching the criteria
*/
public List<Transaction> generateReport(LocalDateTime startDate, LocalDateTime endDate) {
return generateReport(startDate, endDate, null);
}
/**
* Generates a transaction report filtered by date range, party type, and an
* optional workspace ID.
*
* @param startDate the start of the date range (inclusive)
* @param endDate the end of the date range (inclusive)
* @param workspaceId optional workspace ID to further filter transactions
* @return list of transactions matching the criteria
*/
public List<Transaction> generateReport(LocalDateTime startDate, LocalDateTime endDate,
String workspaceId) {
Specification<Transaction> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// Default filter: partyType must be INDIVIDUAL or GROUP
predicates.add(root.get("partyType").in(PartyType.INDIVIDUAL, PartyType.GROUP));
// Date range filter on createdAt
if (startDate != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), startDate));
}
if (endDate != null) {
predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), endDate));
}
// Optional workspace filter
if (workspaceId != null && !workspaceId.isEmpty()) {
predicates.add(cb.equal(root.get("workspaceId").as(String.class), workspaceId));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
return transactionRepository.findAll(spec);
}
/**
* Generates a transaction report and returns a summary with counts and totals.
*
* @param startDate the start of the date range (inclusive)
* @param endDate the end of the date range (inclusive)
* @return a report summary containing transactions and aggregated data
*/
public TransactionReportSummary generateReportSummary(LocalDateTime startDate, LocalDateTime endDate) {
return generateReportSummary(startDate, endDate, null);
}
/**
* Generates a transaction report summary with an optional workspace filter.
*
* @param startDate the start of the date range (inclusive)
* @param endDate the end of the date range (inclusive)
* @param workspaceId optional workspace ID to further filter transactions
* @return a report summary containing transactions and aggregated data
*/
public TransactionReportSummary generateReportSummary(LocalDateTime startDate, LocalDateTime endDate,
String workspaceId) {
List<Transaction> transactions = generateReport(startDate, endDate, workspaceId);
int totalCount = transactions.size();
int individualCount = 0;
int groupCount = 0;
for (Transaction txn : transactions) {
if (txn.getPartyType() == PartyType.INDIVIDUAL) {
individualCount++;
} else if (txn.getPartyType() == PartyType.GROUP) {
groupCount++;
}
}
return new TransactionReportSummary(
transactions,
totalCount,
individualCount,
groupCount,
startDate,
endDate
);
}
/**
* Generates an Excel workbook from a transaction report summary and returns the bytes.
*
* @param summary the report summary with transaction data
* @return byte array of the Excel file
*/
public byte[] generateExcelBytes(TransactionReportSummary summary) {
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Transaction Report");
// Styles
CellStyle titleStyle = workbook.createCellStyle();
Font titleFont = workbook.createFont();
titleFont.setBold(true);
titleFont.setFontHeightInPoints(TITLE_FONT_SIZE);
titleStyle.setFont(titleFont);
titleStyle.setAlignment(HorizontalAlignment.CENTER);
titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
CellStyle headerStyle = workbook.createCellStyle();
headerStyle.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerFont.setFontHeightInPoints(HEADER_FONT_SIZE);
headerFont.setColor(IndexedColors.WHITE.getIndex());
headerStyle.setFont(headerFont);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
CellStyle dataStyle = workbook.createCellStyle();
Font dataFont = workbook.createFont();
dataFont.setFontHeightInPoints(DATA_FONT_SIZE);
dataStyle.setFont(dataFont);
dataStyle.setAlignment(HorizontalAlignment.CENTER);
dataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
dataStyle.setBorderTop(BorderStyle.THIN);
dataStyle.setBorderBottom(BorderStyle.THIN);
dataStyle.setBorderLeft(BorderStyle.THIN);
dataStyle.setBorderRight(BorderStyle.THIN);
CellStyle currencyStyle = workbook.createCellStyle();
currencyStyle.cloneStyleFrom(dataStyle);
currencyStyle.setDataFormat(workbook.createDataFormat().getFormat("#,##0.00"));
CellStyle summaryLabelStyle = workbook.createCellStyle();
summaryLabelStyle.setFont(dataFont);
summaryLabelStyle.setAlignment(HorizontalAlignment.RIGHT);
summaryLabelStyle.setVerticalAlignment(VerticalAlignment.CENTER);
CellStyle summaryValueStyle = workbook.createCellStyle();
Font summaryValueFont = workbook.createFont();
summaryValueFont.setBold(true);
summaryValueFont.setFontHeightInPoints(DATA_FONT_SIZE);
summaryValueStyle.setFont(summaryValueFont);
summaryValueStyle.setAlignment(HorizontalAlignment.LEFT);
summaryValueStyle.setVerticalAlignment(VerticalAlignment.CENTER);
int rowNum = 0;
// Title row
Row titleRow = sheet.createRow(rowNum++);
Cell titleCell = titleRow.createCell(0);
String title = String.format("Transaction Report (%s - %s)",
summary.startDate().format(dateFormatter),
summary.endDate().format(dateFormatter));
titleCell.setCellValue(title);
titleCell.setCellStyle(titleStyle);
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 11));
titleRow.setHeightInPoints(30);
rowNum++;
// Summary rows
writeSummaryRow(sheet, rowNum++, "Total Transactions:", String.valueOf(summary.totalCount()), summaryLabelStyle, summaryValueStyle);
writeSummaryRow(sheet, rowNum++, "INDIVIDUAL:", String.valueOf(summary.individualCount()), summaryLabelStyle, summaryValueStyle);
writeSummaryRow(sheet, rowNum++, "GROUP:", String.valueOf(summary.groupCount()), summaryLabelStyle, summaryValueStyle);
rowNum++;
// Header row
String[] headers = {
"#", "Reference", "Party Type", "Amount", "Charge",
"Total Amount", "Currency", "Payment Status", "Integration Status", "Payment Processor Ref",
"Username", "Credit Phone",
"Velocity Ref", "Credit Name", "Provider", "Error Message", "Date", "Batch Name"
};
Row headerRow = sheet.createRow(rowNum++);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
headerRow.setHeightInPoints(25);
// Data rows
int index = 1;
for (Transaction txn : summary.transactions()) {
Row row = sheet.createRow(rowNum++);
writeCell(row, 0, index++, dataStyle);
writeCell(row, 1, txn.getReference() != null ? txn.getReference() : "", dataStyle);
writeCell(row, 2, txn.getPartyType() != null ? txn.getPartyType().name() : "", dataStyle);
writeCellAmount(row, 3, txn.getAmount(), currencyStyle);
writeCellAmount(row, 4, txn.getGatewayCharge(), currencyStyle);
writeCellAmount(row, 5, txn.getTotalAmount(), currencyStyle);
writeCell(row, 6, txn.getDebitCurrency().name(), dataStyle);
writeCell(row, 7, txn.getPollingStatus() != null ? txn.getPollingStatus().name() : "", dataStyle);
writeCell(row, 8, txn.getIntegrationStatus() != null ? txn.getIntegrationStatus().name() : "", dataStyle);
writeCell(row, 9, txn.getPaymentProcessorRef() != null ? txn.getPaymentProcessorRef() : "", dataStyle);
writeCell(row, 10, txn.getUsername() != null ? txn.getUsername() : txn.getDebitPhone(), dataStyle);
writeCell(row, 11, txn.getCreditPhone() != null ? txn.getCreditPhone() : "", dataStyle);
writeCell(row, 12, txn.getErpSalesPaymentRef() != null ? txn.getErpSalesPaymentRef() : "", dataStyle);
writeCell(row, 13, txn.getCreditName() != null ? txn.getCreditName() : "", dataStyle);
writeCell(row, 14, txn.getProviderLabel() != null ? txn.getProviderLabel() : "", dataStyle);
writeCell(row, 15, txn.getErrorMessage() != null ? txn.getErrorMessage() : "", dataStyle);
writeCell(row, 16, txn.getCreatedAt() != null ? txn.getCreatedAt().format(dateFormatter) : "", dataStyle);
writeCell(row, 17, txn.getBatchDescription() != null ? txn.getBatchDescription() : "", dataStyle);
}
// Auto-size columns
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
return outputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Failed to generate transaction report Excel file", e);
}
}
/**
* Generates a transaction report Excel file, saves it to disk, and returns the file URL.
*
* @param startDate the start of the date range (inclusive)
* @param endDate the end of the date range (inclusive)
* @param workspaceId optional workspace ID filter
* @return result containing the file URL and byte contents
*/
public TransactionReportFileResult generateReportFile(LocalDateTime startDate, LocalDateTime endDate,
String workspaceId) {
TransactionReportSummary summary = generateReportSummary(startDate, endDate, workspaceId);
byte[] fileBytes = generateExcelBytes(summary);
String datePart = startDate.format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
String filename = REPORT_FILENAME_PREFIX + datePart + REPORT_FILENAME_SUFFIX;
java.nio.file.Path reportsDir = java.nio.file.Path.of(reportsStoragePath).toAbsolutePath();
java.nio.file.Path filePath = reportsDir.resolve(filename);
try {
java.nio.file.Files.createDirectories(reportsDir);
try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(filePath)) {
os.write(fileBytes);
}
} catch (IOException e) {
throw new RuntimeException("Failed to persist transaction report file", e);
}
return new TransactionReportFileResult(filename, fileBytes);
}
private void writeSummaryRow(Sheet sheet, int rowNum, String label, String value,
CellStyle labelStyle, CellStyle valueStyle) {
Row row = sheet.createRow(rowNum);
Cell labelCell = row.createCell(0);
labelCell.setCellValue(label);
labelCell.setCellStyle(labelStyle);
Cell valueCell = row.createCell(1);
valueCell.setCellValue(value);
valueCell.setCellStyle(valueStyle);
}
private void writeCell(Row row, int col, String value, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(value);
cell.setCellStyle(style);
}
private void writeCell(Row row, int col, int value, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(value);
cell.setCellStyle(style);
}
private void writeCellAmount(Row row, int col, java.math.BigDecimal value, CellStyle style) {
Cell cell = row.createCell(col);
cell.setCellValue(value != null ? value.doubleValue() : 0.0);
cell.setCellStyle(style);
}
/**
* Result of a transaction report file generation request.
*/
public record TransactionReportFileResult(
String filename,
byte[] fileBytes
) {}
/**
* Summary of a transaction report.
*/
public record TransactionReportSummary(
List<Transaction> transactions,
int totalCount,
int individualCount,
int groupCount,
LocalDateTime startDate,
LocalDateTime endDate
) {}
}

View File

@@ -51,7 +51,7 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount(), "3")) {
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("98");
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
transaction.setErrorMessage("We can't process that transaction right now. Please try again later.");
return transaction;
}
}

View File

@@ -75,7 +75,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
if (!hotRechargeBalanceService.checkBalance(token, transaction.getCreditAmount(), accId)) {
transaction.setStatus(Status.FAILED);
transaction.setResponseCode("92");
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
transaction.setErrorMessage("We can't process that transaction right now. Please try again later.");
return transaction;
}

View File

@@ -122,6 +122,11 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
private LinkedHashMap<String, Object> performRecharge(Transaction transaction) {
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
try {
Thread.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
logger.info("Simulating HotRecharge success");
return Utils.fromJson("{\n" +
" \"successful\": true,\n" +

View File

@@ -6,13 +6,17 @@ import com.google.i18n.phonenumbers.Phonenumber;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import zw.qantra.tm.domain.enums.PartyType;
import zw.qantra.tm.domain.enums.RequestType;
import zw.qantra.tm.domain.enums.Status;
import zw.qantra.tm.domain.models.Provider;
import zw.qantra.tm.domain.models.Transaction;
import zw.qantra.tm.domain.models.User;
import zw.qantra.tm.domain.services.ProviderService;
import zw.qantra.tm.domain.services.UserService;
import zw.qantra.tm.exceptions.ApiException;
import zw.qantra.tm.utils.Utils;
@@ -24,6 +28,7 @@ public class CleanupHandler implements HandlerInterface {
private final Logger logger = LoggerFactory.getLogger(CleanupHandler.class);
private final ProviderService providerService;
private final UserService userService;
@Override
public Object process(Transaction transaction) throws Exception {
@@ -39,6 +44,11 @@ public class CleanupHandler implements HandlerInterface {
transaction.setPartyType(PartyType.INDIVIDUAL);
}
if(transaction.getUsername() == null) {
userService.getUserRepository().findById(UUID.fromString(transaction.getUserId()))
.ifPresent(user -> transaction.setUsername(user.getUsername()));
}
// UPDATE TRAN WITH CONFIRMATION HANDLER
// =======================
if(transaction.getType().equals(RequestType.CONFIRM)) {

View File

@@ -3,6 +3,7 @@ package zw.qantra.tm.seeders;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import zw.qantra.tm.domain.enums.CurrencyType;
import zw.qantra.tm.domain.models.PaymentProcessor;
import zw.qantra.tm.domain.repositories.PaymentProcessorRepository;
@@ -33,6 +34,18 @@ public class PaymentProcessorSeeder implements Seeder {
.accountFieldLabel("Wallet Phone Number")
.description("Pay using your CityWallet")
.authType("REMOTE")
.currency(CurrencyType.USD)
.build(),
PaymentProcessor.builder()
.name("CityWallet")
.label("WALLET")
.type("GATEWAY")
.image("city.png")
.accountFieldName("phoneNumber")
.accountFieldLabel("Wallet Phone Number")
.description("Pay using your CityWallet")
.authType("REMOTE")
.currency(CurrencyType.ZWG)
.build(),
PaymentProcessor.builder()
.name("Ecocash")
@@ -43,6 +56,7 @@ public class PaymentProcessorSeeder implements Seeder {
.accountFieldLabel("EcoCash Phone Number")
.description("Pay using your mobile wallet")
.authType("REMOTE")
.currency(CurrencyType.USD)
.build(),
PaymentProcessor.builder()
.name("Visa/MasterCard")
@@ -53,11 +67,12 @@ public class PaymentProcessorSeeder implements Seeder {
.accountFieldLabel("Card Number")
.description("Pay using your international card")
.authType("WEB")
.currency(CurrencyType.USD)
.build()
);
for (PaymentProcessor processor : processors) {
if (paymentProcessorRepository.existsByLabel(processor.getLabel())) {
if (paymentProcessorRepository.existsByLabelAndCurrency(processor.getLabel(), processor.getCurrency())) {
log.info("PaymentProcessor '{}' already exists, skipping.", processor.getLabel());
} else {
paymentProcessorRepository.save(processor);

View File

@@ -1,26 +1,23 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="khoza" id="convert-party-type-to-string">
<comment>Convert party_type column from integer ordinal to string enum values to match @Enumerated(EnumType.STRING) in Transaction.java</comment>
<!-- Add a temporary VARCHAR column -->
<changeSet author="khoza (generated)" id="1782340507017-3">
<addColumn tableName="transaction">
<column name="party_type_str" type="VARCHAR(20)">
<constraints nullable="true"/>
</column>
<column name="batch_description" type="varchar(255)"/>
</addColumn>
</changeSet>
<changeSet author="khoza (generated)" id="1782340507017-4">
<addColumn tableName="group_batch_item">
<column name="credit_address" type="varchar(255)"/>
</addColumn>
</changeSet>
<changeSet author="khoza (generated)" id="1782340507017-5">
<addColumn tableName="payment_processor">
<column name="currency" type="varchar(255)"/>
</addColumn>
</changeSet>
<changeSet author="khoza (generated)" id="1782340507017-6">
<addColumn tableName="transaction">
<column name="username" type="varchar(255)"/>
</addColumn>
<!-- Convert existing ordinal integer values (0=INDIVIDUAL, 1=GROUP, 2=BATCH) to strings -->
<sql>
UPDATE transaction SET party_type_str = 'INDIVIDUAL' WHERE party_type = 0 OR party_type = '0';
UPDATE transaction SET party_type_str = 'GROUP' WHERE party_type = 1 OR party_type = '1';
UPDATE transaction SET party_type_str = 'BATCH' WHERE party_type = 2 OR party_type = '2';
</sql>
<!-- Drop old integer column -->
<dropColumn tableName="transaction" columnName="party_type"/>
<!-- Rename temp column to original name -->
<renameColumn tableName="transaction" oldColumnName="party_type_str" newColumnName="party_type"/>
</changeSet>
</databaseChangeLog>