Compare commits
10 Commits
c2f5b63fa6
...
8a9d58fe05
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a9d58fe05 | ||
|
|
0dce7f51dc | ||
|
|
68ed2650e8 | ||
|
|
fadb1e3f8d | ||
|
|
7ded6b2861 | ||
| 24f435a09f | |||
| c87f1e5e23 | |||
| 461dc7e7e4 | |||
| 92276c07d8 | |||
| cc1169f09c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,4 +31,5 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
logs/
|
||||
logs/
|
||||
data/
|
||||
|
||||
20
pom.xml
20
pom.xml
@@ -107,6 +107,11 @@
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-joda</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
@@ -183,7 +188,20 @@
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jobrunr</groupId>
|
||||
<artifactId>jobrunr-spring-boot-3-starter</artifactId>
|
||||
<version>8.6.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI for XLSX generation -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.2.5</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
BIN
reports/batch-report-b4b19592-6d9d-4892-a82e-be7c0810fa6a.xlsx
Normal file
BIN
reports/batch-report-b4b19592-6d9d-4892-a82e-be7c0810fa6a.xlsx
Normal file
Binary file not shown.
BIN
reports/batch-report-bffebe3e-1d84-48be-acd4-5213ef61fb4f.xlsx
Normal file
BIN
reports/batch-report-bffebe3e-1d84-48be-acd4-5213ef61fb4f.xlsx
Normal file
Binary file not shown.
BIN
reports/batch-report-e72b4959-fd95-4227-a9b4-1754c89231e9.xlsx
Normal file
BIN
reports/batch-report-e72b4959-fd95-4227-a9b4-1754c89231e9.xlsx
Normal file
Binary file not shown.
@@ -27,4 +27,19 @@ public class DatabaseConfiguration {
|
||||
.initializeDataSourceBuilder()
|
||||
.build();
|
||||
}
|
||||
|
||||
// JobRunr DataSource Properties (H2)
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "jobrunr.datasource")
|
||||
public DataSourceProperties jobrunrDataSourceProperties() {
|
||||
return new DataSourceProperties();
|
||||
}
|
||||
|
||||
// JobRunr DataSource using H2
|
||||
@Bean
|
||||
public DataSource jobrunrDataSource() {
|
||||
return jobrunrDataSourceProperties()
|
||||
.initializeDataSourceBuilder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package zw.qantra.tm.configs;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Configuration
|
||||
public class ReportsResourceConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${app.reports.storage-path:./reports}")
|
||||
private String reportsStoragePath;
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
Path reportsDir = Path.of(reportsStoragePath).toAbsolutePath();
|
||||
registry.addResourceHandler("/reports/**")
|
||||
.addResourceLocations("file:" + reportsDir + "/");
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,9 @@ public class SecurityConfig {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/configs/seed/**").permitAll()
|
||||
.requestMatchers("/test/**").permitAll()
|
||||
.requestMatchers("/reports/**").permitAll()
|
||||
.requestMatchers("/configs/seed/**").permitAll()
|
||||
.requestMatchers("/test/**").permitAll()
|
||||
.requestMatchers("/nflow/**").permitAll()
|
||||
.requestMatchers("/auth/**").permitAll()
|
||||
.requestMatchers("/public/**").permitAll()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package zw.qantra.tm.domain.controllers;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import zw.qantra.tm.domain.dtos.erp.VelocityAccountDto;
|
||||
import zw.qantra.tm.domain.dtos.erp.VelocityStatementDto;
|
||||
import zw.qantra.tm.domain.services.VelocityAccountService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/accounts")
|
||||
@RequiredArgsConstructor
|
||||
public class AccountController {
|
||||
|
||||
private final VelocityAccountService velocityAccountService;
|
||||
|
||||
@GetMapping("/phone/{currencyPhoneConcat}")
|
||||
@PreAuthorize("hasRole('ACCOUNT_READ')")
|
||||
public ResponseEntity<VelocityAccountDto> getAccountByPhone(
|
||||
@PathVariable String currencyPhoneConcat) {
|
||||
try {
|
||||
VelocityAccountDto account = velocityAccountService.getAccountByPhone(currencyPhoneConcat);
|
||||
return ResponseEntity.ok(account);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/phone/{currencyPhoneConcat}/statement")
|
||||
@PreAuthorize("hasRole('ACCOUNT_READ')")
|
||||
public ResponseEntity<VelocityStatementDto> getStatementByPhone(
|
||||
@PathVariable String currencyPhoneConcat,
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate) {
|
||||
try {
|
||||
VelocityStatementDto statement = velocityAccountService.getStatementByPhone(
|
||||
currencyPhoneConcat, startDate, endDate);
|
||||
return ResponseEntity.ok(statement);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
|
||||
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
|
||||
import zw.qantra.tm.domain.dtos.GroupBatchTriggerRequest;
|
||||
import zw.qantra.tm.domain.models.GroupBatch;
|
||||
import zw.qantra.tm.domain.models.GroupBatchItem;
|
||||
import zw.qantra.tm.domain.services.BatchReportService;
|
||||
import zw.qantra.tm.domain.services.GroupBatchService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/public/group-batches")
|
||||
@RequiredArgsConstructor
|
||||
public class GroupBatchController {
|
||||
private final GroupBatchService groupBatchService;
|
||||
private final BatchReportService batchReportService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity findAll(
|
||||
@And({
|
||||
@Spec(path = "recipientGroupId", defaultVal = "null", spec = Equal.class),
|
||||
@Spec(path = "userId", spec = Equal.class),
|
||||
@Spec(path = "status", spec = Equal.class),
|
||||
@Spec(path = "currency", spec = Equal.class),
|
||||
})Specification<GroupBatch> spec, Pageable pageable){
|
||||
return ResponseEntity.ok(groupBatchService.findAll(spec, pageable));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<GroupBatch> create(@RequestBody GroupBatchCreateRequest request) {
|
||||
return ResponseEntity.ok(groupBatchService.createBatch(request));
|
||||
}
|
||||
|
||||
@PutMapping("/{batchId}")
|
||||
public ResponseEntity<GroupBatch> update(@PathVariable UUID batchId,
|
||||
@RequestBody GroupBatchUpdateRequest request) {
|
||||
request.setId(batchId);
|
||||
return ResponseEntity.ok(groupBatchService.updateBatch(request));
|
||||
}
|
||||
|
||||
@PostMapping("/{batchId}/confirm")
|
||||
public ResponseEntity<GroupBatch> confirm(@PathVariable UUID batchId,
|
||||
@RequestBody GroupBatchTriggerRequest request) {
|
||||
return ResponseEntity.ok(groupBatchService.triggerConfirm(
|
||||
batchId,
|
||||
request.getUserId(),
|
||||
request.getIdempotencyKey()));
|
||||
}
|
||||
|
||||
@PostMapping("/{batchId}/request")
|
||||
public ResponseEntity request(@PathVariable UUID batchId,
|
||||
@RequestBody GroupBatchTriggerRequest request) {
|
||||
Object response = groupBatchService.triggerRequest(
|
||||
batchId,
|
||||
request.getUserId(),
|
||||
request.getAuthType(),
|
||||
request.getIdempotencyKey());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/{batchId}/poll")
|
||||
public ResponseEntity poll(@PathVariable UUID batchId,
|
||||
@RequestBody GroupBatchTriggerRequest request) {
|
||||
return ResponseEntity.ok(groupBatchService.triggerPoll(
|
||||
batchId,
|
||||
request.getUserId(),
|
||||
request.getIdempotencyKey()));
|
||||
}
|
||||
|
||||
@GetMapping("/{batchId}")
|
||||
public ResponseEntity<GroupBatch> getBatch(@PathVariable UUID batchId,
|
||||
@RequestParam String userId) {
|
||||
return ResponseEntity.ok(groupBatchService.getBatch(batchId, userId));
|
||||
}
|
||||
|
||||
@GetMapping("/items")
|
||||
public ResponseEntity getBatchItems(
|
||||
@And({
|
||||
@Spec(path = "groupBatchId", defaultVal = "null", spec = Equal.class),
|
||||
@Spec(path = "recipientId", spec = Equal.class),
|
||||
}) Specification<GroupBatchItem> spec, Pageable pageable) {
|
||||
return ResponseEntity.ok(groupBatchService.getBatchItems(spec, pageable));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{batchId}")
|
||||
public ResponseEntity<Void> deleteBatch(@PathVariable UUID batchId,
|
||||
@RequestParam String userId) {
|
||||
groupBatchService.deleteBatch(batchId, userId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/{batchId}/report")
|
||||
public ResponseEntity<Map<String, Object>> downloadBatchReport(@PathVariable UUID batchId,
|
||||
@RequestParam String userId) {
|
||||
BatchReportService.BatchReportResult result = batchReportService.generateBatchReport(batchId, userId);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"fileUrl", result.fileUrl(),
|
||||
"alreadyExisted", result.alreadyExisted()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ public class ProviderController {
|
||||
@GetMapping
|
||||
public ResponseEntity getProviders(
|
||||
@And({
|
||||
@Spec(path = "category", spec = Equal.class)
|
||||
@Spec(path = "category", spec = Equal.class),
|
||||
@Spec(path = "currency", spec = Equal.class),
|
||||
}) Specification<Provider> spec, Pageable pageable) {
|
||||
return ResponseEntity.ok(providerService.findAll(spec, pageable));
|
||||
}
|
||||
@@ -71,4 +72,10 @@ public class ProviderController {
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity evictCache() {
|
||||
providerService.evictProviderProductsCache();
|
||||
return ResponseEntity.ok("Cache evicted");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,17 @@ public class RecipientController {
|
||||
|
||||
private final RecipientService recipientService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<Recipient>> getAllRecipients() {
|
||||
return ResponseEntity.ok(recipientService.getAllRecipients());
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<List<Recipient>> searchRecipients(
|
||||
@And({
|
||||
@Spec(path = "name", spec = Equal.class),
|
||||
@Spec(path = "email", spec = Equal.class),
|
||||
@Spec(path = "phoneNumber", spec = Equal.class),
|
||||
@Spec(path = "account", spec = Equal.class),
|
||||
@Spec(path = "latestProviderLabel", spec = Equal.class),
|
||||
@Spec(path = "userId", defaultVal = "null", spec = Equal.class)
|
||||
}) Specification<Recipient> specification) {
|
||||
return ResponseEntity.ok(recipientService.searchRecipients(specification));
|
||||
}
|
||||
|
||||
@GetMapping("/{account}/user/{userId}")
|
||||
@@ -57,17 +65,4 @@ public class RecipientController {
|
||||
}
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<List<Recipient>> searchRecipients(
|
||||
@And({
|
||||
@Spec(path = "name", spec = Equal.class),
|
||||
@Spec(path = "email", spec = Equal.class),
|
||||
@Spec(path = "phoneNumber", spec = Equal.class),
|
||||
@Spec(path = "account", spec = Equal.class),
|
||||
@Spec(path = "latestProviderLabel", spec = Equal.class),
|
||||
@Spec(path = "userId", spec = Equal.class)
|
||||
}) Specification<Recipient> specification) {
|
||||
return ResponseEntity.ok(recipientService.searchRecipients(specification));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package zw.qantra.tm.domain.controllers;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
||||
import net.kaczmarzyk.spring.data.jpa.domain.EqualIgnoreCase;
|
||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.And;
|
||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
||||
import org.hibernate.query.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.dtos.GroupMemberRequest;
|
||||
import zw.qantra.tm.domain.dtos.RecipientGroupCreateRequest;
|
||||
import zw.qantra.tm.domain.dtos.RecipientGroupUpdateRequest;
|
||||
import zw.qantra.tm.domain.models.RecipientGroup;
|
||||
import zw.qantra.tm.domain.models.RecipientGroupMember;
|
||||
import zw.qantra.tm.domain.services.RecipientGroupService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/public/recipient-groups")
|
||||
@RequiredArgsConstructor
|
||||
public class RecipientGroupController {
|
||||
private final RecipientGroupService recipientGroupService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity findAll(
|
||||
@And({
|
||||
@Spec(path = "name", spec = EqualIgnoreCase.class),
|
||||
@Spec(path = "userId", defaultVal = "null", spec = Equal.class)
|
||||
})Specification<RecipientGroup> spec, Pageable pageable) {
|
||||
return ResponseEntity.ok(recipientGroupService.findAll(spec, pageable));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<RecipientGroup> create(@RequestBody RecipientGroupCreateRequest request) {
|
||||
RecipientGroup group = RecipientGroup.builder()
|
||||
.name(request.getName())
|
||||
.description(request.getDescription())
|
||||
.userId(request.getUserId())
|
||||
.build();
|
||||
return ResponseEntity.ok(recipientGroupService.create(group, request.getRecipients()));
|
||||
}
|
||||
|
||||
@PutMapping("/update/{groupId}")
|
||||
public ResponseEntity<RecipientGroup> update(@PathVariable UUID groupId,
|
||||
@RequestBody RecipientGroupUpdateRequest request) {
|
||||
return ResponseEntity.ok(recipientGroupService.update(
|
||||
groupId,
|
||||
request.getUserId(),
|
||||
request.getName(),
|
||||
request.getDescription()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete/{groupId}")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID groupId,
|
||||
@RequestParam String userId) {
|
||||
recipientGroupService.delete(groupId, userId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{groupId}/members")
|
||||
public ResponseEntity<List<RecipientGroupMember>> addMembersByIds(@PathVariable UUID groupId,
|
||||
@RequestBody GroupMemberRequest request) {
|
||||
return ResponseEntity.ok(recipientGroupService.addMembersByIds(
|
||||
groupId,
|
||||
request.getUserId(),
|
||||
request.getRecipientIds()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{groupId}/members/{recipientId}")
|
||||
public ResponseEntity<Void> removeMember(@PathVariable UUID groupId,
|
||||
@PathVariable UUID recipientId,
|
||||
@RequestParam String userId) {
|
||||
recipientGroupService.removeMember(groupId, recipientId, userId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@GetMapping("/members")
|
||||
public ResponseEntity members(
|
||||
@And({
|
||||
@Spec(path = "recipientGroupId", spec = Equal.class),
|
||||
}) Specification<RecipientGroupMember> spec, Pageable pageable) {
|
||||
return ResponseEntity.ok(recipientGroupService.findAllMembers(spec, pageable));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import zw.qantra.tm.domain.services.processors.payments.CityWalletRemotePaymentProcessor;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.TransactionWorkflow;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.WorkflowUtils;
|
||||
import zw.qantra.tm.utils.LogUtils;
|
||||
@@ -48,6 +49,7 @@ public class TransactonController {
|
||||
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||
private final WorkflowUtils workflowUtils;
|
||||
private final ObjectStringMapper objectStringMapper;
|
||||
private final CityWalletRemotePaymentProcessor cityWalletRemotePaymentProcessor;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<Transaction>> getAll(
|
||||
@@ -70,7 +72,7 @@ public class TransactonController {
|
||||
@Spec(path = "amount", spec = Equal.class),
|
||||
@Spec(path = "reference", spec = Equal.class),
|
||||
@Spec(path = "totalAmount", spec = Equal.class),
|
||||
@Spec(path = "authType", spec = Equal.class),
|
||||
@Spec(path = "partyType", spec = Equal.class),
|
||||
@Spec(path = "sdkActionId", spec = Equal.class),
|
||||
@Spec(path = "paymentProcessorLabel", spec = Equal.class),
|
||||
})
|
||||
@@ -206,6 +208,16 @@ public class TransactonController {
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/velocity-otp")
|
||||
public ResponseEntity<Transaction> updateOtp(@PathVariable UUID id, @RequestBody Transaction transaction) {
|
||||
Transaction fetchTransaction = transactionService.findById(id);
|
||||
if (fetchTransaction == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
fetchTransaction.setVerificationCode(transaction.getVerificationCode());
|
||||
return ResponseEntity.ok(cityWalletRemotePaymentProcessor.updateVelocityTransactionOtp(fetchTransaction));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Transaction> update(@PathVariable UUID id, @RequestBody Transaction transaction) {
|
||||
if (!transactionService.getTransactionRepository().existsById(id)) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class GroupBatchCreateRequest {
|
||||
private UUID recipientGroupId;
|
||||
private String userId;
|
||||
private String description;
|
||||
private String requestedBy;
|
||||
private String currency;
|
||||
private BigDecimal amount;
|
||||
private String paymentProcessorLabel;
|
||||
private String paymentProcessorName;
|
||||
private String paymentProcessorImage;
|
||||
private Transaction transactionTemplate;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import lombok.Data;
|
||||
import zw.qantra.tm.domain.enums.AuthType;
|
||||
|
||||
@Data
|
||||
public class GroupBatchTriggerRequest {
|
||||
private String userId;
|
||||
private String idempotencyKey;
|
||||
private AuthType authType;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class GroupBatchUpdateRequest {
|
||||
private UUID id;
|
||||
private String userId;
|
||||
private String description;
|
||||
private String requestedBy;
|
||||
private String currency;
|
||||
private BigDecimal amount;
|
||||
private String paymentProcessorLabel;
|
||||
private String paymentProcessorName;
|
||||
private String paymentProcessorImage;
|
||||
private Transaction transactionTemplate;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class GroupMemberRequest {
|
||||
private String userId;
|
||||
private List<UUID> recipientIds;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
import zw.qantra.tm.domain.models.Recipient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class RecipientGroupCreateRequest {
|
||||
private String name;
|
||||
private String description;
|
||||
private String userId;
|
||||
private List<Recipient> recipients;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package zw.qantra.tm.domain.dtos;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class RecipientGroupUpdateRequest {
|
||||
private String name;
|
||||
private String description;
|
||||
private String userId;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ public class RegisterRequest {
|
||||
|
||||
@NotBlank(message = "First name is required")
|
||||
private String firstName;
|
||||
|
||||
@NotBlank(message = "Last name is required")
|
||||
private String lastName;
|
||||
|
||||
@NotBlank(message = "Phone is required")
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package zw.qantra.tm.domain.dtos.erp;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class VelocityAccountDto {
|
||||
private String name;
|
||||
private String description;
|
||||
private String accountNumber;
|
||||
private String alternateAccountNumber;
|
||||
private String parentAccountId;
|
||||
private String type;
|
||||
private String category;
|
||||
private String ledger;
|
||||
private BigDecimal balance;
|
||||
private String partyType;
|
||||
private String party;
|
||||
private Object currency;
|
||||
private String customerStringId;
|
||||
private String companyStringId;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package zw.qantra.tm.domain.dtos.erp;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class VelocityStatementDto {
|
||||
private BigDecimal startBalance;
|
||||
private BigDecimal endBalance;
|
||||
private List<StatementItem> transactions;
|
||||
private String accountName;
|
||||
private String accountNumber;
|
||||
private String startDate;
|
||||
private String endDate;
|
||||
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class StatementItem {
|
||||
private String name;
|
||||
private String narration;
|
||||
private String party;
|
||||
private String company;
|
||||
private String notes;
|
||||
private Timestamp postingDate;
|
||||
private BigDecimal amount;
|
||||
private String account;
|
||||
private String accountName;
|
||||
private String currency;
|
||||
private String reference;
|
||||
private TransactionSide type;
|
||||
private BigDecimal balance;
|
||||
}
|
||||
|
||||
public enum TransactionSide {
|
||||
DEBIT, CREDIT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package zw.qantra.tm.domain.enums;
|
||||
|
||||
public enum GroupBatchItemConfirmStatus {
|
||||
PENDING,
|
||||
CONFIRMED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package zw.qantra.tm.domain.enums;
|
||||
|
||||
public enum GroupBatchItemIntegrationStatus {
|
||||
PENDING,
|
||||
INTEGRATED,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package zw.qantra.tm.domain.enums;
|
||||
|
||||
public enum GroupBatchItemStatus {
|
||||
NEW,
|
||||
CONFIRMED,
|
||||
READY_FOR_INTEGRATION,
|
||||
INTEGRATED,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package zw.qantra.tm.domain.enums;
|
||||
|
||||
public enum GroupBatchStatus {
|
||||
CREATED,
|
||||
CONFIRMING,
|
||||
CONFIRMED_ALL,
|
||||
CONFIRMED_PARTIAL,
|
||||
CONFIRM_FAILED,
|
||||
REQUESTING,
|
||||
REQUESTED,
|
||||
REQUEST_FAILED,
|
||||
POLLING,
|
||||
POLL_SUCCESS,
|
||||
POLL_FAILED,
|
||||
INTEGRATING,
|
||||
COMPLETED,
|
||||
PARTIAL,
|
||||
FAILED
|
||||
}
|
||||
|
||||
7
src/main/java/zw/qantra/tm/domain/enums/PartyType.java
Normal file
7
src/main/java/zw/qantra/tm/domain/enums/PartyType.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package zw.qantra.tm.domain.enums;
|
||||
|
||||
public enum PartyType {
|
||||
INDIVIDUAL,
|
||||
GROUP,
|
||||
BATCH
|
||||
}
|
||||
@@ -13,4 +13,6 @@ public class Currency extends BaseEntity {
|
||||
private String isoCode;
|
||||
private String name;
|
||||
private String displayName;
|
||||
private String bankRate;
|
||||
private String forecastRate;
|
||||
}
|
||||
|
||||
66
src/main/java/zw/qantra/tm/domain/models/GroupBatch.java
Normal file
66
src/main/java/zw/qantra/tm/domain/models/GroupBatch.java
Normal file
@@ -0,0 +1,66 @@
|
||||
package zw.qantra.tm.domain.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchStatus;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@SQLRestriction("deleted = false")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class GroupBatch extends BaseEntity {
|
||||
private UUID recipientGroupId;
|
||||
private String userId;
|
||||
private String description;
|
||||
private String requestedBy;
|
||||
private String currency;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private GroupBatchStatus status;
|
||||
|
||||
private String paymentReference;
|
||||
private UUID paymentTransactionId;
|
||||
private Long workflowId;
|
||||
|
||||
private String confirmIdempotencyKey;
|
||||
private String requestIdempotencyKey;
|
||||
private String pollIdempotencyKey;
|
||||
private String paymentProcessorLabel;
|
||||
private String paymentProcessorName;
|
||||
private String paymentProcessorImage;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String resultFileUrl;
|
||||
|
||||
private LocalDateTime confirmStartedAt;
|
||||
private LocalDateTime requestStartedAt;
|
||||
private LocalDateTime pollStartedAt;
|
||||
private LocalDateTime integrationStartedAt;
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String transactionTemplate;
|
||||
|
||||
private Integer count;
|
||||
private BigDecimal value;
|
||||
private Integer successfulCount;
|
||||
private Integer failedCount;
|
||||
}
|
||||
|
||||
50
src/main/java/zw/qantra/tm/domain/models/GroupBatchItem.java
Normal file
50
src/main/java/zw/qantra/tm/domain/models/GroupBatchItem.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package zw.qantra.tm.domain.models;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemConfirmStatus;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemIntegrationStatus;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemStatus;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@SQLRestriction("deleted = false")
|
||||
public class GroupBatchItem extends BaseEntity {
|
||||
private UUID groupBatchId;
|
||||
private UUID recipientId;
|
||||
private String recipientName;
|
||||
private String recipientPhone;
|
||||
private BigDecimal amount;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private GroupBatchItemStatus status;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private GroupBatchItemConfirmStatus confirmStatus;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private GroupBatchItemIntegrationStatus integrationStatus;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String confirmError;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String integrationError;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "transaction_id")
|
||||
private Transaction transaction;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package zw.qantra.tm.domain.models;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||
|
||||
|
||||
@Entity
|
||||
@@ -19,6 +22,8 @@ public class Provider extends BaseEntity {
|
||||
private String name;
|
||||
private String description;
|
||||
private String externalId;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private CurrencyType currency;
|
||||
private String image;
|
||||
private String category;
|
||||
private String accountFieldName;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package zw.qantra.tm.domain.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
|
||||
@@ -10,6 +13,7 @@ import org.hibernate.annotations.SQLRestriction;
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@SQLRestriction("deleted = false")
|
||||
public class Recipient extends BaseEntity {
|
||||
private String name;
|
||||
|
||||
25
src/main/java/zw/qantra/tm/domain/models/RecipientGroup.java
Normal file
25
src/main/java/zw/qantra/tm/domain/models/RecipientGroup.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package zw.qantra.tm.domain.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.persistence.Entity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@SQLRestriction("deleted = false")
|
||||
public class RecipientGroup extends BaseEntity {
|
||||
private String name;
|
||||
private String description;
|
||||
private String userId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package zw.qantra.tm.domain.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@SQLRestriction("deleted = false")
|
||||
public class RecipientGroupMember extends BaseEntity {
|
||||
private UUID recipientGroupId;
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "recipient_id")
|
||||
private Recipient recipient;
|
||||
private UUID recipientUid;
|
||||
private String account;
|
||||
private String userId;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package zw.qantra.tm.domain.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import org.springframework.data.domain.Persistable;
|
||||
import zw.qantra.tm.domain.enums.AuthType;
|
||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||
import zw.qantra.tm.domain.enums.RequestType;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.enums.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
@@ -20,9 +18,12 @@ import java.util.UUID;
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Transaction extends BaseEntity {
|
||||
private String userId;
|
||||
private String trace;
|
||||
private String region;
|
||||
private PartyType partyType;
|
||||
private BigDecimal amount;
|
||||
private BigDecimal charge;
|
||||
private BigDecimal gatewayCharge;
|
||||
@@ -50,6 +51,7 @@ public class Transaction extends BaseEntity {
|
||||
private String creditAccount;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private CurrencyType creditCurrency;
|
||||
private BigDecimal creditAmount;
|
||||
private String creditName;
|
||||
private String creditCard;
|
||||
private String creditRef;
|
||||
@@ -81,6 +83,7 @@ public class Transaction extends BaseEntity {
|
||||
private String orderId;
|
||||
private String orderTrace;
|
||||
private String orderTransactionTrace;
|
||||
private String orderTransactionId;
|
||||
|
||||
private String erpSalesRef;
|
||||
private String erpJournalRef;
|
||||
@@ -107,6 +110,8 @@ public class Transaction extends BaseEntity {
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String targetUrl;
|
||||
private String sdkActionId;
|
||||
private String verificationCode;
|
||||
private Integer retries;
|
||||
|
||||
@Transient
|
||||
private Object additionalData;
|
||||
|
||||
@@ -15,4 +15,5 @@ public interface ChargeRepository extends JpaRepository<Charge, UUID> {
|
||||
List<Charge> findAllByCurrency(CurrencyType currency);
|
||||
boolean existsByChargeLabelAndCurrency(ChargeLabelEnum chargeLabel, CurrencyType currency);
|
||||
boolean existsByChargeLabelAndCurrencyAndPercentageRate(ChargeLabelEnum chargeLabel, CurrencyType currency, BigDecimal percentageRate);
|
||||
boolean existsByChargeLabelAndCurrencyAndDescription(ChargeLabelEnum chargeLabel, CurrencyType currency, String description);
|
||||
}
|
||||
@@ -4,8 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import zw.qantra.tm.domain.models.Currency;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface CurrencyRepository extends JpaRepository<Currency, UUID> {
|
||||
}
|
||||
Optional<Currency> findByIsoCode(String isoCode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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.GroupBatchItemConfirmStatus;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemIntegrationStatus;
|
||||
import zw.qantra.tm.domain.models.GroupBatchItem;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface GroupBatchItemRepository extends JpaRepository<GroupBatchItem, UUID>, JpaSpecificationExecutor<GroupBatchItem> {
|
||||
List<GroupBatchItem> findAllByGroupBatchId(UUID groupBatchId);
|
||||
List<GroupBatchItem> findAllByGroupBatchIdAndConfirmStatus(UUID groupBatchId, GroupBatchItemConfirmStatus status);
|
||||
long countByGroupBatchIdAndConfirmStatus(UUID groupBatchId, GroupBatchItemConfirmStatus status);
|
||||
long countByGroupBatchIdAndIntegrationStatus(UUID groupBatchId, GroupBatchItemIntegrationStatus status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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.models.GroupBatch;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface GroupBatchRepository extends JpaRepository<GroupBatch, UUID>, JpaSpecificationExecutor<GroupBatch> {
|
||||
Optional<GroupBatch> findByIdAndUserId(UUID id, String userId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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.models.RecipientGroupMember;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface RecipientGroupMemberRepository extends JpaRepository<RecipientGroupMember, UUID>, JpaSpecificationExecutor<RecipientGroupMember> {
|
||||
List<RecipientGroupMember> findAllByRecipientGroupId(UUID recipientGroupId);
|
||||
boolean existsByRecipientGroupIdAndRecipientId(UUID recipientGroupId, UUID recipientId);
|
||||
void deleteAllByRecipientGroupId(UUID recipientGroupId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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.models.RecipientGroup;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface RecipientGroupRepository extends JpaRepository<RecipientGroup, UUID>, JpaSpecificationExecutor<RecipientGroup> {
|
||||
List<RecipientGroup> findAllByUserId(String userId);
|
||||
Optional<RecipientGroup> findByIdAndUserId(UUID id, String userId);
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@ public interface TransactionRepository extends JpaRepository<Transaction, UUID>,
|
||||
List<Transaction> findAllByUserId(String uid);
|
||||
List<Transaction> findAllByConfirmationStatusAndPaymentStatusAndIntegrationStatus(
|
||||
Status s1, Status s2, Status s3);
|
||||
List<Transaction> findAllByConfirmationStatusAndIntegrationStatus(Status s1, Status s2);
|
||||
List<Transaction> findAllByPaymentStatusAndPollingStatus(Status s1, Status s2);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
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.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemStatus;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchStatus;
|
||||
import zw.qantra.tm.domain.models.GroupBatch;
|
||||
import zw.qantra.tm.domain.models.GroupBatchItem;
|
||||
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
|
||||
import zw.qantra.tm.domain.repositories.GroupBatchRepository;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BatchReportService {
|
||||
|
||||
private static final Set<GroupBatchStatus> REPORTABLE_STATUSES = Set.of(
|
||||
GroupBatchStatus.COMPLETED,
|
||||
GroupBatchStatus.PARTIAL,
|
||||
GroupBatchStatus.FAILED
|
||||
);
|
||||
|
||||
private static final String REPORT_FILENAME_PREFIX = "batch-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;
|
||||
|
||||
@Value("${app.base-url:http://localhost:6950}")
|
||||
private String baseUrl;
|
||||
|
||||
private final GroupBatchRepository groupBatchRepository;
|
||||
private final GroupBatchItemRepository groupBatchItemRepository;
|
||||
|
||||
@Transactional
|
||||
public BatchReportResult generateBatchReport(UUID batchId, String userId) {
|
||||
GroupBatch batch = groupBatchRepository.findByIdAndUserId(batchId, userId)
|
||||
.orElseThrow(() -> new ApiException("Group batch not found"));
|
||||
|
||||
if (!REPORTABLE_STATUSES.contains(batch.getStatus())) {
|
||||
throw new ApiException(
|
||||
"Batch report can only be generated for completed batches. Current status: " + batch.getStatus()
|
||||
);
|
||||
}
|
||||
|
||||
// If report already exists, return the existing file URL
|
||||
if (batch.getResultFileUrl() != null && !batch.getResultFileUrl().isEmpty()) {
|
||||
return new BatchReportResult(batch.getResultFileUrl(), true);
|
||||
}
|
||||
|
||||
List<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batch.getId());
|
||||
|
||||
byte[] fileBytes;
|
||||
try (Workbook workbook = new XSSFWorkbook()) {
|
||||
Sheet sheet = workbook.createSheet("Batch Report");
|
||||
|
||||
// Styles
|
||||
CellStyle headerStyle = workbook.createCellStyle();
|
||||
headerStyle.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
|
||||
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
headerStyle.setFont(createHeaderFont(workbook));
|
||||
headerStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
|
||||
CellStyle titleStyle = workbook.createCellStyle();
|
||||
titleStyle.setFont(createDataFont(workbook));
|
||||
titleStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||
titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||
|
||||
CellStyle dataStyle = workbook.createCellStyle();
|
||||
dataStyle.setFont(createDataFont(workbook));
|
||||
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 statusSuccessStyle = workbook.createCellStyle();
|
||||
statusSuccessStyle.cloneStyleFrom(dataStyle);
|
||||
statusSuccessStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
|
||||
statusSuccessStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
|
||||
CellStyle statusFailedStyle = workbook.createCellStyle();
|
||||
statusFailedStyle.cloneStyleFrom(dataStyle);
|
||||
statusFailedStyle.setFillForegroundColor(IndexedColors.ROSE.getIndex());
|
||||
statusFailedStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
|
||||
CellStyle statusPendingStyle = workbook.createCellStyle();
|
||||
statusPendingStyle.cloneStyleFrom(dataStyle);
|
||||
statusPendingStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());
|
||||
statusPendingStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||
|
||||
int rowNum = 0;
|
||||
|
||||
// Title row
|
||||
Row titleRow = sheet.createRow(rowNum++);
|
||||
Cell titleCell = titleRow.createCell(0);
|
||||
titleCell.setCellValue("Batch Report - " + (batch.getDescription() != null ? batch.getDescription() : batch.getId().toString()));
|
||||
titleCell.setCellStyle(titleStyle);
|
||||
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 9));
|
||||
titleRow.setHeightInPoints(30);
|
||||
rowNum++;
|
||||
|
||||
// Batch summary
|
||||
writeSummaryRow(sheet, rowNum++, "Status:", batch.getStatus().name(), dataStyle);
|
||||
writeSummaryRow(sheet, rowNum++, "Total Items:", String.valueOf(batch.getCount() != null ? batch.getCount() : 0), dataStyle);
|
||||
writeSummaryRow(sheet, rowNum++, "Successful:", String.valueOf(batch.getSuccessfulCount() != null ? batch.getSuccessfulCount() : 0), dataStyle);
|
||||
writeSummaryRow(sheet, rowNum++, "Failed:", String.valueOf(batch.getFailedCount() != null ? batch.getFailedCount() : 0), dataStyle);
|
||||
|
||||
if (batch.getCompletedAt() != null) {
|
||||
writeSummaryRow(sheet, rowNum++, "Completed At:",
|
||||
batch.getCompletedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), dataStyle);
|
||||
}
|
||||
|
||||
writeSummaryRow(sheet, rowNum++, "Currency:", batch.getCurrency() != null ? batch.getCurrency() : "N/A", dataStyle);
|
||||
|
||||
if (batch.getValue() != null) {
|
||||
writeSummaryRow(sheet, rowNum++, "Total Value:", batch.getValue().toString(), dataStyle);
|
||||
}
|
||||
|
||||
// Blank row
|
||||
rowNum++;
|
||||
|
||||
// Header row
|
||||
String[] headers = {
|
||||
"#", "Recipient Name", "Phone", "Amount",
|
||||
"Status", "Confirm Status", "Integration Status",
|
||||
"Transaction Ref", "Confirm Error", "Integration Error"
|
||||
};
|
||||
|
||||
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 (GroupBatchItem item : items) {
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
|
||||
Cell numCell = row.createCell(0);
|
||||
numCell.setCellValue(index++);
|
||||
numCell.setCellStyle(dataStyle);
|
||||
|
||||
Cell nameCell = row.createCell(1);
|
||||
nameCell.setCellValue(item.getRecipientName() != null ? item.getRecipientName() : "");
|
||||
nameCell.setCellStyle(dataStyle);
|
||||
|
||||
Cell phoneCell = row.createCell(2);
|
||||
phoneCell.setCellValue(item.getRecipientPhone() != null ? item.getRecipientPhone() : "");
|
||||
phoneCell.setCellStyle(dataStyle);
|
||||
|
||||
Cell amountCell = row.createCell(3);
|
||||
amountCell.setCellValue(item.getAmount() != null ? item.getAmount().doubleValue() : 0.0);
|
||||
amountCell.setCellStyle(currencyStyle);
|
||||
|
||||
Cell statusCell = row.createCell(4);
|
||||
statusCell.setCellValue(item.getStatus() != null ? item.getStatus().name() : "");
|
||||
if (item.getStatus() == GroupBatchItemStatus.INTEGRATED) {
|
||||
statusCell.setCellStyle(statusSuccessStyle);
|
||||
} else if (item.getStatus() == GroupBatchItemStatus.FAILED) {
|
||||
statusCell.setCellStyle(statusFailedStyle);
|
||||
} else {
|
||||
statusCell.setCellStyle(statusPendingStyle);
|
||||
}
|
||||
|
||||
Cell confirmStatusCell = row.createCell(5);
|
||||
confirmStatusCell.setCellValue(item.getConfirmStatus() != null ? item.getConfirmStatus().name() : "");
|
||||
confirmStatusCell.setCellStyle(dataStyle);
|
||||
|
||||
Cell integrationStatusCell = row.createCell(6);
|
||||
integrationStatusCell.setCellValue(item.getIntegrationStatus() != null ? item.getIntegrationStatus().name() : "");
|
||||
integrationStatusCell.setCellStyle(dataStyle);
|
||||
|
||||
Cell refCell = row.createCell(7);
|
||||
if (item.getTransaction() != null && item.getTransaction().getReference() != null) {
|
||||
refCell.setCellValue(item.getTransaction().getReference());
|
||||
} else {
|
||||
refCell.setCellValue("");
|
||||
}
|
||||
refCell.setCellStyle(dataStyle);
|
||||
|
||||
Cell confirmErrCell = row.createCell(8);
|
||||
confirmErrCell.setCellValue(item.getConfirmError() != null ? item.getConfirmError() : "");
|
||||
confirmErrCell.setCellStyle(dataStyle);
|
||||
|
||||
Cell integrationErrCell = row.createCell(9);
|
||||
integrationErrCell.setCellValue(item.getIntegrationError() != null ? item.getIntegrationError() : "");
|
||||
integrationErrCell.setCellStyle(dataStyle);
|
||||
}
|
||||
|
||||
// Auto-size columns
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
sheet.autoSizeColumn(i);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
workbook.write(outputStream);
|
||||
fileBytes = outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to generate batch report", e);
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
String filename = REPORT_FILENAME_PREFIX + batchId + REPORT_FILENAME_SUFFIX;
|
||||
Path reportsDir = Path.of(reportsStoragePath).toAbsolutePath();
|
||||
Path filePath = reportsDir.resolve(filename);
|
||||
|
||||
try {
|
||||
Files.createDirectories(reportsDir);
|
||||
try (OutputStream os = Files.newOutputStream(filePath)) {
|
||||
os.write(fileBytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to persist batch report file", e);
|
||||
}
|
||||
|
||||
// Persist the URL
|
||||
String fileUrl = baseUrl + "/api/reports/" + filename;
|
||||
batch.setResultFileUrl(fileUrl);
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
return new BatchReportResult(fileUrl, false, fileBytes);
|
||||
}
|
||||
|
||||
private void writeSummaryRow(Sheet sheet, int rowNum, String label, String value, CellStyle style) {
|
||||
Row row = sheet.getRow(rowNum);
|
||||
if (row == null) {
|
||||
row = sheet.createRow(rowNum);
|
||||
}
|
||||
Cell labelCell = row.createCell(0);
|
||||
labelCell.setCellValue(label);
|
||||
labelCell.setCellStyle(style);
|
||||
|
||||
Cell valueCell = row.createCell(1);
|
||||
valueCell.setCellValue(value);
|
||||
valueCell.setCellStyle(style);
|
||||
}
|
||||
|
||||
private Font createHeaderFont(Workbook workbook) {
|
||||
Font font = workbook.createFont();
|
||||
font.setBold(true);
|
||||
font.setFontHeightInPoints(HEADER_FONT_SIZE);
|
||||
font.setColor(IndexedColors.WHITE.getIndex());
|
||||
return font;
|
||||
}
|
||||
|
||||
private Font createDataFont(Workbook workbook) {
|
||||
Font font = workbook.createFont();
|
||||
font.setFontHeightInPoints(DATA_FONT_SIZE);
|
||||
font.setColor(IndexedColors.BLACK.getIndex());
|
||||
return font;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a batch report generation request.
|
||||
*/
|
||||
public record BatchReportResult(String fileUrl, boolean alreadyExisted, byte... fileBytes) {
|
||||
|
||||
public BatchReportResult(String fileUrl, boolean alreadyExisted) {
|
||||
this(fileUrl, alreadyExisted, (byte[]) null);
|
||||
}
|
||||
|
||||
public boolean isNewlyGenerated() {
|
||||
return fileBytes != null && fileBytes.length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package zw.qantra.tm.domain.services;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.domain.models.Currency;
|
||||
import zw.qantra.tm.domain.repositories.CurrencyRepository;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -12,4 +14,9 @@ public class CurrencyService {
|
||||
public CurrencyRepository getCurrencyRepository() {
|
||||
return currencyRepository;
|
||||
}
|
||||
}
|
||||
|
||||
public Currency findCurrencyByIsoCode(String isoCode) {
|
||||
return currencyRepository.findByIsoCode(isoCode)
|
||||
.orElseThrow(() -> new ApiException("Currency not found for ISO code: " + isoCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
package zw.qantra.tm.domain.services;
|
||||
|
||||
import io.nflow.engine.service.WorkflowInstanceService;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Instant;
|
||||
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 org.springframework.transaction.annotation.Transactional;
|
||||
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
|
||||
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
|
||||
import zw.qantra.tm.domain.dtos.StateResponse;
|
||||
import zw.qantra.tm.domain.enums.AuthType;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemConfirmStatus;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemIntegrationStatus;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchItemStatus;
|
||||
import zw.qantra.tm.domain.enums.GroupBatchStatus;
|
||||
import zw.qantra.tm.domain.models.*;
|
||||
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
|
||||
import zw.qantra.tm.domain.repositories.GroupBatchRepository;
|
||||
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.BatchWorkflow;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.WorkflowUtils;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GroupBatchService {
|
||||
private static final String BATCH_WORKFLOW_TYPE = "batchWorkflow";
|
||||
|
||||
private static final Set<GroupBatchStatus> UPDATABLE_STATUSES = Set.of(
|
||||
GroupBatchStatus.CREATED
|
||||
);
|
||||
|
||||
private static final Set<GroupBatchStatus> DELETABLE_STATUSES = Set.of(
|
||||
GroupBatchStatus.CREATED,
|
||||
GroupBatchStatus.CONFIRMING,
|
||||
GroupBatchStatus.CONFIRMED_ALL,
|
||||
GroupBatchStatus.CONFIRMED_PARTIAL,
|
||||
GroupBatchStatus.CONFIRM_FAILED
|
||||
);
|
||||
|
||||
private final RecipientGroupService recipientGroupService;
|
||||
private final RecipientRepository recipientRepository;
|
||||
private final GroupBatchRepository groupBatchRepository;
|
||||
private final GroupBatchItemRepository groupBatchItemRepository;
|
||||
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||
private final WorkflowInstanceService workflowInstanceService;
|
||||
private final WorkflowUtils workflowUtils;
|
||||
private final TransactionService transactionService;
|
||||
|
||||
@Transactional
|
||||
public GroupBatch createBatch(GroupBatchCreateRequest request) {
|
||||
RecipientGroup group = recipientGroupService.get(
|
||||
request.getRecipientGroupId(), request.getUserId());
|
||||
|
||||
List<RecipientGroupMember> members = recipientGroupService.members(
|
||||
group.getId(), request.getUserId());
|
||||
|
||||
if (members.isEmpty()) {
|
||||
throw new ApiException("Recipient group has no members");
|
||||
}
|
||||
if (request.getTransactionTemplate() == null) {
|
||||
throw new ApiException("transactionTemplate is required");
|
||||
}
|
||||
|
||||
GroupBatch batch = GroupBatch.builder()
|
||||
.recipientGroupId(group.getId())
|
||||
.userId(request.getUserId())
|
||||
.description(request.getDescription())
|
||||
.requestedBy(request.getRequestedBy())
|
||||
.currency(request.getCurrency())
|
||||
.status(GroupBatchStatus.CREATED)
|
||||
.paymentProcessorLabel(request.getPaymentProcessorLabel())
|
||||
.paymentProcessorName(request.getPaymentProcessorName())
|
||||
.paymentProcessorImage(request.getPaymentProcessorImage())
|
||||
.transactionTemplate(Utils.toJson(request.getTransactionTemplate()))
|
||||
.build();
|
||||
|
||||
GroupBatch savedBatch = groupBatchRepository.save(batch);
|
||||
|
||||
BigDecimal defaultAmount = request.getAmount() == null
|
||||
? request.getTransactionTemplate().getAmount()
|
||||
: request.getAmount();
|
||||
|
||||
BigDecimal totalValue = BigDecimal.ZERO;
|
||||
int itemCount = 0;
|
||||
|
||||
for (RecipientGroupMember member : members) {
|
||||
Recipient recipient = recipientRepository.findById(member.getRecipientUid())
|
||||
.orElseThrow(() -> new ApiException("Recipient not found: " + member.getRecipientUid()));
|
||||
|
||||
GroupBatchItem item = GroupBatchItem.builder()
|
||||
.groupBatchId(savedBatch.getId())
|
||||
.recipientId(recipient.getId())
|
||||
.recipientName(recipient.getName())
|
||||
.recipientPhone(recipient.getPhoneNumber())
|
||||
.amount(defaultAmount)
|
||||
.status(GroupBatchItemStatus.NEW)
|
||||
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
|
||||
.integrationStatus(GroupBatchItemIntegrationStatus.PENDING)
|
||||
.build();
|
||||
|
||||
totalValue = totalValue.add(defaultAmount);
|
||||
itemCount++;
|
||||
groupBatchItemRepository.save(item);
|
||||
}
|
||||
|
||||
savedBatch.setCount(itemCount);
|
||||
savedBatch.setValue(totalValue);
|
||||
savedBatch.setSuccessfulCount(0);
|
||||
savedBatch.setFailedCount(0);
|
||||
return groupBatchRepository.save(savedBatch);
|
||||
}
|
||||
|
||||
public GroupBatch triggerConfirm(UUID batchId, String userId, String idempotencyKey) {
|
||||
GroupBatch batch = getBatch(batchId, userId);
|
||||
|
||||
if (batch.getConfirmIdempotencyKey() != null && batch.getConfirmIdempotencyKey().equals(idempotencyKey)) {
|
||||
return batch;
|
||||
}
|
||||
|
||||
batch.setConfirmIdempotencyKey(idempotencyKey);
|
||||
batch.setStatus(GroupBatchStatus.CONFIRMING);
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
if (batch.getWorkflowId() == null) {
|
||||
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||
.setType(BatchWorkflow.TYPE)
|
||||
.setExternalId(batch.getUserId() + "-batch-" + new Instant().getMillis())
|
||||
.setBusinessKey(batch.getId().toString())
|
||||
.putStateVariable("batchId", Utils.toJson(batch.getId().toString()))
|
||||
.setNextActivation(DateTime.now())
|
||||
.build();
|
||||
|
||||
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
||||
batch.setWorkflowId(workflowId);
|
||||
return groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||
.setId(batch.getWorkflowId())
|
||||
.setState("confirm")
|
||||
.setNextActivation(DateTime.now())
|
||||
.setStateVariables(new HashMap<>() {{
|
||||
put("batchId", Utils.toJson(batch.getId().toString()));
|
||||
}})
|
||||
.setStateText("Resumed from network trigger")
|
||||
.build();
|
||||
|
||||
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
|
||||
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
|
||||
.setExecutionEnd(DateTime.now())
|
||||
.build();
|
||||
|
||||
workflowInstanceService.updateWorkflowInstance(update, action);
|
||||
return batch;
|
||||
}
|
||||
|
||||
public Object triggerRequest(UUID batchId, String userId,
|
||||
AuthType authType, String idempotencyKey) {
|
||||
GroupBatch batch = getBatch(batchId, userId);
|
||||
|
||||
if (batch.getRequestIdempotencyKey() != null && batch.getRequestIdempotencyKey().equals(idempotencyKey)) {
|
||||
return batch;
|
||||
}
|
||||
|
||||
batch.setRequestIdempotencyKey(idempotencyKey);
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
if (batch.getWorkflowId() == null) {
|
||||
throw new ApiException("Batch workflow has not been started. Trigger confirm first.");
|
||||
}
|
||||
|
||||
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||
.setId(batch.getWorkflowId())
|
||||
.setState("request")
|
||||
.setNextActivation(DateTime.now())
|
||||
.setStateVariables(new HashMap<>() {{
|
||||
put("batchId", Utils.toJson(batch.getId().toString()));
|
||||
put("authType", Utils.toJson((authType == null ? AuthType.REMOTE : authType).name()));
|
||||
}})
|
||||
.setStateText("Resumed from network trigger")
|
||||
.build();
|
||||
|
||||
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
|
||||
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
|
||||
.setExecutionEnd(DateTime.now())
|
||||
.build();
|
||||
|
||||
workflowInstanceService.updateWorkflowInstance(update, action);
|
||||
|
||||
try {
|
||||
CompletableFuture<Object> future = workflowUtils
|
||||
.waitForState(batch.getWorkflowId(), new String[]{"requestSuccess", "done", "failed"},
|
||||
15000, 50);
|
||||
StateResponse response = (StateResponse) future.get();
|
||||
String id = response.getBody().toString(); // this returns Batch Transaction ID
|
||||
String cleaned = id.replaceAll("^\"|\"$", "");
|
||||
return transactionService.findById(UUID.fromString(cleaned));
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Object triggerPoll(UUID batchId, String userId, String idempotencyKey) {
|
||||
GroupBatch batch = getBatch(batchId, userId);
|
||||
|
||||
if (batch.getPollIdempotencyKey() != null && batch.getPollIdempotencyKey().equals(idempotencyKey)) {
|
||||
return batch;
|
||||
}
|
||||
|
||||
batch.setPollIdempotencyKey(idempotencyKey);
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
if (batch.getWorkflowId() == null) {
|
||||
throw new ApiException("Batch workflow has not been started. Trigger confirm first.");
|
||||
}
|
||||
|
||||
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||
.setId(batch.getWorkflowId())
|
||||
.setState("poll")
|
||||
.setNextActivation(DateTime.now())
|
||||
.setStateVariables(new HashMap<>() {{
|
||||
put("batchId", Utils.toJson(batch.getId().toString()));
|
||||
}})
|
||||
.setStateText("Resumed from network trigger")
|
||||
.build();
|
||||
|
||||
WorkflowInstanceAction action = new WorkflowInstanceAction.Builder(update)
|
||||
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
|
||||
.setExecutionEnd(DateTime.now())
|
||||
.build();
|
||||
|
||||
workflowInstanceService.updateWorkflowInstance(update, action);
|
||||
|
||||
try {
|
||||
CompletableFuture<Object> future = workflowUtils
|
||||
.waitForState(batch.getWorkflowId(), new String[]{"done", "failed"},
|
||||
15000, 50);
|
||||
StateResponse response = (StateResponse) future.get();
|
||||
String id = response.getBody().toString(); // this returns Batch Transaction ID
|
||||
String cleaned = id.replaceAll("^\"|\"$", "");
|
||||
return transactionService.findById(UUID.fromString(cleaned));
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public GroupBatch getBatch(UUID batchId, String userId) {
|
||||
return groupBatchRepository.findByIdAndUserId(batchId, userId)
|
||||
.orElseThrow(() -> new ApiException("Group batch not found"));
|
||||
}
|
||||
|
||||
public Page<GroupBatchItem> getBatchItems(Specification<GroupBatchItem> spec, Pageable pageable) {
|
||||
return groupBatchItemRepository.findAll(spec, pageable);
|
||||
}
|
||||
|
||||
public Page<GroupBatch> findAll(Specification<GroupBatch> spec, Pageable pageable) {
|
||||
return groupBatchRepository.findAll(spec, pageable);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GroupBatch updateBatch(GroupBatchUpdateRequest request) {
|
||||
GroupBatch batch = getBatch(request.getId(), request.getUserId());
|
||||
|
||||
if (!UPDATABLE_STATUSES.contains(batch.getStatus())) {
|
||||
throw new ApiException(
|
||||
"Batch cannot be updated at current status: " + batch.getStatus()
|
||||
+ ". Only batches in CREATED status can be updated."
|
||||
);
|
||||
}
|
||||
|
||||
if (request.getDescription() != null) {
|
||||
batch.setDescription(request.getDescription());
|
||||
}
|
||||
if (request.getRequestedBy() != null) {
|
||||
batch.setRequestedBy(request.getRequestedBy());
|
||||
}
|
||||
if (request.getCurrency() != null) {
|
||||
batch.setCurrency(request.getCurrency());
|
||||
}
|
||||
if (request.getPaymentProcessorLabel() != null) {
|
||||
batch.setPaymentProcessorLabel(request.getPaymentProcessorLabel());
|
||||
}
|
||||
if (request.getPaymentProcessorName() != null) {
|
||||
batch.setPaymentProcessorName(request.getPaymentProcessorName());
|
||||
}
|
||||
if (request.getPaymentProcessorImage() != null) {
|
||||
batch.setPaymentProcessorImage(request.getPaymentProcessorImage());
|
||||
}
|
||||
if (request.getTransactionTemplate() != null) {
|
||||
batch.setTransactionTemplate(Utils.toJson(request.getTransactionTemplate()));
|
||||
}
|
||||
if (request.getAmount() != null) {
|
||||
BigDecimal newAmount = request.getAmount();
|
||||
BigDecimal newTotalValue = BigDecimal.ZERO;
|
||||
int newCount = 0;
|
||||
|
||||
List<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batch.getId());
|
||||
for (GroupBatchItem item : items) {
|
||||
item.setAmount(newAmount);
|
||||
groupBatchItemRepository.save(item);
|
||||
newTotalValue = newTotalValue.add(newAmount);
|
||||
newCount++;
|
||||
}
|
||||
|
||||
batch.setValue(newTotalValue);
|
||||
batch.setCount(newCount);
|
||||
}
|
||||
|
||||
return groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteBatch(UUID batchId, String userId) {
|
||||
GroupBatch batch = getBatch(batchId, userId);
|
||||
|
||||
if (!DELETABLE_STATUSES.contains(batch.getStatus())) {
|
||||
throw new ApiException(
|
||||
"Batch cannot be deleted at current status: " + batch.getStatus()
|
||||
+ ". Only batches up to the confirmed stage can be deleted."
|
||||
);
|
||||
}
|
||||
|
||||
batch.setDeleted(true);
|
||||
groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package zw.qantra.tm.domain.services;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import zw.qantra.tm.domain.enums.*;
|
||||
import zw.qantra.tm.domain.models.*;
|
||||
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
|
||||
import zw.qantra.tm.domain.repositories.GroupBatchRepository;
|
||||
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.handlers.*;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GroupBatchWorkflowService {
|
||||
private final GroupBatchRepository groupBatchRepository;
|
||||
private final GroupBatchItemRepository groupBatchItemRepository;
|
||||
private final RecipientRepository recipientRepository;
|
||||
private final TransactionService transactionService;
|
||||
private final ConfirmHandler confirmHandler;
|
||||
private final GatewayPaymentHandler gatewayPaymentHandler;
|
||||
private final PollStatusHandler pollStatusHandler;
|
||||
private final IntegrationHandler integrationHandler;
|
||||
private final CalculateChargesHandler calculateChargesHandler;
|
||||
private final CleanupHandler cleanUpHandler;
|
||||
|
||||
@Transactional
|
||||
public GroupBatch processConfirm(UUID batchId) {
|
||||
GroupBatch batch = getBatchById(batchId);
|
||||
List<GroupBatchStatus> acceptableStatuses = List.of(
|
||||
GroupBatchStatus.CREATED,
|
||||
GroupBatchStatus.CONFIRMING,
|
||||
GroupBatchStatus.CONFIRM_FAILED);
|
||||
|
||||
if (!acceptableStatuses.contains(batch.getStatus())) {
|
||||
return batch;
|
||||
}
|
||||
|
||||
batch.setStatus(GroupBatchStatus.CONFIRMING);
|
||||
batch.setConfirmStartedAt(LocalDateTime.now());
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
Transaction template = Utils.fromJson(batch.getTransactionTemplate(), Transaction.class);
|
||||
if (template == null) {
|
||||
throw new ApiException("Batch transaction template is missing");
|
||||
}
|
||||
|
||||
List<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
for (GroupBatchItem item : items) {
|
||||
futures.add(CompletableFuture.runAsync(() -> confirmItem(batch, item, template)));
|
||||
}
|
||||
futures.forEach(CompletableFuture::join);
|
||||
|
||||
long confirmSuccessCount = groupBatchItemRepository.countByGroupBatchIdAndConfirmStatus(
|
||||
batchId, GroupBatchItemConfirmStatus.CONFIRMED);
|
||||
|
||||
if (confirmSuccessCount == 0) {
|
||||
batch.setStatus(GroupBatchStatus.CONFIRM_FAILED);
|
||||
} else if (confirmSuccessCount == items.size()) {
|
||||
batch.setStatus(GroupBatchStatus.CONFIRMED_ALL);
|
||||
} else {
|
||||
batch.setStatus(GroupBatchStatus.CONFIRMED_PARTIAL);
|
||||
}
|
||||
|
||||
recalculateStats(batch);
|
||||
|
||||
return groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GroupBatch processRequest(UUID batchId, AuthType authType) throws Exception {
|
||||
GroupBatch batch = getBatchById(batchId);
|
||||
List<GroupBatchStatus> acceptableStatuses = List.of(
|
||||
GroupBatchStatus.CONFIRMED_ALL,
|
||||
GroupBatchStatus.CONFIRMED_PARTIAL,
|
||||
GroupBatchStatus.REQUEST_FAILED);
|
||||
if (!acceptableStatuses.contains(batch.getStatus())) {
|
||||
throw new ApiException("Batch request step requires a successful confirm step");
|
||||
}
|
||||
|
||||
batch.setStatus(GroupBatchStatus.REQUESTING);
|
||||
batch.setRequestStartedAt(LocalDateTime.now());
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
List<GroupBatchItem> confirmedItems = groupBatchItemRepository.findAllByGroupBatchIdAndConfirmStatus(
|
||||
batchId, GroupBatchItemConfirmStatus.CONFIRMED);
|
||||
if (confirmedItems.isEmpty()) {
|
||||
throw new ApiException("No confirmed batch items available for payment request");
|
||||
}
|
||||
|
||||
Transaction template = Utils.fromJson(batch.getTransactionTemplate(), Transaction.class);
|
||||
if (template == null) {
|
||||
throw new ApiException("Batch transaction template is missing");
|
||||
}
|
||||
|
||||
BigDecimal total = confirmedItems.stream()
|
||||
.map(item -> item.getAmount() == null ? BigDecimal.ZERO : item.getAmount())
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
Transaction transaction = Utils.deepCopy(template, Transaction.class);
|
||||
transaction.setId(null);
|
||||
transaction.setPartyType(PartyType.GROUP);
|
||||
transaction.setType(RequestType.REQUEST);
|
||||
transaction.setAuthType(authType == null ? AuthType.REMOTE : authType);
|
||||
transaction.setAmount(total);
|
||||
transaction.setTotalAmount(total);
|
||||
transaction.setReference("batch-" + batch.getId());
|
||||
transaction.setUserId(batch.getUserId());
|
||||
transaction.setPaymentProcessorLabel(batch.getPaymentProcessorLabel());
|
||||
transaction.setPaymentProcessorName(batch.getPaymentProcessorName());
|
||||
transaction.setPaymentProcessorImage(batch.getPaymentProcessorImage());
|
||||
|
||||
transaction = calculateChargesHandler.process(transaction);
|
||||
transaction = (Transaction) cleanUpHandler.process(transaction);
|
||||
Transaction result = (Transaction) gatewayPaymentHandler.process(transaction);
|
||||
|
||||
batch.setPaymentTransactionId(result.getId());
|
||||
batch.setPaymentReference(result.getPaymentProcessorRef());
|
||||
batch.setStatus(result.getPaymentStatus() == Status.FAILED
|
||||
? GroupBatchStatus.REQUEST_FAILED
|
||||
: GroupBatchStatus.REQUESTED);
|
||||
|
||||
return groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
public GroupBatch processPoll(UUID batchId) throws Exception {
|
||||
GroupBatch batch = getBatchById(batchId);
|
||||
if (batch.getStatus() != GroupBatchStatus.REQUESTED && batch.getStatus() != GroupBatchStatus.POLL_FAILED) {
|
||||
throw new ApiException("Batch poll step requires a successful request step");
|
||||
}
|
||||
if (batch.getPaymentTransactionId() == null) {
|
||||
throw new ApiException("Batch payment transaction was not initialized");
|
||||
}
|
||||
|
||||
batch.setStatus(GroupBatchStatus.POLLING);
|
||||
batch.setPollStartedAt(LocalDateTime.now());
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
Transaction paymentTransaction = transactionService.findById(batch.getPaymentTransactionId());
|
||||
|
||||
try {
|
||||
paymentTransaction = (Transaction) pollStatusHandler.process(paymentTransaction);
|
||||
} catch (Exception e) {
|
||||
batch.setStatus(GroupBatchStatus.POLL_FAILED);
|
||||
groupBatchRepository.save(batch);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (paymentTransaction.getPollingStatus() == Status.SUCCESS) {
|
||||
batch.setStatus(GroupBatchStatus.POLL_SUCCESS);
|
||||
return groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
batch.setStatus(GroupBatchStatus.POLL_FAILED);
|
||||
return groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public GroupBatch processIntegration(UUID batchId) {
|
||||
GroupBatch batch = getBatchById(batchId);
|
||||
if (batch.getStatus() != GroupBatchStatus.POLL_SUCCESS) {
|
||||
throw new ApiException("Batch integration step requires a successful poll step");
|
||||
}
|
||||
|
||||
batch.setStatus(GroupBatchStatus.INTEGRATING);
|
||||
batch.setIntegrationStartedAt(LocalDateTime.now());
|
||||
groupBatchRepository.save(batch);
|
||||
|
||||
List<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
for (GroupBatchItem item : items) {
|
||||
futures.add(CompletableFuture.runAsync(() -> integrateItem(item)));
|
||||
}
|
||||
futures.forEach(CompletableFuture::join);
|
||||
|
||||
long integrated = groupBatchItemRepository.countByGroupBatchIdAndIntegrationStatus(
|
||||
batchId, GroupBatchItemIntegrationStatus.INTEGRATED);
|
||||
long failed = groupBatchItemRepository.countByGroupBatchIdAndIntegrationStatus(
|
||||
batchId, GroupBatchItemIntegrationStatus.FAILED);
|
||||
|
||||
if (integrated == 0 && failed > 0) {
|
||||
batch.setStatus(GroupBatchStatus.FAILED);
|
||||
} else if (failed > 0) {
|
||||
batch.setStatus(GroupBatchStatus.PARTIAL);
|
||||
} else {
|
||||
batch.setStatus(GroupBatchStatus.COMPLETED);
|
||||
}
|
||||
|
||||
batch.setCompletedAt(LocalDateTime.now());
|
||||
|
||||
recalculateStats(batch);
|
||||
|
||||
return groupBatchRepository.save(batch);
|
||||
}
|
||||
|
||||
private void confirmItem(GroupBatch batch, GroupBatchItem item, Transaction template) {
|
||||
GroupBatchItem mutableItem = groupBatchItemRepository.findById(item.getId()).orElse(item);
|
||||
try {
|
||||
Transaction transaction = Utils.deepCopy(template, Transaction.class);
|
||||
transaction.setId(null);
|
||||
transaction.setUserId(batch.getUserId());
|
||||
transaction.setType(RequestType.CONFIRM);
|
||||
transaction.setPartyType(PartyType.BATCH);
|
||||
transaction.setAuthType(AuthType.REMOTE); // default to remote for now
|
||||
transaction.setAmount(mutableItem.getAmount());
|
||||
transaction.setTotalAmount(mutableItem.getAmount());
|
||||
|
||||
Recipient recipient = recipientRepository.findById(mutableItem.getRecipientId())
|
||||
.orElseThrow(() -> new ApiException("Recipient not found for batch item"));
|
||||
transaction.setCreditName(recipient.getName());
|
||||
transaction.setCreditPhone(recipient.getPhoneNumber());
|
||||
transaction.setCreditAccount(recipient.getAccount());
|
||||
transaction.setPaymentProcessorLabel(batch.getPaymentProcessorLabel());
|
||||
transaction.setPaymentProcessorName(batch.getPaymentProcessorName());
|
||||
transaction.setPaymentProcessorImage(batch.getPaymentProcessorImage());
|
||||
|
||||
transaction = calculateChargesHandler.process(transaction);
|
||||
transaction = (Transaction) cleanUpHandler.process(transaction);
|
||||
Transaction result = (Transaction) confirmHandler.process(transaction);
|
||||
mutableItem.setTransaction(result);
|
||||
|
||||
if (result.getConfirmationStatus() == Status.SUCCESS || result.getStatus() == Status.SUCCESS) {
|
||||
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.CONFIRMED);
|
||||
mutableItem.setStatus(GroupBatchItemStatus.CONFIRMED);
|
||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.PENDING);
|
||||
mutableItem.setConfirmError(null);
|
||||
} else {
|
||||
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);
|
||||
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.SKIPPED);
|
||||
mutableItem.setConfirmError(result.getErrorMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);
|
||||
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.SKIPPED);
|
||||
mutableItem.setConfirmError(e.getMessage());
|
||||
}
|
||||
|
||||
groupBatchItemRepository.save(mutableItem);
|
||||
}
|
||||
|
||||
private void integrateItem(GroupBatchItem item) {
|
||||
GroupBatchItem mutableItem = groupBatchItemRepository.findById(item.getId()).orElse(item);
|
||||
if (mutableItem.getConfirmStatus() != GroupBatchItemConfirmStatus.CONFIRMED) {
|
||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.SKIPPED);
|
||||
mutableItem.setStatus(GroupBatchItemStatus.SKIPPED);
|
||||
groupBatchItemRepository.save(mutableItem);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mutableItem.getTransaction() == null) {
|
||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.FAILED);
|
||||
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
||||
mutableItem.setIntegrationError("Missing transaction for integration");
|
||||
groupBatchItemRepository.save(mutableItem);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Transaction transaction = transactionService.findById(mutableItem.getTransaction().getId());
|
||||
transaction.setPaymentStatus(Status.SUCCESS);
|
||||
transaction.setPollingStatus(Status.SUCCESS);
|
||||
transactionService.save(transaction);
|
||||
|
||||
integrationHandler.process(transaction);
|
||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.INTEGRATED);
|
||||
mutableItem.setStatus(GroupBatchItemStatus.INTEGRATED);
|
||||
mutableItem.setIntegrationError(null);
|
||||
} catch (Exception e) {
|
||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.FAILED);
|
||||
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
||||
mutableItem.setIntegrationError(e.getMessage());
|
||||
}
|
||||
|
||||
groupBatchItemRepository.save(mutableItem);
|
||||
}
|
||||
|
||||
private void recalculateStats(GroupBatch batch) {
|
||||
List<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batch.getId());
|
||||
|
||||
long successfulCount = items.stream()
|
||||
.filter(item -> item.getStatus() == GroupBatchItemStatus.INTEGRATED
|
||||
|| (item.getIntegrationStatus() == GroupBatchItemIntegrationStatus.INTEGRATED)
|
||||
|| (item.getConfirmStatus() == GroupBatchItemConfirmStatus.CONFIRMED
|
||||
&& item.getIntegrationStatus() != GroupBatchItemIntegrationStatus.FAILED))
|
||||
.count();
|
||||
|
||||
long failedCount = items.stream()
|
||||
.filter(item -> item.getStatus() == GroupBatchItemStatus.FAILED
|
||||
|| item.getIntegrationStatus() == GroupBatchItemIntegrationStatus.FAILED
|
||||
|| (item.getConfirmStatus() == GroupBatchItemConfirmStatus.FAILED
|
||||
&& item.getIntegrationStatus() != GroupBatchItemIntegrationStatus.INTEGRATED))
|
||||
.count();
|
||||
|
||||
BigDecimal totalValue = items.stream()
|
||||
.map(item -> item.getAmount() == null ? BigDecimal.ZERO : item.getAmount())
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
batch.setCount(items.size());
|
||||
batch.setValue(totalValue);
|
||||
batch.setSuccessfulCount((int) successfulCount);
|
||||
batch.setFailedCount((int) failedCount);
|
||||
}
|
||||
|
||||
public GroupBatch getBatchById(UUID batchId) {
|
||||
return groupBatchRepository.findById(batchId)
|
||||
.orElseThrow(() -> new ApiException("Group batch not found"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ public class HotRechargeBalanceService {
|
||||
private String hotApiUrl;
|
||||
|
||||
@Cacheable(value = "hotrechargeBalance", key = "#token + '_' + #amount.toString()")
|
||||
public boolean checkBalance(String token, BigDecimal amount) {
|
||||
public boolean checkBalance(String token, BigDecimal amount, String accId) {
|
||||
try {
|
||||
String balanceUrl = hotApiUrl + "/account/balance/4";
|
||||
String balanceUrl = hotApiUrl + "/account/balance/" + accId;
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(token);
|
||||
|
||||
|
||||
@@ -34,52 +34,6 @@ public class HotRechargeTokenService {
|
||||
private long cacheExpiryTime;
|
||||
|
||||
public synchronized String getToken() {
|
||||
if (isCacheValid()) {
|
||||
logger.info("Cache is valid, attempting token refresh");
|
||||
return refreshCachedToken();
|
||||
}
|
||||
return fetchNewToken();
|
||||
}
|
||||
|
||||
private boolean isCacheValid() {
|
||||
return cachedToken != null && cachedRefreshToken != null && System.currentTimeMillis() < cacheExpiryTime;
|
||||
}
|
||||
|
||||
private synchronized String refreshCachedToken() {
|
||||
if (!isCacheValid()) {
|
||||
return fetchNewToken();
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info("Refreshing HotRecharge token via refresh endpoint");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
Map<String, String> payload = Map.of(
|
||||
"token", cachedToken,
|
||||
"refreshToken", cachedRefreshToken
|
||||
);
|
||||
|
||||
LinkedHashMap<String, Object> response = restService.postAs(hotAuthUrl + "/identity/refresh", payload, headers, LinkedHashMap.class);
|
||||
cachedToken = (String) response.get("token");
|
||||
cachedRefreshToken = (String) response.get("refreshToken");
|
||||
cacheExpiryTime = System.currentTimeMillis() + (cacheDurationMinutes * 60 * 1000L);
|
||||
|
||||
logger.info("HotRecharge token refreshed via refresh endpoint, cache extended by {} minutes", cacheDurationMinutes);
|
||||
return cachedToken;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to refresh HotRecharge token: " + e.getMessage(), e);
|
||||
// Fall back to login on refresh failure
|
||||
return fetchNewToken();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized String fetchNewToken() {
|
||||
if (isCacheValid()) {
|
||||
return refreshCachedToken();
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info("Fetching new HotRecharge token via login");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
@@ -99,9 +53,33 @@ public class HotRechargeTokenService {
|
||||
logger.info("New HotRecharge token cached, cache expires in {} minutes", cacheDurationMinutes);
|
||||
return cachedToken;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to fetch HotRecharge token: " + e.getMessage(), e);
|
||||
cachedToken = null;
|
||||
cachedRefreshToken = null;
|
||||
logger.error("Failed to fetch HotRecharge token: {}", e.getMessage(), e);
|
||||
logger.info("Trying to refresh token");
|
||||
return refreshCachedToken();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized String refreshCachedToken() {
|
||||
try {
|
||||
logger.info("Refreshing HotRecharge token via refresh endpoint");
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
Map<String, String> payload = Map.of(
|
||||
"token", cachedToken,
|
||||
"refreshToken", cachedRefreshToken
|
||||
);
|
||||
|
||||
LinkedHashMap<String, Object> response = restService.postAs(hotAuthUrl + "/identity/refresh", payload, headers, LinkedHashMap.class);
|
||||
cachedToken = (String) response.get("token");
|
||||
cachedRefreshToken = (String) response.get("refreshToken");
|
||||
cacheExpiryTime = System.currentTimeMillis() + (cacheDurationMinutes * 60 * 1000L);
|
||||
|
||||
logger.info("HotRecharge token refreshed via refresh endpoint, cache extended by {} minutes", cacheDurationMinutes);
|
||||
return cachedToken;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to refresh HotRecharge token: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,40 @@
|
||||
package zw.qantra.tm.domain.services.processors.workflows;
|
||||
package zw.qantra.tm.domain.services;
|
||||
|
||||
import io.nflow.engine.service.WorkflowInstanceService;
|
||||
import io.nflow.engine.workflow.curated.CronWorkflow;
|
||||
import io.nflow.engine.workflow.definition.*;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.LocalDateTime;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import zw.qantra.tm.domain.dtos.StateResponse;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
import zw.qantra.tm.domain.services.TransactionService;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.WorkflowUtils;
|
||||
import zw.qantra.tm.utils.LogUtils;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static io.nflow.engine.workflow.definition.NextAction.moveToStateAfter;
|
||||
import static org.joda.time.DateTime.now;
|
||||
import static org.joda.time.Period.hours;
|
||||
|
||||
@Component
|
||||
public class PollCronWorkflow extends CronWorkflow {
|
||||
Logger log = org.slf4j.LoggerFactory.getLogger(PollCronWorkflow.class);
|
||||
@RequiredArgsConstructor
|
||||
public class JobService {
|
||||
Logger log = new JobRunrDashboardLogger(LoggerFactory.getLogger(JobService.class));
|
||||
|
||||
@Autowired
|
||||
private TransactionService transactionService;
|
||||
@Autowired
|
||||
private WorkflowInstanceService workflowInstanceService;
|
||||
@Autowired
|
||||
private WorkflowUtils workflowUtils;
|
||||
private final WorkflowInstanceService workflowInstanceService;
|
||||
private final WorkflowUtils workflowUtils;
|
||||
private final TransactionService transactionService;
|
||||
private final SettingService settingService;
|
||||
|
||||
public static final String TYPE = "transactionPollCronWorkflow";
|
||||
|
||||
protected PollCronWorkflow() {
|
||||
super(TYPE, new WorkflowSettings.Builder()
|
||||
.setHistoryDeletableAfter(hours(1))
|
||||
.setDeleteHistoryCondition(() -> true).build());
|
||||
}
|
||||
|
||||
public NextAction doWork(StateExecution execution) {
|
||||
public void checkOrphanedTransactions() {
|
||||
List<Transaction> transactions = transactionService.getTransactionRepository()
|
||||
.findAllByConfirmationStatusAndIntegrationStatus(Status.SUCCESS, Status.PENDING);
|
||||
.findAllByPaymentStatusAndPollingStatus(Status.SUCCESS, Status.PENDING);
|
||||
if (!transactions.isEmpty())
|
||||
log.info("Transaction count {}", transactions.size());
|
||||
|
||||
@@ -57,6 +43,13 @@ public class PollCronWorkflow extends CronWorkflow {
|
||||
for (Transaction transaction: transactions) {
|
||||
attempts++;
|
||||
try {
|
||||
int maxRetries = settingService.getIntegerSetting("MAX_RETRIES");
|
||||
int retries = transaction.getRetries() == null ? 0 : transaction.getRetries();
|
||||
|
||||
if (retries >= maxRetries)
|
||||
break;
|
||||
|
||||
transaction.setRetries(retries + 1);
|
||||
// polling logic
|
||||
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||
.setId(transaction.getWorkflowId())
|
||||
@@ -118,27 +111,11 @@ public class PollCronWorkflow extends CronWorkflow {
|
||||
}
|
||||
|
||||
}catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
log.debug(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.trace("Polling complete. {} transactions processed, {} successful", attempts, success);
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("attempts", attempts.toString());
|
||||
map.put("success", success.toString());
|
||||
execution.setVariable(LocalDateTime.now().toString(), map);
|
||||
|
||||
return moveToStateAfter(WAIT_FOR_WORK_TO_FINISH, now().plusMinutes(2),
|
||||
String.format("Cron complete on %s transactions", transactions.size()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handleFailureImpl(StateExecution execution) {
|
||||
return super.handleFailureImpl(execution);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DateTime waitForWorkToFinishImpl(StateExecution execution) {
|
||||
return super.waitForWorkToFinishImpl(execution);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
@@ -81,7 +82,8 @@ public class ProviderService {
|
||||
return products;
|
||||
}
|
||||
|
||||
@Cacheable(value = "providerProducts", key = "#providerUid")
|
||||
// todo: figure out caching for different providers
|
||||
// @Cacheable(value = "providerProducts", key = "#providerUid")
|
||||
public List<SBZProductDto> getProviderProducts(String providerUid) {
|
||||
try {
|
||||
Provider provider = providerRepository.findById(UUID.fromString(providerUid)).orElse(null);
|
||||
@@ -100,7 +102,7 @@ public class ProviderService {
|
||||
try {
|
||||
response = restService.getAs(stockUrl, headers, LinkedHashMap.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching products for provider " + providerId, e);
|
||||
log.error("Error fetching products for provider " + providerId);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -126,6 +128,7 @@ public class ProviderService {
|
||||
|
||||
return products;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Error fetching products: {}", providerUid);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -159,11 +162,7 @@ public class ProviderService {
|
||||
}
|
||||
|
||||
public Object getProviderByClientId(String clientId) {
|
||||
Provider provider = providerRepository.findByClientId(clientId);
|
||||
if (provider != null) {
|
||||
return ((List)getProviders(Collections.singletonList(provider))).stream().findFirst().orElse(null);
|
||||
}
|
||||
return null;
|
||||
return providerRepository.findByClientId(clientId);
|
||||
}
|
||||
|
||||
public Object findAll(Specification<Provider> specification, Pageable pageable) {
|
||||
@@ -237,4 +236,17 @@ public class ProviderService {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Evict ALL providerProducts cache entries
|
||||
@CacheEvict(value = "providerProducts", allEntries = true)
|
||||
public void evictProviderProductsCache() {
|
||||
// empty - annotation handles the eviction
|
||||
}
|
||||
|
||||
// Or evict a SINGLE entry by key
|
||||
@CacheEvict(value = "providerProducts", key = "#providerUid")
|
||||
public void evictProviderProductCache(String providerUid) {
|
||||
// only evicts cache for this specific providerUid
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
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 org.springframework.transaction.annotation.Transactional;
|
||||
import zw.qantra.tm.domain.models.Recipient;
|
||||
import zw.qantra.tm.domain.models.RecipientGroup;
|
||||
import zw.qantra.tm.domain.models.RecipientGroupMember;
|
||||
import zw.qantra.tm.domain.repositories.RecipientGroupMemberRepository;
|
||||
import zw.qantra.tm.domain.repositories.RecipientGroupRepository;
|
||||
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RecipientGroupService {
|
||||
private final RecipientGroupRepository recipientGroupRepository;
|
||||
private final RecipientGroupMemberRepository recipientGroupMemberRepository;
|
||||
private final RecipientRepository recipientRepository;
|
||||
|
||||
@Transactional
|
||||
public RecipientGroup create(RecipientGroup group, List<Recipient> recipients) {
|
||||
RecipientGroup savedGroup = recipientGroupRepository.save(group);
|
||||
|
||||
if (recipients != null && !recipients.isEmpty()) {
|
||||
addMembers(savedGroup.getId(), savedGroup.getUserId(), recipients);
|
||||
}
|
||||
|
||||
return savedGroup;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<RecipientGroupMember> addMembers(UUID groupId, String userId, List<Recipient> recipients) {
|
||||
get(groupId, userId);
|
||||
List<RecipientGroupMember> added = new ArrayList<>();
|
||||
for (Recipient incomingRecipient : recipients) {
|
||||
Recipient recipient = resolveOrCreateRecipient(incomingRecipient, userId);
|
||||
|
||||
if (recipientGroupMemberRepository.existsByRecipientGroupIdAndRecipientId(groupId, recipient.getId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RecipientGroupMember member = RecipientGroupMember.builder()
|
||||
.recipientGroupId(groupId)
|
||||
.recipientUid(recipient.getId())
|
||||
.recipient(recipient)
|
||||
.account(recipient.getAccount())
|
||||
.userId(userId)
|
||||
.build();
|
||||
added.add(recipientGroupMemberRepository.save(member));
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
public Page<RecipientGroup> findAll(Specification<RecipientGroup> spec, Pageable pageable) {
|
||||
return recipientGroupRepository.findAll(spec, pageable);
|
||||
}
|
||||
|
||||
public List<RecipientGroup> list(String userId) {
|
||||
return recipientGroupRepository.findAllByUserId(userId);
|
||||
}
|
||||
|
||||
public RecipientGroup get(UUID groupId, String userId) {
|
||||
return recipientGroupRepository.findByIdAndUserId(groupId, userId)
|
||||
.orElseThrow(() -> new ApiException("Recipient group not found"));
|
||||
}
|
||||
|
||||
public RecipientGroup update(UUID groupId, String userId, String name, String description) {
|
||||
RecipientGroup group = get(groupId, userId);
|
||||
group.setName(name);
|
||||
group.setDescription(description);
|
||||
return recipientGroupRepository.save(group);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(UUID groupId, String userId) {
|
||||
RecipientGroup group = get(groupId, userId);
|
||||
group.setDeleted(true);
|
||||
recipientGroupRepository.save(group);
|
||||
|
||||
List<RecipientGroupMember> members = recipientGroupMemberRepository.findAllByRecipientGroupId(groupId);
|
||||
members.forEach(member -> member.setDeleted(true));
|
||||
recipientGroupMemberRepository.saveAll(members);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<RecipientGroupMember> addMembersByIds(UUID groupId, String userId, List<UUID> recipientIds) {
|
||||
get(groupId, userId);
|
||||
List<RecipientGroupMember> added = new ArrayList<>();
|
||||
for (UUID recipientId : recipientIds) {
|
||||
Recipient recipient = recipientRepository.findById(recipientId)
|
||||
.orElseThrow(() -> new ApiException("Recipient not found: " + recipientId));
|
||||
if (!userId.equals(recipient.getUserId())) {
|
||||
throw new ApiException("Recipient does not belong to user");
|
||||
}
|
||||
if (recipientGroupMemberRepository.existsByRecipientGroupIdAndRecipientId(groupId, recipientId)) {
|
||||
continue;
|
||||
}
|
||||
RecipientGroupMember member = RecipientGroupMember.builder()
|
||||
.recipientGroupId(groupId)
|
||||
.recipientUid(recipientId)
|
||||
.recipient(recipient)
|
||||
.account(recipient.getAccount())
|
||||
.userId(userId)
|
||||
.build();
|
||||
added.add(recipientGroupMemberRepository.save(member));
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
private Recipient resolveOrCreateRecipient(Recipient incomingRecipient, String fallbackUserId) {
|
||||
if (incomingRecipient == null || incomingRecipient.getAccount() == null || incomingRecipient.getAccount().isBlank()) {
|
||||
throw new ApiException("Recipient account is required");
|
||||
}
|
||||
|
||||
String recipientUserId = incomingRecipient.getUserId() == null || incomingRecipient.getUserId().isBlank()
|
||||
? fallbackUserId
|
||||
: incomingRecipient.getUserId();
|
||||
|
||||
List<Recipient> existing = recipientRepository.findByAccountAndUserId(incomingRecipient.getAccount(), recipientUserId);
|
||||
if (!existing.isEmpty()) {
|
||||
return existing.get(0);
|
||||
}
|
||||
|
||||
incomingRecipient.setId(null);
|
||||
incomingRecipient.setUserId(recipientUserId);
|
||||
return recipientRepository.save(incomingRecipient);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void removeMember(UUID groupId, UUID recipientId, String userId) {
|
||||
get(groupId, userId);
|
||||
List<RecipientGroupMember> members = recipientGroupMemberRepository.findAllByRecipientGroupId(groupId);
|
||||
RecipientGroupMember target = members.stream()
|
||||
.filter(item -> recipientId.equals(item.getRecipientUid()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ApiException("Group member not found"));
|
||||
target.setDeleted(true);
|
||||
recipientGroupMemberRepository.save(target);
|
||||
}
|
||||
|
||||
public List<RecipientGroupMember> members(UUID groupId, String userId) {
|
||||
get(groupId, userId);
|
||||
return recipientGroupMemberRepository.findAllByRecipientGroupId(groupId);
|
||||
}
|
||||
|
||||
public Page<RecipientGroupMember> findAllMembers(Specification<RecipientGroupMember> spec, Pageable pageable) {
|
||||
return recipientGroupMemberRepository.findAll(spec, pageable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,19 @@ public class SettingService {
|
||||
return settingRepository;
|
||||
}
|
||||
|
||||
public Integer getIntegerSetting(String key) {
|
||||
try {
|
||||
Setting setting = settingRepository.findBySettingName(key);
|
||||
if (setting != null) {
|
||||
return Integer.parseInt(setting.getSettingValue());
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean getBooleanSetting(String key) {
|
||||
try {
|
||||
Setting setting = settingRepository.findBySettingName(key);
|
||||
|
||||
@@ -3,6 +3,8 @@ package zw.qantra.tm.domain.services;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -15,6 +17,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import zw.qantra.tm.domain.enums.RequestType;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
import zw.qantra.tm.domain.models.Currency;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
import zw.qantra.tm.domain.repositories.TransactionRepository;
|
||||
import zw.qantra.tm.exceptions.ApiException;
|
||||
@@ -28,6 +31,21 @@ public class TransactionService {
|
||||
@Getter
|
||||
private final TransactionRepository transactionRepository;
|
||||
private final AdditionalDataService additionalDataService;
|
||||
private final CurrencyService currencyService;
|
||||
|
||||
public BigDecimal calculateCreditAmount(Transaction transaction) {
|
||||
if (transaction.getCreditCurrency() == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
Currency currency = currencyService.findCurrencyByIsoCode(transaction.getCreditCurrency().name());
|
||||
|
||||
BigDecimal bankRate = new BigDecimal(currency.getBankRate());
|
||||
BigDecimal creditAmount = transaction.getAmount().multiply(bankRate).setScale(4, RoundingMode.HALF_EVEN);
|
||||
|
||||
transaction.setCreditAmount(creditAmount);
|
||||
return creditAmount;
|
||||
}
|
||||
|
||||
public void reassignTransactions(String oldUid, String newUid){
|
||||
List<Transaction> transactions = transactionRepository.findAllByUserId(oldUid);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package zw.qantra.tm.domain.services;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.domain.dtos.erp.VelocityAccountDto;
|
||||
import zw.qantra.tm.domain.dtos.erp.VelocityStatementDto;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class VelocityAccountService {
|
||||
|
||||
private final VelocityPaymentService velocityPaymentService;
|
||||
|
||||
private static final String VELOCITY_API_ACCOUNT_BY_PHONE_PATH = "/accounts/phone/{currencyPhoneConcat}";
|
||||
private static final String VELOCITY_API_STATEMENT_BY_PHONE_PATH = "/statements/phone/{currencyPhoneConcat}";
|
||||
|
||||
/**
|
||||
* Fetches an account by the currency-phone concatenation.
|
||||
*
|
||||
* @param currencyPhoneConcat The concatenation of currency and phone (e.g. "USD+263771234567").
|
||||
* @return VelocityAccountDto with account details.
|
||||
*/
|
||||
public VelocityAccountDto getAccountByPhone(String currencyPhoneConcat) {
|
||||
try {
|
||||
LinkedHashMap responseMap = velocityPaymentService.makeRequest(
|
||||
HttpMethod.GET,
|
||||
VELOCITY_API_ACCOUNT_BY_PHONE_PATH.replace("{currencyPhoneConcat}", currencyPhoneConcat),
|
||||
null
|
||||
);
|
||||
if (responseMap == null || responseMap.isEmpty()) {
|
||||
log.error("Error fetching account");
|
||||
throw new IllegalStateException("Error fetching account");
|
||||
}
|
||||
return Utils.deepCopy(responseMap, VelocityAccountDto.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching account by phone {}: {}", currencyPhoneConcat, e.getMessage(), e);
|
||||
throw new IllegalStateException("Error fetching account: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an account statement by the currency-phone concatenation and date range.
|
||||
*
|
||||
* @param currencyPhoneConcat The concatenation of currency and phone (e.g. "USD+263771234567").
|
||||
* @param startDate The start date for the statement (e.g. "2025-01-01").
|
||||
* @param endDate The end date for the statement (e.g. "2025-12-31").
|
||||
* @return VelocityStatementDto with statement details.
|
||||
*/
|
||||
public VelocityStatementDto getStatementByPhone(String currencyPhoneConcat, String startDate, String endDate) {
|
||||
try {
|
||||
String path = VELOCITY_API_STATEMENT_BY_PHONE_PATH
|
||||
.replace("{currencyPhoneConcat}", currencyPhoneConcat)
|
||||
+ "?startDate=" + startDate + "&endDate=" + endDate;
|
||||
|
||||
LinkedHashMap responseMap = velocityPaymentService.makeRequest(
|
||||
HttpMethod.GET,
|
||||
path,
|
||||
null
|
||||
);
|
||||
if (responseMap == null || responseMap.isEmpty()) {
|
||||
log.error("Error fetching statement");
|
||||
throw new IllegalStateException("Error fetching statement");
|
||||
}
|
||||
return Utils.deepCopy(responseMap, VelocityStatementDto.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching statement for phone {} ({} - {}): {}",
|
||||
currencyPhoneConcat, startDate, endDate, e.getMessage(), e);
|
||||
throw new IllegalStateException("Error fetching statement: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ public class VelocityPaymentService {
|
||||
private static final String VELOCITY_API_POLL_PAYMENT_PATH = "/transactions/poll/{traceId}";
|
||||
private static final String VELOCITY_API_UPDATE_WORKFLOW_PATH = "/sales-orders/update-workflow/{traceId}";
|
||||
private static final String VELOCITY_API_CUSTOMERS = "/customers";
|
||||
|
||||
private static final String VELOCITY_API_UPDATE_TRANSACTION_PATH = "/transactions/{id}";
|
||||
|
||||
public VelocityCustomerResponseDto createCustomer(RegisterRequest registerRequest) {
|
||||
VelocityCustomerDto payload = VelocityCustomerDto.builder()
|
||||
@@ -75,10 +75,6 @@ public class VelocityPaymentService {
|
||||
if(errors != null && !errors.isEmpty()){
|
||||
String errorMessage = errors.get(0);
|
||||
|
||||
if(errorMessage.contains("already exists")) {
|
||||
log.warn("Customer already exists in Velocity: {}", errorMessage);
|
||||
throw new AlreadyExistsException(errorMessage); // or handle as needed
|
||||
}
|
||||
log.error("Error creating customer: {}", errorMessage);
|
||||
throw new IllegalStateException("Error creating customer: " + errorMessage);
|
||||
}
|
||||
@@ -86,6 +82,11 @@ public class VelocityPaymentService {
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating customer: {}", e.getMessage(), e);
|
||||
if(e.getMessage().contains("already exists")) {
|
||||
log.warn("Customer already exists in Velocity: {}", e.getMessage());
|
||||
throw new AlreadyExistsException(e.getMessage()); // or handle as needed
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Error creating customer: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
@@ -155,10 +156,10 @@ public class VelocityPaymentService {
|
||||
.paymentProcessorLabel(paymentProcessorLabel)
|
||||
.debitCurrency(transaction.getDebitCurrency().name())
|
||||
.debitRef(transaction.getErpSalesRef())
|
||||
.creditPhone(velocityApiProperties.getCreditPhone())
|
||||
.creditPhone("")
|
||||
.creditRegion("ZW")
|
||||
.creditAccount(velocityApiProperties.getCreditAccount())
|
||||
.authType(velocityApiProperties.getDefaultAuthType())
|
||||
.creditAccount("")
|
||||
.authType(paymentProcessorLabel.equals("VMC") ? "WEB" : "REMOTE")
|
||||
.build();
|
||||
|
||||
if ("MPGS".equalsIgnoreCase(transaction.getPaymentProcessorLabel())) {
|
||||
@@ -170,14 +171,22 @@ public class VelocityPaymentService {
|
||||
LinkedHashMap response = makeRequest(
|
||||
HttpMethod.POST, VELOCITY_API_INITIATE_PAYMENT_PATH, payload);
|
||||
LinkedHashMap body = (LinkedHashMap) response.get("body");
|
||||
|
||||
if(body == null){
|
||||
String errorMessage = (String) response.get("errorMessage");
|
||||
log.error("Error initiating payment: {}", errorMessage);
|
||||
throw new IllegalStateException("Error initiating payment: " + errorMessage);
|
||||
}
|
||||
String transactionStatus = (String) body.get("status");
|
||||
String tranTrace = (String) body.get("trace");
|
||||
String tranId = (String) body.get("id");
|
||||
String transactionName = (String) body.get("name");
|
||||
String redirectUrl = body.containsKey("redirectUrl") ? (String) body.get("redirectUrl") : null;
|
||||
|
||||
if ("SUCCESS".equalsIgnoreCase(transactionStatus)) {
|
||||
transaction.setErpSalesPaymentRef(transactionName);
|
||||
transaction.setOrderTransactionTrace(tranTrace);
|
||||
transaction.setOrderTransactionId(tranId);
|
||||
transaction.setTargetUrl(redirectUrl);
|
||||
return transaction;
|
||||
}
|
||||
@@ -193,6 +202,16 @@ public class VelocityPaymentService {
|
||||
}
|
||||
|
||||
public Transaction pollPaymentStatus(Transaction transaction) {
|
||||
int maxRetries = settingService.getIntegerSetting("MAX_RETRIES");
|
||||
int retries = transaction.getRetries() == null ? 0 : transaction.getRetries();
|
||||
|
||||
if (retries >= maxRetries) {
|
||||
transaction.setErrorMessage("Maximum retries reached");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
transaction.setRetries(retries + 1);
|
||||
|
||||
LinkedHashMap response = makeRequest(
|
||||
HttpMethod.PUT,
|
||||
VELOCITY_API_POLL_PAYMENT_PATH.replace("{traceId}", transaction.getOrderTransactionTrace()),
|
||||
@@ -200,14 +219,44 @@ public class VelocityPaymentService {
|
||||
);
|
||||
|
||||
LinkedHashMap body = (LinkedHashMap) response.get("body");
|
||||
if(body == null){
|
||||
String errorMessage = (String) response.get("errorMessage");
|
||||
log.error("Error polling payment: {}", errorMessage);
|
||||
throw new IllegalStateException("Error initiating payment: " + errorMessage);
|
||||
}
|
||||
transaction.setStatus(Status.valueOf((String) body.get("status")));
|
||||
transaction.setPaymentStatus(Status.valueOf((String) body.get("paymentStatus")));
|
||||
transaction.setPollingStatus(Status.valueOf((String) body.get("pollStatus")));
|
||||
transaction.setDebitRef((String) body.get("debitRef"));
|
||||
transaction.setPaymentProcessorRef((String) body.get("paymentProcessorRef"));
|
||||
transaction.setResponseCode((String) body.get("responseCode"));
|
||||
|
||||
if (!transaction.getStatus().equals(Status.SUCCESS)) {
|
||||
transaction.setErrorMessage((String) body.get("errorMessage"));
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the OTP (additional data) on a transaction requiring manual verification.
|
||||
* Equivalent to the Flutter/Dart {@code updateTransaction} method.
|
||||
*
|
||||
* @param id The transaction ID to update.
|
||||
* @param otp The OTP value to set as additional data.
|
||||
* @return A {@code LinkedHashMap} containing the Velocity API response.
|
||||
*/
|
||||
public LinkedHashMap updateTransaction(String id, String otp) {
|
||||
Map<String, String> payload = new HashMap<>();
|
||||
payload.put("additionalData", otp);
|
||||
|
||||
return makeRequest(
|
||||
HttpMethod.PUT,
|
||||
VELOCITY_API_UPDATE_TRANSACTION_PATH.replace("{id}", id),
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
@Async
|
||||
public void updateSalesOrderWorkflow(Transaction transaction) {
|
||||
LinkedHashMap response = makeRequest(
|
||||
@@ -226,7 +275,7 @@ public class VelocityPaymentService {
|
||||
}
|
||||
|
||||
|
||||
private LinkedHashMap makeRequest(HttpMethod method, String path, Object payload) {
|
||||
LinkedHashMap makeRequest(HttpMethod method, String path, Object payload) {
|
||||
long startNs = System.nanoTime();
|
||||
ResponseEntity<LinkedHashMap> response;
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||
import zw.qantra.tm.domain.enums.RequestType;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
@@ -41,14 +42,22 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter
|
||||
// do this if we not simulating success, if simulating success, we skip balance check and just return success
|
||||
if(!settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
String token = hotRechargeTokenService.getToken();
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) {
|
||||
|
||||
// Account Type Id Account Name
|
||||
// 1 ZWG
|
||||
// 2 ZWG Non-VAT (Utilitiies)
|
||||
// 3 USD
|
||||
// 4 USD Non-VAT (Utilitiies)
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount(), "3")) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
transaction.setResponseCode("98");
|
||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
|
||||
transaction.setCreditCurrency(CurrencyType.USD);
|
||||
transaction.setCreditAmount(transaction.getAmount());
|
||||
transaction.setCreditAccount(Utils.formatPhoneNumber(transaction.getCreditAccount(), "ZW"));
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
@@ -59,9 +68,15 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter
|
||||
put("value", transaction.getBillName());
|
||||
}});
|
||||
additionalDataList.add(new HashMap() {{
|
||||
put("name", "Destination Phone Number");
|
||||
put("name", "From");
|
||||
put("value", transaction.getDebitPhone());
|
||||
}});
|
||||
|
||||
additionalDataList.add(new HashMap() {{
|
||||
put("name", "To");
|
||||
put("value", transaction.getCreditAccount());
|
||||
}});
|
||||
|
||||
if(transaction.getBillProductName() != null && !transaction.getBillProductName().isEmpty()) {
|
||||
additionalDataList.add(new HashMap() {{
|
||||
put("name", "Product");
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||
import zw.qantra.tm.domain.enums.RequestType;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
@@ -33,6 +34,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
private final HotRechargeTokenService hotRechargeTokenService;
|
||||
private final HotRechargeBalanceService hotRechargeBalanceService;
|
||||
private final SettingService settingService;
|
||||
private final TransactionService transactionService;
|
||||
|
||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||
private String hotApiUrl;
|
||||
@@ -45,7 +47,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
// do this if we not simulating success, if simulating success, we skip balance check and just return success
|
||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
transaction.setResponseCode("02");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
@@ -57,21 +59,38 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
return transaction;
|
||||
}
|
||||
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
Map<String, Object> customerData = checkCustomer(token, transaction);
|
||||
if (customerData == null) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("91");
|
||||
transaction.setResponseCode("90");
|
||||
transaction.setErrorMessage("Customer not found or invalid");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
// Account Type Id Account Name
|
||||
// 1 ZWG
|
||||
// 2 ZWG Non-VAT (Utilitiies)
|
||||
// 3 USD
|
||||
// 4 USD Non-VAT (Utilitiies)
|
||||
String accId = "4";
|
||||
String currency = extractCurrency(customerData);
|
||||
logger.info("Customer currency: {}", currency.isEmpty() ? "UNKNOWN" : currency);
|
||||
if(currency.equals("ZWG")){
|
||||
accId = "2";
|
||||
transaction.setCreditCurrency(CurrencyType.ZWG);
|
||||
}else {
|
||||
transaction.setCreditCurrency(CurrencyType.USD);
|
||||
}
|
||||
|
||||
transaction.setCreditAmount(transactionService.calculateCreditAmount(transaction));
|
||||
|
||||
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.");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
|
||||
@@ -103,6 +122,9 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
Map<String, Object> details = (Map<String, Object>) customerData.get("details");
|
||||
if (details != null) {
|
||||
for (Map.Entry<String, Object> entry : details.entrySet()) {
|
||||
// don't show customer ZWG value. It leads to confusion
|
||||
if(entry.getValue().toString().equals("ZWG"))
|
||||
continue;
|
||||
additionalDataList.add(Map.of("name", entry.getKey(), "value", entry.getValue().toString()));
|
||||
}
|
||||
}
|
||||
@@ -110,15 +132,27 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
||||
return additionalDataList;
|
||||
}
|
||||
|
||||
private String extractCurrency(Map<String, Object> customerData) {
|
||||
if (customerData == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Object detailsObj = customerData.get("details");
|
||||
if (!(detailsObj instanceof Map<?, ?> details)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Object currency = details.get("Currency");
|
||||
if (currency == null) {
|
||||
currency = details.get("currency");
|
||||
}
|
||||
|
||||
return currency == null ? "" : currency.toString().trim();
|
||||
}
|
||||
|
||||
private Map<String, Object> checkCustomer(String token, Transaction transaction) {
|
||||
try {
|
||||
Provider provider = providerService.getProviderRepository()
|
||||
.findByClientId(transaction.getBillClientId());
|
||||
if(provider == null){
|
||||
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
|
||||
}
|
||||
Map meta = Utils.fromJson(provider.getMeta(), Map.class);
|
||||
String hotProductId = (String) meta.get("hotProductId");
|
||||
String hotProductId = providerService.getProviderId(transaction.getBillClientId());
|
||||
|
||||
String customerUrl = hotApiUrl + "/query/customer/" + hotProductId + "/" + transaction.getCreditAccount();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||
import zw.qantra.tm.domain.enums.RequestType;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.models.AdditionalData;
|
||||
@@ -36,6 +37,8 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||
private String hotApiUrl;
|
||||
|
||||
private static final String ZESA_ZWG_CLIENT = "zesa_prepaid_zwg";
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Object process(Transaction transaction) throws Exception {
|
||||
@@ -53,7 +56,7 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("responseCode");
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setCreditRef("Simulated Integration Success");
|
||||
transaction.setErrorMessage(null);
|
||||
return transaction;
|
||||
@@ -70,12 +73,8 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
return transaction;
|
||||
}
|
||||
|
||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount())) {
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("92");
|
||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
||||
return transaction;
|
||||
}
|
||||
// todo: a balance check might be necessary here. Removing
|
||||
// for now so not to spam hotrecharge with requests
|
||||
|
||||
LinkedHashMap<String, Object> rechargeResponse = performRecharge(token, transaction);
|
||||
if (rechargeResponse == null) {
|
||||
@@ -132,19 +131,30 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
headers.setBearerAuth(token);
|
||||
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
|
||||
|
||||
String providerId = providerService.getProviderId(transaction.getBillClientId());
|
||||
Map<String, Object> request = new LinkedHashMap<>();
|
||||
request.put("AgentReference", UUID.randomUUID().toString());
|
||||
request.put("ProductId", providerService.getProviderId(transaction.getBillClientId()));
|
||||
request.put("ProductId", providerId);
|
||||
request.put("Target", transaction.getCreditAccount());
|
||||
request.put("Amount", transaction.getAmount());
|
||||
request.put("Amount", transaction.getCreditAmount());
|
||||
|
||||
// for zesa skip this
|
||||
if(!providerService.getProviderId(transaction.getBillClientId()).equals("41")) {
|
||||
// 41 = zesa
|
||||
if(providerId.equals("41")) {
|
||||
request.put("CustomerSMS", "A ZETDC token was purchased for " +
|
||||
"%ACOUNTNAME% (%METERNUMBER%) that resulted in %KWH% units.");
|
||||
}
|
||||
else {
|
||||
request.put("CustomerSMS", "%COMPANYNAME% topped up your account with $%AMOUNT%.");
|
||||
}
|
||||
|
||||
// do this for zesa only
|
||||
if(providerService.getProviderId(transaction.getBillClientId()).equals("41")) {
|
||||
if(providerId.equals("41")) {
|
||||
// update product to zwg product id
|
||||
if(transaction.getCreditCurrency().equals(CurrencyType.ZWG)) {
|
||||
String zwgProviderId = providerService.getProviderId(ZESA_ZWG_CLIENT);
|
||||
request.put("ProductId", zwgProviderId);
|
||||
}
|
||||
|
||||
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
||||
if (transaction.getDebitPhone() != null) {
|
||||
Map<String, String> option = new LinkedHashMap<>();
|
||||
@@ -159,9 +169,8 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
||||
}
|
||||
|
||||
// do this for Econet bundles
|
||||
if(providerService.getProviderId(transaction.getBillClientId()).equals("111")) {
|
||||
String productCode = providerService.getExternalId(
|
||||
providerService.getProviderId(transaction.getBillClientId()), transaction.getProductUid());
|
||||
if(providerId.equals("111")) {
|
||||
String productCode = providerService.getExternalId(providerId, transaction.getProductUid());
|
||||
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
||||
Map<String, String> option = new LinkedHashMap<>();
|
||||
option.put("Name", "ProductCode");
|
||||
|
||||
@@ -59,7 +59,7 @@ public class StewardIntegrationProcessor implements TransactionProcessorInterfac
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_STEWARD_SUCCESS")){
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("responseCode");
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setCreditRef("Simulated Integration Success");
|
||||
transaction.setErrorMessage(null);
|
||||
return transaction;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package zw.qantra.tm.domain.services.processors.payments;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import zw.qantra.tm.domain.enums.Status;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
import zw.qantra.tm.domain.services.SettingService;
|
||||
import zw.qantra.tm.domain.services.TransactionService;
|
||||
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
||||
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
||||
import zw.qantra.tm.rest.RestService;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@Service("REQUEST_WALLET_REMOTE")
|
||||
@RequiredArgsConstructor
|
||||
public class CityWalletRemotePaymentProcessor implements TransactionProcessorInterface {
|
||||
private final Logger logger = LoggerFactory.getLogger(CityWalletRemotePaymentProcessor.class);
|
||||
private final TransactionService transactionService;
|
||||
private final SettingService settingService;
|
||||
private final VelocityPaymentService velocityPaymentService;
|
||||
|
||||
@Override
|
||||
public Object process(Transaction transaction) throws Exception {
|
||||
logger.info("processing transaction");
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) {
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
||||
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
||||
return transaction;
|
||||
}
|
||||
|
||||
try {
|
||||
transaction = velocityPaymentService.processPayment(transaction);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("99");
|
||||
transaction.setSystemErrorMessage("Network error: " + e.getMessage());
|
||||
transaction.setErrorMessage("Payment failed. Please try again.");
|
||||
}
|
||||
|
||||
return transactionService.save(transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object reverse(Transaction transaction) {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object poll(Transaction transaction) throws Exception {
|
||||
logger.info("polling transaction");
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) {
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setTargetUrl("/home");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
try {
|
||||
transaction = velocityPaymentService.pollPaymentStatus(transaction);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("99");
|
||||
transaction.setSystemErrorMessage("Network error: " + e.getMessage());
|
||||
transaction.setErrorMessage("Payment failed. Please try again.");
|
||||
}
|
||||
|
||||
return transactionService.save(transaction);
|
||||
}
|
||||
|
||||
public Transaction updateVelocityTransactionOtp(Transaction transaction) {
|
||||
String code = transaction.getVerificationCode();
|
||||
|
||||
try {
|
||||
velocityPaymentService.updateTransaction(transaction.getOrderTransactionId(), code);
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Network error during payment update: {}", e.getMessage(), e);
|
||||
transaction.setStatus(Status.FAILED);
|
||||
transaction.setResponseCode("99");
|
||||
transaction.setSystemErrorMessage("Network error: " + e.getMessage());
|
||||
transaction.setErrorMessage("Payment failed. Please try again.");
|
||||
}
|
||||
return transaction;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,27 +23,17 @@ import zw.qantra.tm.rest.RestService;
|
||||
@RequiredArgsConstructor
|
||||
public class EcocashRemotePaymentProcessor implements TransactionProcessorInterface {
|
||||
private final Logger logger = LoggerFactory.getLogger(EcocashRemotePaymentProcessor.class);
|
||||
private final RestService restService;
|
||||
private final TransactionService transactionService;
|
||||
private final SettingService settingService;
|
||||
private final VelocityPaymentService velocityPaymentService;
|
||||
|
||||
@Value("${sbz.aggregator.encryption-key}")
|
||||
private String encryptionKey;
|
||||
@Value("${sbz.aggregator.client-id}")
|
||||
private String clientId;
|
||||
@Value("${sbz.aggregator.client-secret}")
|
||||
private String clientSecret;
|
||||
@Value("${steward.payment.processor.url}")
|
||||
private String url;
|
||||
|
||||
@Override
|
||||
public Object process(Transaction transaction) throws Exception {
|
||||
logger.info("processing transaction");
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")) {
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
||||
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
||||
return transaction;
|
||||
@@ -74,18 +64,13 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")) {
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setTargetUrl("/home");
|
||||
return transaction;
|
||||
}
|
||||
|
||||
try {
|
||||
transaction = velocityPaymentService.pollPaymentStatus(transaction);
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
|
||||
transactionService.save(transaction);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
||||
@@ -95,7 +80,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
|
||||
transaction.setErrorMessage("Payment failed. Please try again.");
|
||||
}
|
||||
|
||||
return transaction;
|
||||
return transactionService.save(transaction);
|
||||
}
|
||||
|
||||
public String getHash(String originalString){
|
||||
|
||||
@@ -31,29 +31,17 @@ import java.util.Random;
|
||||
@RequiredArgsConstructor
|
||||
public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
|
||||
private final Logger logger = LoggerFactory.getLogger(MPGSWebPaymentProcessor.class);
|
||||
private final RestService restService;
|
||||
private final TransactionService transactionService;
|
||||
private final SettingService settingService;
|
||||
private final VelocityPaymentService velocityPaymentService;
|
||||
|
||||
@Value("${sbz.aggregator.encryption-key}")
|
||||
private String encryptionKey;
|
||||
@Value("${sbz.aggregator.client-id}")
|
||||
private String clientId;
|
||||
@Value("${sbz.aggregator.client-secret}")
|
||||
private String clientSecret;
|
||||
@Value("${steward.payment.processor.url}")
|
||||
private String url;
|
||||
@Value("${mpgs.return-url}")
|
||||
private String returnUrl;
|
||||
|
||||
@Override
|
||||
public Object process(Transaction transaction) throws Exception {
|
||||
logger.info("processing transaction");
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) {
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
||||
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
||||
return transaction;
|
||||
@@ -80,7 +68,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
|
||||
|
||||
if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) {
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
transaction.setResponseCode("02");
|
||||
transaction.setTargetUrl("/home");
|
||||
return transaction;
|
||||
}
|
||||
@@ -88,11 +76,6 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
|
||||
try {
|
||||
transaction = velocityPaymentService.pollPaymentStatus(transaction);
|
||||
|
||||
transaction.setStatus(Status.SUCCESS);
|
||||
transaction.setResponseCode("00");
|
||||
|
||||
transactionService.save(transaction);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
||||
@@ -102,7 +85,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
|
||||
transaction.setErrorMessage("Payment failed. Please try again");
|
||||
}
|
||||
|
||||
return transaction;
|
||||
return transactionService.save(transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package zw.qantra.tm.domain.services.processors.workflows;
|
||||
|
||||
import io.nflow.engine.workflow.curated.State;
|
||||
import io.nflow.engine.workflow.definition.NextAction;
|
||||
import io.nflow.engine.workflow.definition.StateExecution;
|
||||
import io.nflow.engine.workflow.definition.WorkflowDefinition;
|
||||
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import zw.qantra.tm.domain.enums.AuthType;
|
||||
import zw.qantra.tm.domain.models.GroupBatch;
|
||||
import zw.qantra.tm.domain.services.GroupBatchWorkflowService;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class BatchWorkflow extends WorkflowDefinition {
|
||||
public static final String TYPE = "batchWorkflow";
|
||||
|
||||
public static final State CONFIRM = new State("confirm", WorkflowStateType.start);
|
||||
public static final State CONFIRM_SUCCESS = new State("confirmSuccess", WorkflowStateType.manual);
|
||||
public static final State REQUEST = new State("request");
|
||||
public static final State REQUEST_SUCCESS = new State("requestSuccess", WorkflowStateType.manual);
|
||||
public static final State POLL = new State("poll");
|
||||
public static final State INTEGRATION = new State("integration");
|
||||
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
||||
public static final State DONE = new State("done", WorkflowStateType.end);
|
||||
|
||||
@Autowired
|
||||
private GroupBatchWorkflowService groupBatchWorkflowService;
|
||||
|
||||
public BatchWorkflow() {
|
||||
super(TYPE, CONFIRM, FAILED);
|
||||
|
||||
permit(CONFIRM, CONFIRM_SUCCESS, FAILED);
|
||||
permit(CONFIRM_SUCCESS, REQUEST);
|
||||
permit(REQUEST, REQUEST_SUCCESS, FAILED);
|
||||
permit(REQUEST_SUCCESS, POLL);
|
||||
permit(POLL, INTEGRATION, FAILED);
|
||||
permit(INTEGRATION, DONE, FAILED);
|
||||
}
|
||||
|
||||
public NextAction confirm(StateExecution execution) {
|
||||
try {
|
||||
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
||||
GroupBatch batch = groupBatchWorkflowService.processConfirm(batchId);
|
||||
execution.setVariable("batchStatus", batch.getStatus().name());
|
||||
return NextAction.moveToState(CONFIRM_SUCCESS, "Batch confirm complete");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return NextAction.moveToState(FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void confirmSuccess(StateExecution execution) {
|
||||
log.info("Batch confirm paused for request trigger");
|
||||
}
|
||||
|
||||
public NextAction request(StateExecution execution) {
|
||||
try {
|
||||
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
||||
String authTypeValue = execution.getVariable("authType", String.class);
|
||||
AuthType authType = authTypeValue == null ? AuthType.WEB : AuthType.valueOf(authTypeValue);
|
||||
GroupBatch batch = groupBatchWorkflowService.processRequest(batchId, authType);
|
||||
execution.setVariable("batchStatus", batch.getStatus().name());
|
||||
execution.setVariable("body", batch.getPaymentTransactionId());
|
||||
return NextAction.moveToState(REQUEST_SUCCESS, "Batch request complete");
|
||||
} catch (Exception e) {
|
||||
return NextAction.moveToState(FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void requestSuccess(StateExecution execution) {
|
||||
log.info("Batch request paused for poll trigger");
|
||||
}
|
||||
|
||||
public NextAction poll(StateExecution execution) {
|
||||
try {
|
||||
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
||||
GroupBatch batch = groupBatchWorkflowService.processPoll(batchId);
|
||||
execution.setVariable("batchStatus", batch.getStatus().name());
|
||||
return NextAction.moveToState(INTEGRATION, "Batch poll complete");
|
||||
} catch (Exception e) {
|
||||
return NextAction.moveToState(FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public NextAction integration(StateExecution execution) {
|
||||
try {
|
||||
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
||||
GroupBatch batch = groupBatchWorkflowService.processIntegration(batchId);
|
||||
execution.setVariable("batchStatus", batch.getStatus().name());
|
||||
return NextAction.moveToState(DONE, "Batch integration complete");
|
||||
} catch (Exception e) {
|
||||
return NextAction.moveToState(FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package zw.qantra.tm.domain.services.processors.workflows;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.nflow.engine.workflow.curated.State;
|
||||
import io.nflow.engine.workflow.definition.NextAction;
|
||||
import io.nflow.engine.workflow.definition.StateExecution;
|
||||
import io.nflow.engine.workflow.definition.WorkflowDefinition;
|
||||
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.joda.time.DateTime;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.handlers.HandlerInterface;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.handlers.WorkflowHandlerFactory;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class PurchaseInvoiceWorkflow extends WorkflowDefinition {
|
||||
@Autowired
|
||||
private WorkflowHandlerFactory workflowHandlerFactory;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public static final String TYPE = "purchaseInvoiceWorkflow";
|
||||
|
||||
public static final State PURCHASE_INVOICE = new State("purchaseInvoice", WorkflowStateType.start);
|
||||
public static final State PURCHASE_INVOICE_PAYMENT = new State("purchaseInvoicePayment");
|
||||
|
||||
public static final State TRAN_FAILED = new State("failed", WorkflowStateType.manual);
|
||||
public static final State DONE = new State("done", WorkflowStateType.end);
|
||||
|
||||
public PurchaseInvoiceWorkflow() {
|
||||
super(TYPE, PURCHASE_INVOICE, TRAN_FAILED);
|
||||
|
||||
permit(PURCHASE_INVOICE, PURCHASE_INVOICE_PAYMENT);
|
||||
permit(PURCHASE_INVOICE_PAYMENT, DONE);
|
||||
}
|
||||
|
||||
public NextAction purchaseInvoice(StateExecution execution) throws Exception {
|
||||
try {
|
||||
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
|
||||
byte[] json = objectMapper.writeValueAsBytes(map);
|
||||
Transaction transaction = objectMapper.readValue(json, Transaction.class);
|
||||
|
||||
HandlerInterface handlerInterface =
|
||||
(HandlerInterface) workflowHandlerFactory.getWorkflowHandler("purchaseInvoice");
|
||||
transaction = (Transaction) handlerInterface.process(transaction);
|
||||
|
||||
execution.setVariable("body", Utils.toJson(transaction));
|
||||
return NextAction.moveToState(PURCHASE_INVOICE_PAYMENT,
|
||||
"Purchase invoice successful - id: " + transaction.getErpPurchaseRef());
|
||||
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public NextAction purchaseInvoicePayment(StateExecution execution) {
|
||||
try {
|
||||
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
|
||||
byte[] json = objectMapper.writeValueAsBytes(map);
|
||||
Transaction transaction = objectMapper.readValue(json, Transaction.class);
|
||||
|
||||
HandlerInterface handlerInterface =
|
||||
(HandlerInterface) workflowHandlerFactory.getWorkflowHandler("purchaseInvoicePayment");
|
||||
transaction = (Transaction) handlerInterface.process(transaction);
|
||||
|
||||
execution.setVariable("body", Utils.toJson(transaction));
|
||||
|
||||
return NextAction.moveToState(DONE,
|
||||
"Purchase invoice payment generated - id: " + transaction.getErpPurchasePaymentRef());
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,8 +117,8 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
||||
// Generate OTP
|
||||
Otp otp = otpService.generateOtp(username, "REGISTRATION");
|
||||
|
||||
String string = String.format("Your Peak verification code is: %s", otp.getOtp());
|
||||
emailService.sendSimpleMessage(email, "Peak verification code", string);
|
||||
String string = String.format("Your Velocity Pay verification code is: %s", otp.getOtp());
|
||||
emailService.sendSimpleMessage(email, "Velocity Pay verification code", string);
|
||||
// LinkedHashMap response = restService.postAs(infobipUrl, string, headers, LinkedHashMap.class);
|
||||
|
||||
log.info("otp generated successfully: {}", otp.getOtp());
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package zw.qantra.tm.domain.services.processors.workflows;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.nflow.engine.workflow.curated.State;
|
||||
import io.nflow.engine.workflow.definition.NextAction;
|
||||
import io.nflow.engine.workflow.definition.StateExecution;
|
||||
import io.nflow.engine.workflow.definition.WorkflowDefinition;
|
||||
import io.nflow.engine.workflow.definition.WorkflowStateType;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.joda.time.DateTime;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import zw.qantra.tm.domain.models.Transaction;
|
||||
import zw.qantra.tm.domain.services.TransactionService;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.handlers.HandlerInterface;
|
||||
import zw.qantra.tm.domain.services.processors.workflows.handlers.WorkflowHandlerFactory;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SalesInvoiceWorkflow extends WorkflowDefinition {
|
||||
@Autowired
|
||||
private WorkflowHandlerFactory workflowHandlerFactory;
|
||||
@Autowired
|
||||
private TransactionService transactionService;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public static final String TYPE = "salesInvoiceWorkflow";
|
||||
|
||||
public static final State SALES_INVOICE = new State("salesInvoice", WorkflowStateType.start);
|
||||
public static final State JOURNAL_ENTRY = new State("journalEntry");
|
||||
public static final State SALES_PAYMENT = new State("salesPayment");
|
||||
|
||||
public static final State TRAN_FAILED = new State("failed", WorkflowStateType.manual);
|
||||
public static final State DONE = new State("done", WorkflowStateType.end);
|
||||
|
||||
SalesInvoiceWorkflow() {
|
||||
super(TYPE, SALES_INVOICE, TRAN_FAILED);
|
||||
|
||||
permit(SALES_INVOICE, JOURNAL_ENTRY);
|
||||
permit(JOURNAL_ENTRY, SALES_PAYMENT);
|
||||
permit(SALES_PAYMENT, DONE);
|
||||
}
|
||||
|
||||
public NextAction salesInvoice(StateExecution execution) {
|
||||
try {
|
||||
// cannot cast direct to Transaction object because of some jackson mapper hullabaloo :(
|
||||
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
|
||||
byte[] json = objectMapper.writeValueAsBytes(map);
|
||||
Transaction transaction = objectMapper.readValue(json, Transaction.class);
|
||||
|
||||
HandlerInterface handlerInterface =
|
||||
(HandlerInterface) workflowHandlerFactory.getWorkflowHandler("salesInvoice");
|
||||
transaction = (Transaction) handlerInterface.process(transaction);
|
||||
|
||||
execution.setVariable("body", Utils.toJson(transaction));
|
||||
return NextAction.moveToState(JOURNAL_ENTRY,
|
||||
"Sales invoice generated - id:" + transaction.getErpSalesRef());
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public NextAction journalEntry(StateExecution execution) {
|
||||
try {
|
||||
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
|
||||
byte[] json = objectMapper.writeValueAsBytes(map);
|
||||
Transaction transaction = objectMapper.readValue(json, Transaction.class);
|
||||
|
||||
HandlerInterface handlerInterface =
|
||||
(HandlerInterface) workflowHandlerFactory.getWorkflowHandler("journalEntry");
|
||||
transaction = (Transaction) handlerInterface.process(transaction);
|
||||
|
||||
execution.setVariable("body", Utils.toJson(transaction));
|
||||
return NextAction.moveToState(SALES_PAYMENT,
|
||||
"Journal Entry successful - id: " + transaction.getErpJournalRef());
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public NextAction salesPayment(StateExecution execution) {
|
||||
try {
|
||||
LinkedHashMap map = execution.getVariable("body", LinkedHashMap.class);
|
||||
byte[] json = objectMapper.writeValueAsBytes(map);
|
||||
Transaction transaction = objectMapper.readValue(json, Transaction.class);
|
||||
|
||||
HandlerInterface handlerInterface =
|
||||
(HandlerInterface) workflowHandlerFactory.getWorkflowHandler("salesPayment");
|
||||
transaction = (Transaction) handlerInterface.process(transaction);
|
||||
|
||||
execution.setVariable("body", Utils.toJson(transaction));
|
||||
|
||||
return NextAction.moveToState(DONE,
|
||||
"Sales payment successful - id: " + transaction.getErpSalesPaymentRef());
|
||||
}catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return NextAction.retryAfter(new DateTime().plusMinutes(1), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
package zw.qantra.tm.domain.services.processors.workflows;
|
||||
|
||||
import io.nflow.engine.service.WorkflowInstanceService;
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jobrunr.scheduling.BackgroundJob;
|
||||
import org.jobrunr.scheduling.cron.Cron;
|
||||
import org.joda.time.DateTime;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
import zw.qantra.tm.domain.services.JobService;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@@ -16,18 +17,12 @@ public class StartupApplicationListener implements ApplicationListener<ContextRe
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
private final WorkflowInstanceService workflowInstanceService;
|
||||
private final WorkflowInstanceFactory workflowInstanceFactory;
|
||||
private final JobService jobService;
|
||||
|
||||
@Override public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||
.setType(PollCronWorkflow.TYPE)
|
||||
.setExternalId("PollCronWorkflow")
|
||||
.setNextActivation(DateTime.now())
|
||||
.build();
|
||||
log.info("Background Job Enqueued: {}", DateTime.now());
|
||||
|
||||
long workflowId = workflowInstanceService.insertWorkflowInstance(instance);
|
||||
|
||||
log.info("Cron workflow ID {} started", workflowId);
|
||||
BackgroundJob.scheduleRecurrently(
|
||||
"orphan-transactions-manager", Cron.every5minutes(), jobService::checkOrphanedTransactions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package zw.qantra.tm.domain.services.processors.workflows;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.nflow.engine.service.WorkflowInstanceService;
|
||||
@@ -18,6 +16,7 @@ import zw.qantra.tm.utils.Utils;
|
||||
import static io.nflow.engine.service.WorkflowInstanceInclude.*;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WorkflowUtils {
|
||||
@@ -60,10 +59,21 @@ public class WorkflowUtils {
|
||||
}catch (IndexOutOfBoundsException exception){
|
||||
action = null;
|
||||
}
|
||||
|
||||
String bodyStr = instance.getStateVariable("body");
|
||||
Object body = bodyStr;
|
||||
try {
|
||||
Object parsed = Utils.fromJson(bodyStr, Map.class);
|
||||
if (parsed != null) {
|
||||
body = parsed;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Error parsing state variable 'body' as JSON: {}", e.getMessage());
|
||||
}
|
||||
return StateResponse.builder()
|
||||
.state(instance.state)
|
||||
.status(instance.status.toString())
|
||||
.body(Utils.fromJson(instance.getStateVariable("body"), Map.class))
|
||||
.body(body)
|
||||
.message(action != null ? action.stateText : null)
|
||||
.workflowId(instance.id.toString())
|
||||
.externalId(instance.externalId)
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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;
|
||||
@@ -28,12 +29,14 @@ public class CleanupHandler implements HandlerInterface {
|
||||
public Object process(Transaction transaction) throws Exception {
|
||||
// MAKE SURE IMPORTANT FIELDS ARE SET
|
||||
// =======================
|
||||
if(transaction.getType().equals(RequestType.CONFIRM)) {
|
||||
transaction.setTrace(Utils.getUid());
|
||||
transaction.setReference(UUID.randomUUID().toString());
|
||||
transaction.setConfirmationStatus(Status.PENDING);
|
||||
transaction.setPaymentStatus(Status.PENDING);
|
||||
transaction.setIntegrationStatus(Status.PENDING);
|
||||
transaction.setTrace(Utils.getUid());
|
||||
transaction.setReference(UUID.randomUUID().toString());
|
||||
transaction.setConfirmationStatus(Status.PENDING);
|
||||
transaction.setPaymentStatus(Status.PENDING);
|
||||
transaction.setIntegrationStatus(Status.PENDING);
|
||||
|
||||
if(transaction.getPartyType() == null) {
|
||||
transaction.setPartyType(PartyType.INDIVIDUAL);
|
||||
}
|
||||
|
||||
// UPDATE TRAN WITH CONFIRMATION HANDLER
|
||||
@@ -74,14 +77,12 @@ public class CleanupHandler implements HandlerInterface {
|
||||
String debitPhoneNumber = transaction.getDebitPhone();
|
||||
String creditPhoneNumber = transaction.getCreditPhone();
|
||||
|
||||
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
|
||||
|
||||
if(debitPhoneNumber != null && !debitPhoneNumber.isEmpty()){
|
||||
transaction.setDebitPhone(Utils.formatPhoneNumber(debitPhoneNumber, ""));
|
||||
transaction.setDebitPhone(Utils.formatPhoneNumber(debitPhoneNumber, transaction.getRegion()));
|
||||
}
|
||||
|
||||
if(creditPhoneNumber != null && !creditPhoneNumber.isEmpty()){
|
||||
transaction.setDebitPhone(Utils.formatPhoneNumber(creditPhoneNumber, "ZW"));
|
||||
transaction.setCreditAccount(Utils.formatPhoneNumber(creditPhoneNumber, "ZW"));
|
||||
}
|
||||
|
||||
return transaction;
|
||||
|
||||
@@ -38,7 +38,7 @@ public class GatewayPaymentHandler implements HandlerInterface {
|
||||
}
|
||||
|
||||
// if we've done this before then return
|
||||
if(transaction.getPaymentStatus().equals(Status.SUCCESS))
|
||||
if(Status.SUCCESS.equals(transaction.getPaymentStatus()))
|
||||
return transaction;
|
||||
|
||||
transactionService.save(transaction);
|
||||
|
||||
@@ -36,10 +36,10 @@ public class IntegrationHandler implements HandlerInterface {
|
||||
transaction.setType(RequestType.INTEGRATION);
|
||||
|
||||
// if we've done this before then return
|
||||
if(transaction.getIntegrationStatus().equals(Status.SUCCESS))
|
||||
if(Status.SUCCESS.equals(transaction.getIntegrationStatus()))
|
||||
return transaction;
|
||||
|
||||
if(!transaction.getPollingStatus().equals(Status.SUCCESS))
|
||||
if(!Status.SUCCESS.equals(transaction.getPollingStatus()))
|
||||
throw new ApiException("Transaction failed: Polling is not yet complete" );
|
||||
|
||||
// prevent race condition
|
||||
@@ -106,8 +106,8 @@ public class IntegrationHandler implements HandlerInterface {
|
||||
lock.add(uid);
|
||||
}
|
||||
|
||||
private boolean removeLock(String uid) {
|
||||
return lock.remove(uid);
|
||||
private void removeLock(String uid) {
|
||||
lock.remove(uid);
|
||||
}
|
||||
|
||||
private boolean isLocked(String uid) {
|
||||
|
||||
@@ -39,34 +39,35 @@ public class ChargeSeeder implements Seeder {
|
||||
}
|
||||
|
||||
// Ecocash Gateway Fee (1%)
|
||||
if (chargeRepository.existsByChargeLabelAndCurrencyAndPercentageRate(
|
||||
ChargeLabelEnum.GATEWAY_FEE, CurrencyType.USD, new BigDecimal("1"))) {
|
||||
log.info("Charge 'Gateway fee (1%)' already exists, skipping.");
|
||||
if (chargeRepository.existsByChargeLabelAndCurrencyAndDescription(
|
||||
ChargeLabelEnum.GATEWAY_FEE, CurrencyType.USD, "Ecocash Gateway fee")) {
|
||||
log.info("Charge 'Ecocash Gateway fee (1%)' already exists, skipping.");
|
||||
} else {
|
||||
chargeRepository.save(Charge.builder()
|
||||
.description("Gateway fee")
|
||||
.description("Ecocash Gateway fee")
|
||||
.chargeLabel(ChargeLabelEnum.GATEWAY_FEE)
|
||||
.currency(CurrencyType.USD)
|
||||
.percentageRate(new BigDecimal("1"))
|
||||
.min(new BigDecimal("0.01"))
|
||||
.includes(new HashSet<>(Arrays.asList(ecocashCondition)))
|
||||
.build());
|
||||
log.info("Charge 'Gateway fee (1%)' seeded.");
|
||||
log.info("Charge 'Ecocash Gateway fee (1%)' seeded.");
|
||||
}
|
||||
|
||||
// MPGS Gateway Fee (2%, min 0.55)
|
||||
if (chargeRepository.existsByChargeLabelAndCurrencyAndPercentageRate(
|
||||
ChargeLabelEnum.GATEWAY_FEE, CurrencyType.USD, new BigDecimal("2"))) {
|
||||
log.info("Charge 'Gateway fee (2%)' already exists, skipping.");
|
||||
if (chargeRepository.existsByChargeLabelAndCurrencyAndDescription(
|
||||
ChargeLabelEnum.GATEWAY_FEE, CurrencyType.USD, "MPGS Gateway fee")) {
|
||||
log.info("Charge 'MPGS Gateway fee (1% min 0.50)' already exists, skipping.");
|
||||
} else {
|
||||
chargeRepository.save(Charge.builder()
|
||||
.description("Gateway fee")
|
||||
.description("MPGS Gateway fee")
|
||||
.chargeLabel(ChargeLabelEnum.GATEWAY_FEE)
|
||||
.currency(CurrencyType.USD)
|
||||
.percentageRate(new BigDecimal("1"))
|
||||
.min(new BigDecimal("0.20"))
|
||||
.min(new BigDecimal("0.50"))
|
||||
.includes(new HashSet<>(Arrays.asList(mpgsCondition)))
|
||||
.build());
|
||||
log.info("Charge 'Gateway fee (2%)' seeded.");
|
||||
log.info("Charge 'MPGS Gateway fee (1% min 0.50)' seeded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/main/java/zw/qantra/tm/seeders/CurrencySeeder.java
Normal file
53
src/main/java/zw/qantra/tm/seeders/CurrencySeeder.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package zw.qantra.tm.seeders;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import zw.qantra.tm.domain.models.Currency;
|
||||
import zw.qantra.tm.domain.repositories.CurrencyRepository;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Seeds Currency entities.
|
||||
* Priority: Should run early before providers (currencies are referenced by providers).
|
||||
* Only seeds entities that don't already exist (checked by isoCode).
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CurrencySeeder implements Seeder {
|
||||
|
||||
private final CurrencyRepository currencyRepository;
|
||||
|
||||
@Override
|
||||
public void seed() {
|
||||
List<Currency> currencies = Arrays.asList(
|
||||
Currency.builder()
|
||||
.isoCode("USD")
|
||||
.name("USD")
|
||||
.displayName("USD")
|
||||
.bankRate("1")
|
||||
.forecastRate("1")
|
||||
.build(),
|
||||
Currency.builder()
|
||||
.isoCode("ZWG")
|
||||
.name("ZWG")
|
||||
.displayName("ZWG")
|
||||
.bankRate("26.5")
|
||||
.forecastRate("31")
|
||||
.build()
|
||||
);
|
||||
|
||||
for (Currency currency : currencies) {
|
||||
if (currencyRepository.findByIsoCode(currency.getIsoCode()).isPresent()) {
|
||||
log.info("Currency '{}' already exists, skipping.", currency.getIsoCode());
|
||||
} else {
|
||||
currencyRepository.save(currency);
|
||||
log.info("Currency '{}' seeded.", currency.getIsoCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,21 @@ import org.springframework.context.annotation.Configuration;
|
||||
* Orchestrates all seeders in the correct priority order matching
|
||||
* ConfigController.seedLiveConfig() execution sequence:
|
||||
* <p>
|
||||
* 1. PaymentProcessorSeeder
|
||||
* 2. IntegrationProcessorSeeder
|
||||
* 3. CategorySeeder
|
||||
* 4. ProviderSeeder
|
||||
* 5. ChargeConditionSeeder
|
||||
* 6. ChargeSeeder
|
||||
* 7. SettingSeeder
|
||||
* 1. CurrencySeeder
|
||||
* 2. PaymentProcessorSeeder
|
||||
* 3. IntegrationProcessorSeeder
|
||||
* 4. CategorySeeder
|
||||
* 5. ProviderSeeder
|
||||
* 6. ChargeConditionSeeder
|
||||
* 7. ChargeSeeder
|
||||
* 8. SettingSeeder
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class LiveConfigSeeder {
|
||||
|
||||
private final CurrencySeeder currencySeeder;
|
||||
private final PaymentProcessorSeeder paymentProcessorSeeder;
|
||||
private final IntegrationProcessorSeeder integrationProcessorSeeder;
|
||||
private final CategorySeeder categorySeeder;
|
||||
@@ -39,6 +41,9 @@ public class LiveConfigSeeder {
|
||||
public void seedAll() {
|
||||
log.info("Starting live configuration seeding...");
|
||||
|
||||
log.info("Seeding Currencies...");
|
||||
currencySeeder.seed();
|
||||
|
||||
log.info("Seeding Payment Processors...");
|
||||
paymentProcessorSeeder.seed();
|
||||
|
||||
@@ -62,4 +67,4 @@ public class LiveConfigSeeder {
|
||||
|
||||
log.info("Live configuration seeding complete.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,16 @@ public class PaymentProcessorSeeder implements Seeder {
|
||||
@Override
|
||||
public void seed() {
|
||||
List<PaymentProcessor> processors = Arrays.asList(
|
||||
PaymentProcessor.builder()
|
||||
.name("CityWallet")
|
||||
.label("WALLET")
|
||||
.type("GATEWAY")
|
||||
.image("city.png")
|
||||
.accountFieldName("phoneNumber")
|
||||
.accountFieldLabel("Wallet Phone Number")
|
||||
.description("Pay using your CityWallet")
|
||||
.authType("REMOTE")
|
||||
.build(),
|
||||
PaymentProcessor.builder()
|
||||
.name("Ecocash")
|
||||
.label("ECOCASH")
|
||||
|
||||
@@ -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.Provider;
|
||||
import zw.qantra.tm.domain.repositories.ProviderRepository;
|
||||
import zw.qantra.tm.utils.Utils;
|
||||
@@ -29,6 +30,7 @@ public class ProviderSeeder implements Seeder {
|
||||
.name("Econet Airtime")
|
||||
.description("Econet Airtime")
|
||||
.clientId("econet_airtime")
|
||||
.currency(CurrencyType.USD)
|
||||
.label("ECONET")
|
||||
.category("AIRTIME")
|
||||
.image("econet.png")
|
||||
@@ -47,6 +49,7 @@ public class ProviderSeeder implements Seeder {
|
||||
.name("NetOne Airtime")
|
||||
.description("NetOne Airtime")
|
||||
.clientId("netone_usd_airtime")
|
||||
.currency(CurrencyType.USD)
|
||||
.label("NETONE")
|
||||
.category("AIRTIME")
|
||||
.image("netone.png")
|
||||
@@ -61,10 +64,30 @@ public class ProviderSeeder implements Seeder {
|
||||
.priority(3)
|
||||
.meta("{\"hotProductId\": \"35\"}")
|
||||
.build(),
|
||||
Provider.builder()
|
||||
.name("Zesa Prepaid")
|
||||
.description("Prepaid Zesa")
|
||||
.clientId("zesa_prepaid_zwg")
|
||||
.currency(CurrencyType.ZWG)
|
||||
.label("ZESA")
|
||||
.category("UTILITIES")
|
||||
.image("zesa.png")
|
||||
.externalId("5468b1ed-4e11-4348-9bc1-f422890fc2c2")
|
||||
.integrationProcessorLabel("HOT")
|
||||
.accountFieldName("Meter Number")
|
||||
.percentageCommission(1.5)
|
||||
.requiresAccount(true)
|
||||
.requiresAmount(true)
|
||||
.requiresPhone(true)
|
||||
.priority(1)
|
||||
.meta("{\"hotProductId\": \"24\"}")
|
||||
.uid(Utils.getUid())
|
||||
.build(),
|
||||
Provider.builder()
|
||||
.name("Zesa Prepaid")
|
||||
.description("Prepaid Zesa")
|
||||
.clientId("zesa_prepaid_usd")
|
||||
.currency(CurrencyType.USD)
|
||||
.label("ZESA")
|
||||
.category("UTILITIES")
|
||||
.image("zesa.png")
|
||||
@@ -83,6 +106,7 @@ public class ProviderSeeder implements Seeder {
|
||||
.name("Econet Bundles")
|
||||
.description("Econet Bundles")
|
||||
.clientId("econet_bundles_usd")
|
||||
.currency(CurrencyType.USD)
|
||||
.label("ECONET_BUNDLES")
|
||||
.category("AIRTIME")
|
||||
.image("econet.png")
|
||||
|
||||
@@ -10,11 +10,13 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
public class Utils {
|
||||
|
||||
public static String formatPhoneNumber(String phoneNumber, String region) {
|
||||
@@ -76,7 +78,7 @@ public class Utils {
|
||||
try {
|
||||
obj = mapper.readValue(json, clazz);
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
log.debug("Error deserializing JSON: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return (T) obj;
|
||||
|
||||
@@ -38,9 +38,20 @@ powertel.clientid=powertel_zesa
|
||||
ecocash.apikey=OUzGezwTgktLg_9v0I48I6WUM4LVLGFl
|
||||
ecocash.url=https://developers.ecocash.co.zw/api/ecocash_pay/api/v2/payment/instant/c2b/sandbox
|
||||
|
||||
velocity.api.base-url=https://api.velocityafrica.net
|
||||
velocity.api.api-key=z5WdSlItIhYL6nd7rxzf-jFdKN3jJyL1LubhDFBvXmc
|
||||
#velocity.api.base-url=http://localhost:9005
|
||||
#velocity.api.api-key=gH13nirZdUQdqC9impgzTzqEqv-jdbLOTx_HD5fQr_A
|
||||
#velocity.api.base-url=https://api.velocityafrica.net
|
||||
#velocity.api.api-key=z5WdSlItIhYL6nd7rxzf-jFdKN3jJyL1LubhDFBvXmc
|
||||
velocity.api.base-url=http://localhost:6975
|
||||
velocity.api.api-key=gH13nirZdUQdqC9impgzTzqEqv-jdbLOTx_HD5fQr_A
|
||||
velocity.api.cancel-url=http://localhost:9005/poll
|
||||
velocity.api.success-url=http://localhost:9005/poll
|
||||
velocity.api.success-url=http://localhost:9005/poll
|
||||
|
||||
jobrunr.background-job-server.enabled=true
|
||||
jobrunr.dashboard.enabled=true
|
||||
jobrunr.dashboard.port=8058
|
||||
jobrunr.database.datasource=jobrunrDataSource
|
||||
jobrunr.datasource.url=jdbc:h2:file:./data/jobrunr;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE
|
||||
jobrunr.datasource.driver-class-name=org.h2.Driver
|
||||
jobrunr.datasource.username=sa
|
||||
jobrunr.datasource.password=
|
||||
|
||||
app.base-url=http://localhost:6950
|
||||
|
||||
@@ -39,4 +39,13 @@ ecocash.apikey=OUzGezwTgktLg_9v0I48I6WUM4LVLGFl
|
||||
ecocash.url=https://developers.ecocash.co.zw/api/ecocash_pay/api/v2/payment/instant/c2b/sandbox
|
||||
|
||||
velocity.api.cancel-url=http://localhost:9005/poll
|
||||
velocity.api.success-url=http://localhost:9005/poll
|
||||
velocity.api.success-url=http://localhost:9005/poll
|
||||
|
||||
jobrunr.background-job-server.enabled=true
|
||||
jobrunr.dashboard.enabled=true
|
||||
jobrunr.dashboard.port=8058
|
||||
jobrunr.database.datasource=jobrunrDataSource
|
||||
jobrunr.datasource.url=jdbc:h2:file:./data/jobrunr;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE
|
||||
jobrunr.datasource.driver-class-name=org.h2.Driver
|
||||
jobrunr.datasource.username=sa
|
||||
jobrunr.datasource.password=
|
||||
|
||||
@@ -28,7 +28,7 @@ sbz.aggregator.encryption-key=91b5b3c7-860f-473c-ac5c-b12d9121819b
|
||||
|
||||
steward.payment.processor.url=${sbz.merchant.url}
|
||||
|
||||
mpgs.return-url=https://vdt.co.zw/#/return
|
||||
mpgs.return-url=https://pay.velocityafrica.net/return
|
||||
|
||||
# JWT Configuration
|
||||
jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-production
|
||||
@@ -68,5 +68,19 @@ hot.cache.duration.minutes=5
|
||||
|
||||
velocity.api.base-url=https://api.velocityafrica.net
|
||||
velocity.api.api-key=cHE08ADmiATtf8VYgA59-wKVKVKj2WusKDAaaRLDsG8
|
||||
velocity.api.cancel-url=https://pay.velocityafrica/poll
|
||||
velocity.api.success-url=https://pay.velocityafrica/poll
|
||||
velocity.api.cancel-url=https://pay.velocityafrica.net/poll
|
||||
velocity.api.success-url=https://pay.velocityafrica.net/poll
|
||||
|
||||
jobrunr.background-job-server.enabled=true
|
||||
jobrunr.dashboard.enabled=true
|
||||
jobrunr.dashboard.port=8058
|
||||
jobrunr.database.datasource=jobrunrDataSource
|
||||
jobrunr.datasource.url=jdbc:h2:file:./data/jobrunr;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE
|
||||
jobrunr.datasource.driver-class-name=org.h2.Driver
|
||||
jobrunr.datasource.username=sa
|
||||
jobrunr.datasource.password=
|
||||
|
||||
# Batch report storage
|
||||
app.reports.storage-path=./reports
|
||||
app.base-url=https://pay.velocityafrica.net
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
<?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 (generated)" id="1780061546055-17">
|
||||
<addColumn tableName="transaction">
|
||||
<column name="order_id" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-18">
|
||||
<addColumn tableName="transaction">
|
||||
<column name="order_trace" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-19">
|
||||
<addColumn tableName="transaction">
|
||||
<column name="order_transaction_trace" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-20">
|
||||
<dropColumn columnName="version" tableName="transaction"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-1">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="additional_data"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-2">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="category"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-3">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="charge"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-4">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="charge_condition"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-5">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="currency"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-6">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="integration_processor"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-7">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="otp"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-8">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="payment_processor"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-9">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="provider"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-10">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="recipient"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-11">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="setting"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-12">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="transaction"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-13">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="transaction_event"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-14">
|
||||
<addDefaultValue columnDataType="boolean" columnName="deleted" defaultValueBoolean="false" tableName="users"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-15">
|
||||
<dropUniqueConstraint constraintName="UC_USERSUSERNAME_COL" tableName="users"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780061546055-16">
|
||||
<addUniqueConstraint columnNames="username" constraintName="UC_USERSUSERNAME_COL" tableName="users"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780062553770-3">
|
||||
<addColumn tableName="provider">
|
||||
<column name="priority" type="integer"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780062553770-1">
|
||||
<dropUniqueConstraint constraintName="UC_USERSUSERNAME_COL" tableName="users"/>
|
||||
</changeSet>
|
||||
<changeSet author="khoza (generated)" id="1780062553770-2">
|
||||
<addUniqueConstraint columnNames="username" constraintName="UC_USERSUSERNAME_COL" tableName="users"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
@@ -1,7 +1,7 @@
|
||||
changeLogFile=src/main/resources/liquibase-diff-changeLog.xml
|
||||
url=jdbc:postgresql://localhost:5432/qpay?serverTimezone=Africa/Harare&useLegacyDatetimeCode=false
|
||||
username=postgres
|
||||
password=zdDZMzq6F4B4L1IUl
|
||||
password=example
|
||||
|
||||
driver=org.postgresql.Driver
|
||||
referenceUrl=hibernate:spring:zw.qantra.tm.domain.models?dialect=org.hibernate.dialect.PostgreSQLDialect&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
|
||||
|
||||
Reference in New Issue
Block a user