bug fixes
This commit is contained in:
@@ -35,7 +35,8 @@ public class SecurityConfig {
|
||||
.requestMatchers("/test/**").permitAll()
|
||||
.requestMatchers("/nflow/**").permitAll()
|
||||
.requestMatchers("/auth/**").permitAll()
|
||||
.requestMatchers("/public/**").permitAll()
|
||||
.requestMatchers("/public/**").permitAll()
|
||||
.requestMatchers("/explorer/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.authenticationProvider(authenticationProvider(userService))
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -65,5 +65,6 @@ public class GroupBatchItem extends BaseEntity {
|
||||
private String billProductName;
|
||||
private String providerImage;
|
||||
private String providerLabel;
|
||||
private String creditAddress;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user