Compare commits
30 Commits
main
...
d53a427a5a
| Author | SHA1 | Date | |
|---|---|---|---|
| d53a427a5a | |||
| 525f466541 | |||
| b983a9c301 | |||
| 858639987b | |||
| a2c04e4139 | |||
| 443e8fd21b | |||
| ee594cb276 | |||
| e634ff11c7 | |||
| 1e261dc3fe | |||
| f5cc3a7bfa | |||
| 5d923b954e | |||
| 545ce6c704 | |||
| 51259aab33 | |||
| 09165b64e5 | |||
| 8ff6e5f8ea | |||
| dc0c42d12e | |||
| db1fe2672a | |||
| a84a1000a8 | |||
| 299cdf6e35 | |||
| 598287b0cc | |||
|
|
caf4e4bc89 | ||
|
|
a367acd81d | ||
|
|
4d0e18c1a6 | ||
|
|
dcb917e899 | ||
|
|
6c3ef60665 | ||
|
|
6ccded7fce | ||
|
|
0eda74414e | ||
|
|
5d3c9a46a3 | ||
|
|
573e4b154c | ||
|
|
c6ce11026c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ build/
|
|||||||
.vscode/
|
.vscode/
|
||||||
logs/
|
logs/
|
||||||
data/
|
data/
|
||||||
|
reports/
|
||||||
@@ -2,6 +2,13 @@ FROM 144.91.121.112:8082/java-17
|
|||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install fontconfig and fonts for Apache POI XSSFWorkbook font rendering
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
fontconfig \
|
||||||
|
fonts-dejavu \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mvn clean compile -P production package
|
RUN mvn clean compile -P production package
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
||||||
CMD ["java", "-Dspring.profiles.active=nflow.db.postgresql", "-jar", "/app/target/tm-0.0.1-RELEASE.jar"]
|
CMD ["java", "-Djava.awt.headless=true", "-Dspring.profiles.active=nflow.db.postgresql", "-jar", "/app/target/tm-0.0.1-RELEASE.jar"]
|
||||||
|
|||||||
12
pom.xml
12
pom.xml
@@ -83,11 +83,6 @@
|
|||||||
<artifactId>jakarta.persistence-api</artifactId>
|
<artifactId>jakarta.persistence-api</artifactId>
|
||||||
<version>3.1.0</version>
|
<version>3.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.googlecode.libphonenumber</groupId>
|
|
||||||
<artifactId>libphonenumber</artifactId>
|
|
||||||
<version>8.12.10</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.code.gson</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>gson</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
@@ -167,12 +162,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.googlecode.libphonenumber</groupId>
|
<groupId>com.googlecode.libphonenumber</groupId>
|
||||||
<artifactId>libphonenumber</artifactId>
|
<artifactId>libphonenumber</artifactId>
|
||||||
<version>8.13.36</version> <!-- Use the latest version -->
|
<version>8.13.36</version>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-mail</artifactId>
|
|
||||||
<version>3.1.5</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- Maven -->
|
<!-- Maven -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
package zw.qantra.tm;
|
package zw.qantra.tm;
|
||||||
|
|
||||||
import io.nflow.rest.config.RestConfiguration;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import org.springframework.context.annotation.Import;
|
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@Import(RestConfiguration.class)
|
|
||||||
@EnableCaching
|
@EnableCaching
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
public class TmApplication {
|
public class TmApplication {
|
||||||
@@ -24,4 +23,14 @@ public class TmApplication {
|
|||||||
public Gson gson() {
|
public Gson gson() {
|
||||||
return new Gson();
|
return new Gson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally imports nFlow REST configuration only when NOT running the "test" profile.
|
||||||
|
* This prevents nFlow's PostgreSQL-dependent beans from loading in tests that use H2.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Profile("!test")
|
||||||
|
@org.springframework.context.annotation.Import(io.nflow.rest.config.RestConfiguration.class)
|
||||||
|
static class NflowRestConfig {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,13 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@Profile("!test")
|
||||||
public class NflowDatabaseConfig extends PgDatabaseConfiguration {
|
public class NflowDatabaseConfig extends PgDatabaseConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/test/**").permitAll()
|
.requestMatchers("/test/**").permitAll()
|
||||||
.requestMatchers("/nflow/**").permitAll()
|
.requestMatchers("/nflow/**").permitAll()
|
||||||
.requestMatchers("/auth/**").permitAll()
|
.requestMatchers("/auth/**").permitAll()
|
||||||
.requestMatchers("/public/**").permitAll()
|
.requestMatchers("/public/**").permitAll()
|
||||||
|
.requestMatchers("/explorer/**").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.authenticationProvider(authenticationProvider(userService))
|
.authenticationProvider(authenticationProvider(userService))
|
||||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
|
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
.formLogin(withDefaults());
|
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import zw.qantra.tm.domain.dtos.erp.VelocityAccountDto;
|
import zw.qantra.tm.domain.dtos.erp.VelocityAccountDto;
|
||||||
import zw.qantra.tm.domain.dtos.erp.VelocityStatementDto;
|
import zw.qantra.tm.domain.dtos.erp.VelocityStatementDto;
|
||||||
|
import zw.qantra.tm.domain.models.Workspace;
|
||||||
import zw.qantra.tm.domain.services.VelocityAccountService;
|
import zw.qantra.tm.domain.services.VelocityAccountService;
|
||||||
|
import zw.qantra.tm.domain.services.WorkspaceService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/accounts")
|
@RequestMapping("/accounts")
|
||||||
@@ -14,31 +19,39 @@ import zw.qantra.tm.domain.services.VelocityAccountService;
|
|||||||
public class AccountController {
|
public class AccountController {
|
||||||
|
|
||||||
private final VelocityAccountService velocityAccountService;
|
private final VelocityAccountService velocityAccountService;
|
||||||
|
private final WorkspaceService workspaceService;
|
||||||
|
|
||||||
@GetMapping("/phone/{currencyPhoneConcat}")
|
@GetMapping("/{workspaceId}/balance/{currency}")
|
||||||
@PreAuthorize("hasRole('ACCOUNT_READ')")
|
public ResponseEntity getAccount(
|
||||||
public ResponseEntity<VelocityAccountDto> getAccountByPhone(
|
@PathVariable UUID workspaceId,
|
||||||
@PathVariable String currencyPhoneConcat) {
|
@PathVariable String currency
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
|
Workspace workspace = workspaceService.getWorkspace(workspaceId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Workspace not found"));
|
||||||
|
String currencyPhoneConcat = currency + workspace.getPhone();
|
||||||
VelocityAccountDto account = velocityAccountService.getAccountByPhone(currencyPhoneConcat);
|
VelocityAccountDto account = velocityAccountService.getAccountByPhone(currencyPhoneConcat);
|
||||||
return ResponseEntity.ok(account);
|
return ResponseEntity.ok(account);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/phone/{currencyPhoneConcat}/statement")
|
@GetMapping("/{workspaceId}/statement/{currency}")
|
||||||
@PreAuthorize("hasRole('ACCOUNT_READ')")
|
public ResponseEntity getStatement(
|
||||||
public ResponseEntity<VelocityStatementDto> getStatementByPhone(
|
@PathVariable UUID workspaceId,
|
||||||
@PathVariable String currencyPhoneConcat,
|
@PathVariable String currency,
|
||||||
@RequestParam String startDate,
|
@RequestParam String startDate,
|
||||||
@RequestParam String endDate) {
|
@RequestParam String endDate) {
|
||||||
try {
|
try {
|
||||||
|
Workspace workspace = workspaceService.getWorkspace(workspaceId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Workspace not found"));
|
||||||
|
String currencyPhoneConcat = currency + workspace.getPhone();
|
||||||
VelocityStatementDto statement = velocityAccountService.getStatementByPhone(
|
VelocityStatementDto statement = velocityAccountService.getStatementByPhone(
|
||||||
currencyPhoneConcat, startDate, endDate);
|
currencyPhoneConcat, startDate, endDate);
|
||||||
return ResponseEntity.ok(statement);
|
return ResponseEntity.ok(statement);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package zw.qantra.tm.domain.controllers;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import zw.qantra.tm.domain.models.AppVersion;
|
||||||
|
import zw.qantra.tm.domain.services.AppVersionService;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/public/app-version")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppVersionController {
|
||||||
|
|
||||||
|
private final AppVersionService appVersionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client checks if a newer app build is available.
|
||||||
|
* Pass the current build number as a query param.
|
||||||
|
*/
|
||||||
|
@GetMapping("/check/app")
|
||||||
|
public ResponseEntity<AppVersion> checkAppUpdate(@RequestParam Integer currentBuildNumber) {
|
||||||
|
Optional<AppVersion> update = appVersionService.checkForAppUpdate(currentBuildNumber);
|
||||||
|
return update.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.noContent().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client checks if a newer web build is available.
|
||||||
|
* Pass the current build number as a query param.
|
||||||
|
*/
|
||||||
|
@GetMapping("/check/web")
|
||||||
|
public ResponseEntity<AppVersion> checkWebUpdate(@RequestParam Integer currentBuildNumber) {
|
||||||
|
Optional<AppVersion> update = appVersionService.checkForWebUpdate(currentBuildNumber);
|
||||||
|
return update.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.noContent().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the latest version record.
|
||||||
|
*/
|
||||||
|
@GetMapping("/latest")
|
||||||
|
public ResponseEntity<AppVersion> getLatestVersion() {
|
||||||
|
Optional<AppVersion> latest = appVersionService.getLatestVersion();
|
||||||
|
return latest.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.noContent().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist a new version record (internal/admin use).
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<AppVersion> save(@RequestBody AppVersion appVersion) {
|
||||||
|
return ResponseEntity.ok(appVersionService.save(appVersion));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package zw.qantra.tm.domain.controllers;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import zw.qantra.tm.domain.dtos.CloudflarePurgeResponse;
|
||||||
|
import zw.qantra.tm.domain.services.CloudflareCacheService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/public/cache/cloudflare")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CloudflareCacheController {
|
||||||
|
|
||||||
|
private final CloudflareCacheService cloudflareCacheService;
|
||||||
|
|
||||||
|
@PostMapping("/clear")
|
||||||
|
public ResponseEntity<CloudflarePurgeResponse> clearCache() {
|
||||||
|
log.info("Received request to clear Cloudflare cache");
|
||||||
|
CloudflarePurgeResponse response = cloudflareCacheService.purgeEverything();
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package zw.qantra.tm.domain.controllers;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
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.And;
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@@ -9,6 +10,7 @@ import org.springframework.data.jpa.domain.Specification;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
|
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
|
||||||
|
import zw.qantra.tm.domain.dtos.GroupBatchItemUpdateRequest;
|
||||||
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
|
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
|
||||||
import zw.qantra.tm.domain.dtos.GroupBatchTriggerRequest;
|
import zw.qantra.tm.domain.dtos.GroupBatchTriggerRequest;
|
||||||
import zw.qantra.tm.domain.models.GroupBatch;
|
import zw.qantra.tm.domain.models.GroupBatch;
|
||||||
@@ -16,11 +18,12 @@ import zw.qantra.tm.domain.models.GroupBatchItem;
|
|||||||
import zw.qantra.tm.domain.services.BatchReportService;
|
import zw.qantra.tm.domain.services.BatchReportService;
|
||||||
import zw.qantra.tm.domain.services.GroupBatchService;
|
import zw.qantra.tm.domain.services.GroupBatchService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/public/group-batches")
|
@RequestMapping("/group-batches")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GroupBatchController {
|
public class GroupBatchController {
|
||||||
private final GroupBatchService groupBatchService;
|
private final GroupBatchService groupBatchService;
|
||||||
@@ -30,7 +33,7 @@ public class GroupBatchController {
|
|||||||
public ResponseEntity findAll(
|
public ResponseEntity findAll(
|
||||||
@And({
|
@And({
|
||||||
@Spec(path = "recipientGroupId", defaultVal = "null", spec = Equal.class),
|
@Spec(path = "recipientGroupId", defaultVal = "null", spec = Equal.class),
|
||||||
@Spec(path = "userId", spec = Equal.class),
|
@Spec(path = "workspaceId", spec = Equal.class),
|
||||||
@Spec(path = "status", spec = Equal.class),
|
@Spec(path = "status", spec = Equal.class),
|
||||||
@Spec(path = "currency", spec = Equal.class),
|
@Spec(path = "currency", spec = Equal.class),
|
||||||
})Specification<GroupBatch> spec, Pageable pageable){
|
})Specification<GroupBatch> spec, Pageable pageable){
|
||||||
@@ -54,7 +57,7 @@ public class GroupBatchController {
|
|||||||
@RequestBody GroupBatchTriggerRequest request) {
|
@RequestBody GroupBatchTriggerRequest request) {
|
||||||
return ResponseEntity.ok(groupBatchService.triggerConfirm(
|
return ResponseEntity.ok(groupBatchService.triggerConfirm(
|
||||||
batchId,
|
batchId,
|
||||||
request.getUserId(),
|
request.getWorkspaceId(),
|
||||||
request.getIdempotencyKey()));
|
request.getIdempotencyKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +66,7 @@ public class GroupBatchController {
|
|||||||
@RequestBody GroupBatchTriggerRequest request) {
|
@RequestBody GroupBatchTriggerRequest request) {
|
||||||
Object response = groupBatchService.triggerRequest(
|
Object response = groupBatchService.triggerRequest(
|
||||||
batchId,
|
batchId,
|
||||||
request.getUserId(),
|
request.getWorkspaceId(),
|
||||||
request.getAuthType(),
|
request.getAuthType(),
|
||||||
request.getIdempotencyKey());
|
request.getIdempotencyKey());
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
@@ -74,14 +77,14 @@ public class GroupBatchController {
|
|||||||
@RequestBody GroupBatchTriggerRequest request) {
|
@RequestBody GroupBatchTriggerRequest request) {
|
||||||
return ResponseEntity.ok(groupBatchService.triggerPoll(
|
return ResponseEntity.ok(groupBatchService.triggerPoll(
|
||||||
batchId,
|
batchId,
|
||||||
request.getUserId(),
|
request.getWorkspaceId(),
|
||||||
request.getIdempotencyKey()));
|
request.getIdempotencyKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{batchId}")
|
@GetMapping("/{batchId}")
|
||||||
public ResponseEntity<GroupBatch> getBatch(@PathVariable UUID batchId,
|
public ResponseEntity<GroupBatch> getBatch(@PathVariable UUID batchId,
|
||||||
@RequestParam String userId) {
|
@RequestParam UUID workspaceId) {
|
||||||
return ResponseEntity.ok(groupBatchService.getBatch(batchId, userId));
|
return ResponseEntity.ok(groupBatchService.getBatch(batchId, workspaceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/items")
|
@GetMapping("/items")
|
||||||
@@ -89,21 +92,34 @@ public class GroupBatchController {
|
|||||||
@And({
|
@And({
|
||||||
@Spec(path = "groupBatchId", defaultVal = "null", spec = Equal.class),
|
@Spec(path = "groupBatchId", defaultVal = "null", spec = Equal.class),
|
||||||
@Spec(path = "recipientId", spec = Equal.class),
|
@Spec(path = "recipientId", spec = Equal.class),
|
||||||
|
@Spec(path = "recipientName", spec = EqualIgnoreCase.class),
|
||||||
|
@Spec(path = "recipientAccount", spec = Equal.class),
|
||||||
|
@Spec(path = "status", spec = Equal.class),
|
||||||
}) Specification<GroupBatchItem> spec, Pageable pageable) {
|
}) Specification<GroupBatchItem> spec, Pageable pageable) {
|
||||||
return ResponseEntity.ok(groupBatchService.getBatchItems(spec, pageable));
|
return ResponseEntity.ok(groupBatchService.getBatchItems(spec, pageable));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{batchId}/items")
|
||||||
|
public ResponseEntity<List<GroupBatchItem>> updateBatchItems(
|
||||||
|
@PathVariable UUID batchId,
|
||||||
|
@RequestBody GroupBatchItemUpdateRequest.ListWrapper wrapper) {
|
||||||
|
return ResponseEntity.ok(groupBatchService.updateBatchItems(
|
||||||
|
batchId,
|
||||||
|
wrapper.getWorkspaceId(),
|
||||||
|
wrapper.getItems()));
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{batchId}")
|
@DeleteMapping("/{batchId}")
|
||||||
public ResponseEntity<Void> deleteBatch(@PathVariable UUID batchId,
|
public ResponseEntity<Void> deleteBatch(@PathVariable UUID batchId,
|
||||||
@RequestParam String userId) {
|
@RequestParam UUID workspaceId) {
|
||||||
groupBatchService.deleteBatch(batchId, userId);
|
groupBatchService.deleteBatch(batchId, workspaceId);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{batchId}/report")
|
@GetMapping("/{batchId}/report")
|
||||||
public ResponseEntity<Map<String, Object>> downloadBatchReport(@PathVariable UUID batchId,
|
public ResponseEntity<Map<String, Object>> downloadBatchReport(@PathVariable UUID batchId,
|
||||||
@RequestParam String userId) {
|
@RequestParam UUID workspaceId) {
|
||||||
BatchReportService.BatchReportResult result = batchReportService.generateBatchReport(batchId, userId);
|
BatchReportService.BatchReportResult result = batchReportService.generateBatchReport(batchId, workspaceId);
|
||||||
return ResponseEntity.ok(Map.of(
|
return ResponseEntity.ok(Map.of(
|
||||||
"fileUrl", result.fileUrl(),
|
"fileUrl", result.fileUrl(),
|
||||||
"alreadyExisted", result.alreadyExisted()
|
"alreadyExisted", result.alreadyExisted()
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package zw.qantra.tm.domain.controllers;
|
package zw.qantra.tm.domain.controllers;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.domain.In;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.And;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import zw.qantra.tm.domain.models.PaymentProcessor;
|
import zw.qantra.tm.domain.models.PaymentProcessor;
|
||||||
@@ -18,8 +25,13 @@ public class PaymentProcessorController {
|
|||||||
private final PaymentProcessorService paymentProcessorService;
|
private final PaymentProcessorService paymentProcessorService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<List<PaymentProcessor>> getAllPaymentProcessors() {
|
public ResponseEntity getAllPaymentProcessors(
|
||||||
return ResponseEntity.ok(paymentProcessorService.getPaymentProcessorRepository().findAll());
|
@And({
|
||||||
|
@Spec(path = "label", spec = Equal.class),
|
||||||
|
@Spec(path = "label", params = "labelIn", paramSeparator = ',', spec = In.class),
|
||||||
|
@Spec(path = "currency", spec = Equal.class),
|
||||||
|
})Specification<PaymentProcessor> specification, Pageable pageable) {
|
||||||
|
return ResponseEntity.ok(paymentProcessorService.getAllPaymentProcessors(specification, pageable));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ public class ProviderController {
|
|||||||
return ResponseEntity.ok(providerService.getProviderProducts(id.toString()));
|
return ResponseEntity.ok(providerService.getProviderProducts(id.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/products/{productId}")
|
@GetMapping("/{id}/products/{productName}")
|
||||||
public ResponseEntity getProducts(@PathVariable UUID id, @PathVariable UUID productId) {
|
public ResponseEntity getProducts(@PathVariable UUID id, @PathVariable String productName) {
|
||||||
return ResponseEntity.ok(providerService.getProviderProduct(id, productId));
|
return ResponseEntity.ok(providerService.getProviderProduct(id, productName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/client/{clientId}")
|
@GetMapping("/client/{clientId}")
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package zw.qantra.tm.domain.controllers;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
||||||
import net.kaczmarzyk.spring.data.jpa.domain.Like;
|
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Or;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -22,22 +21,23 @@ public class RecipientController {
|
|||||||
|
|
||||||
private final RecipientService recipientService;
|
private final RecipientService recipientService;
|
||||||
|
|
||||||
@GetMapping("/search")
|
@GetMapping
|
||||||
public ResponseEntity<List<Recipient>> searchRecipients(
|
public ResponseEntity searchRecipients(
|
||||||
@And({
|
@And({
|
||||||
@Spec(path = "name", spec = Equal.class),
|
@Spec(path = "name", spec = Equal.class),
|
||||||
@Spec(path = "email", spec = Equal.class),
|
@Spec(path = "email", spec = Equal.class),
|
||||||
@Spec(path = "phoneNumber", spec = Equal.class),
|
@Spec(path = "phoneNumber", spec = Equal.class),
|
||||||
@Spec(path = "account", spec = Equal.class),
|
@Spec(path = "account", spec = Equal.class),
|
||||||
@Spec(path = "latestProviderLabel", spec = Equal.class),
|
@Spec(path = "latestProviderLabel", spec = Equal.class),
|
||||||
@Spec(path = "userId", defaultVal = "null", spec = Equal.class)
|
@Spec(path = "userId", spec = Equal.class),
|
||||||
}) Specification<Recipient> specification) {
|
@Spec(path = "workspaceId", defaultVal = "null", spec = Equal.class)
|
||||||
return ResponseEntity.ok(recipientService.searchRecipients(specification));
|
}) Specification<Recipient> specification, Pageable pageable) {
|
||||||
|
return ResponseEntity.ok(recipientService.findAllRecipients(specification, pageable));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{account}/user/{userId}")
|
@GetMapping("/{account}/workspace/{workspaceId}")
|
||||||
public ResponseEntity<Recipient> getRecipient(@PathVariable String account, @PathVariable String userId) {
|
public ResponseEntity<Recipient> getRecipient(@PathVariable String account, @PathVariable UUID workspaceId) {
|
||||||
List<Recipient> recipients = recipientService.getRecipient(account, userId);
|
List<Recipient> recipients = recipientService.getRecipient(account, workspaceId);
|
||||||
if(recipients.size() > 0){
|
if(recipients.size() > 0){
|
||||||
return ResponseEntity.ok(recipients.get(0));
|
return ResponseEntity.ok(recipients.get(0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,12 @@ import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
|||||||
import org.hibernate.query.Page;
|
import org.hibernate.query.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileRequest;
|
||||||
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileResult;
|
||||||
import zw.qantra.tm.domain.dtos.GroupMemberRequest;
|
import zw.qantra.tm.domain.dtos.GroupMemberRequest;
|
||||||
import zw.qantra.tm.domain.dtos.RecipientGroupCreateRequest;
|
import zw.qantra.tm.domain.dtos.RecipientGroupCreateRequest;
|
||||||
import zw.qantra.tm.domain.dtos.RecipientGroupUpdateRequest;
|
import zw.qantra.tm.domain.dtos.RecipientGroupUpdateRequest;
|
||||||
@@ -21,7 +25,7 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/public/recipient-groups")
|
@RequestMapping("/recipient-groups")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RecipientGroupController {
|
public class RecipientGroupController {
|
||||||
private final RecipientGroupService recipientGroupService;
|
private final RecipientGroupService recipientGroupService;
|
||||||
@@ -30,7 +34,7 @@ public class RecipientGroupController {
|
|||||||
public ResponseEntity findAll(
|
public ResponseEntity findAll(
|
||||||
@And({
|
@And({
|
||||||
@Spec(path = "name", spec = EqualIgnoreCase.class),
|
@Spec(path = "name", spec = EqualIgnoreCase.class),
|
||||||
@Spec(path = "userId", defaultVal = "null", spec = Equal.class)
|
@Spec(path = "workspaceId", defaultVal = "null", spec = Equal.class)
|
||||||
})Specification<RecipientGroup> spec, Pageable pageable) {
|
})Specification<RecipientGroup> spec, Pageable pageable) {
|
||||||
return ResponseEntity.ok(recipientGroupService.findAll(spec, pageable));
|
return ResponseEntity.ok(recipientGroupService.findAll(spec, pageable));
|
||||||
}
|
}
|
||||||
@@ -40,25 +44,48 @@ public class RecipientGroupController {
|
|||||||
RecipientGroup group = RecipientGroup.builder()
|
RecipientGroup group = RecipientGroup.builder()
|
||||||
.name(request.getName())
|
.name(request.getName())
|
||||||
.description(request.getDescription())
|
.description(request.getDescription())
|
||||||
|
.workspaceId(request.getWorkspaceId())
|
||||||
.userId(request.getUserId())
|
.userId(request.getUserId())
|
||||||
.build();
|
.build();
|
||||||
return ResponseEntity.ok(recipientGroupService.create(group, request.getRecipients()));
|
return ResponseEntity.ok(recipientGroupService.create(group, request.getRecipients()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/create-from-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
public ResponseEntity<GroupCreateFromFileResult> createFromFile(
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam("groupName") String groupName,
|
||||||
|
@RequestParam(value = "description", required = false) String description,
|
||||||
|
@RequestParam(value = "requestedBy", required = false) String requestedBy,
|
||||||
|
@RequestParam(value = "currency", required = false) String currency,
|
||||||
|
@RequestParam("userId") UUID userId,
|
||||||
|
@RequestParam("workspaceId") UUID workspaceId
|
||||||
|
) {
|
||||||
|
|
||||||
|
GroupCreateFromFileRequest request = new GroupCreateFromFileRequest();
|
||||||
|
request.setGroupName(groupName.trim());
|
||||||
|
request.setDescription(description.trim());
|
||||||
|
request.setRequestedBy(requestedBy);
|
||||||
|
request.setCurrency(currency);
|
||||||
|
request.setWorkspaceId(workspaceId);
|
||||||
|
request.setUserId(userId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(recipientGroupService.createFromFile(file, request));
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/update/{groupId}")
|
@PutMapping("/update/{groupId}")
|
||||||
public ResponseEntity<RecipientGroup> update(@PathVariable UUID groupId,
|
public ResponseEntity<RecipientGroup> update(@PathVariable UUID groupId,
|
||||||
@RequestBody RecipientGroupUpdateRequest request) {
|
@RequestBody RecipientGroupUpdateRequest request) {
|
||||||
return ResponseEntity.ok(recipientGroupService.update(
|
return ResponseEntity.ok(recipientGroupService.update(
|
||||||
groupId,
|
groupId,
|
||||||
request.getUserId(),
|
request.getWorkspaceId(),
|
||||||
request.getName(),
|
request.getName(),
|
||||||
request.getDescription()));
|
request.getDescription()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/delete/{groupId}")
|
@DeleteMapping("/delete/{groupId}")
|
||||||
public ResponseEntity<Void> delete(@PathVariable UUID groupId,
|
public ResponseEntity<Void> delete(@PathVariable UUID groupId,
|
||||||
@RequestParam String userId) {
|
@RequestParam UUID workspaceId) {
|
||||||
recipientGroupService.delete(groupId, userId);
|
recipientGroupService.delete(groupId, workspaceId);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,15 +94,15 @@ public class RecipientGroupController {
|
|||||||
@RequestBody GroupMemberRequest request) {
|
@RequestBody GroupMemberRequest request) {
|
||||||
return ResponseEntity.ok(recipientGroupService.addMembersByIds(
|
return ResponseEntity.ok(recipientGroupService.addMembersByIds(
|
||||||
groupId,
|
groupId,
|
||||||
request.getUserId(),
|
request.getWorkspaceId(),
|
||||||
request.getRecipientIds()));
|
request.getRecipientIds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{groupId}/members/{recipientId}")
|
@DeleteMapping("/{groupId}/members/{recipientId}")
|
||||||
public ResponseEntity<Void> removeMember(@PathVariable UUID groupId,
|
public ResponseEntity<Void> removeMember(@PathVariable UUID groupId,
|
||||||
@PathVariable UUID recipientId,
|
@PathVariable UUID recipientId,
|
||||||
@RequestParam String userId) {
|
@RequestParam UUID workspaceId) {
|
||||||
recipientGroupService.removeMember(groupId, recipientId, userId);
|
recipientGroupService.removeMember(groupId, recipientId, workspaceId);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,18 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import zw.qantra.tm.domain.enums.RequestType;
|
import zw.qantra.tm.domain.enums.RequestType;
|
||||||
import zw.qantra.tm.domain.models.Transaction;
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
import zw.qantra.tm.domain.services.EmailService;
|
import zw.qantra.tm.domain.services.NotificationService;
|
||||||
import zw.qantra.tm.domain.services.TransactionService;
|
import zw.qantra.tm.domain.services.TransactionService;
|
||||||
|
import zw.qantra.tm.domain.services.WorkspaceMigrationService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/test")
|
@RequestMapping("/test")
|
||||||
@CrossOrigin(origins = "*")
|
@CrossOrigin(origins = "*")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TestController {
|
public class TestController {
|
||||||
private final EmailService emailService;
|
private final NotificationService notificationService;
|
||||||
private final TransactionService transactionService;
|
private final TransactionService transactionService;
|
||||||
|
private final WorkspaceMigrationService workspaceMigrationService;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
|
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ public class TestController {
|
|||||||
|
|
||||||
@GetMapping("/hello/{email}")
|
@GetMapping("/hello/{email}")
|
||||||
public ResponseEntity<String> helloEndpoint(@PathVariable String email) {
|
public ResponseEntity<String> helloEndpoint(@PathVariable String email) {
|
||||||
emailService.sendSimpleMessage(email, "Hello", "Hello World!");
|
notificationService.sendEmail(email, "Hello", "Hello World!");
|
||||||
return ResponseEntity.ok("Hello World!");
|
return ResponseEntity.ok("Hello World!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,4 +61,10 @@ public class TestController {
|
|||||||
public ResponseEntity<String> publicEndpoint() {
|
public ResponseEntity<String> publicEndpoint() {
|
||||||
return ResponseEntity.ok("This is a public endpoint - no authentication required!");
|
return ResponseEntity.ok("This is a public endpoint - no authentication required!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/migrate-workspaces")
|
||||||
|
public ResponseEntity<WorkspaceMigrationService.MigrationResult> migrateWorkspaces() {
|
||||||
|
WorkspaceMigrationService.MigrationResult result = workspaceMigrationService.migrate();
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package zw.qantra.tm.domain.controllers;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
|
import zw.qantra.tm.domain.services.TransactionReportService;
|
||||||
|
import zw.qantra.tm.domain.services.TransactionReportService.TransactionReportFileResult;
|
||||||
|
import zw.qantra.tm.domain.services.TransactionReportService.TransactionReportSummary;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/public/reports/transactions")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TransactionReportController {
|
||||||
|
|
||||||
|
private final TransactionReportService transactionReportService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a transaction report filtered by date range.
|
||||||
|
* Transactions are automatically filtered to only include
|
||||||
|
* partyType = INDIVIDUAL or GROUP.
|
||||||
|
*
|
||||||
|
* @param startDate start of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
|
||||||
|
* @param endDate end of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
|
||||||
|
* @param workspaceId optional workspace ID filter
|
||||||
|
* @return list of matching transactions
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<Transaction>> getTransactionReport(
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
|
||||||
|
@RequestParam(required = false) String workspaceId) {
|
||||||
|
List<Transaction> transactions = transactionReportService.generateReport(startDate, endDate, workspaceId);
|
||||||
|
return ResponseEntity.ok(transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a transaction report summary with counts and totals,
|
||||||
|
* filtered by date range and default party type filter.
|
||||||
|
*
|
||||||
|
* @param startDate start of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
|
||||||
|
* @param endDate end of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
|
||||||
|
* @param workspaceId optional workspace ID filter
|
||||||
|
* @return report summary with aggregated data
|
||||||
|
*/
|
||||||
|
@GetMapping("/summary")
|
||||||
|
public ResponseEntity<TransactionReportSummary> getTransactionReportSummary(
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
|
||||||
|
@RequestParam(required = false) String workspaceId) {
|
||||||
|
TransactionReportSummary summary = transactionReportService.generateReportSummary(startDate, endDate, workspaceId);
|
||||||
|
return ResponseEntity.ok(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a transaction report as an Excel file (.xlsx).
|
||||||
|
* The report is filtered by date range, with a default filter for
|
||||||
|
* partyType = INDIVIDUAL or GROUP.
|
||||||
|
*
|
||||||
|
* @param startDate start of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
|
||||||
|
* @param endDate end of the date range (inclusive), format: yyyy-MM-ddTHH:mm:ss
|
||||||
|
* @param workspaceId optional workspace ID filter
|
||||||
|
* @return the generated Excel file as a downloadable resource
|
||||||
|
*/
|
||||||
|
@GetMapping("/download")
|
||||||
|
public ResponseEntity<Resource> downloadTransactionReport(
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
|
||||||
|
@RequestParam(required = false) String workspaceId) {
|
||||||
|
TransactionReportFileResult reportFile = transactionReportService.generateReportFile(startDate, endDate, workspaceId);
|
||||||
|
|
||||||
|
ByteArrayResource resource = new ByteArrayResource(reportFile.fileBytes());
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + reportFile.filename() + "\"")
|
||||||
|
.body(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
|||||||
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
import io.nflow.engine.workflow.instance.WorkflowInstanceFactory;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
import net.kaczmarzyk.spring.data.jpa.domain.Equal;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.domain.EqualIgnoreCase;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.domain.In;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.domain.LikeIgnoreCase;
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Or;
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Or;
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
|
||||||
import net.kaczmarzyk.spring.data.jpa.web.annotation.Conjunction;
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Conjunction;
|
||||||
@@ -56,7 +59,8 @@ public class TransactonController {
|
|||||||
@Conjunction(value = {
|
@Conjunction(value = {
|
||||||
@Or({
|
@Or({
|
||||||
@Spec(path = "status", spec = Equal.class),
|
@Spec(path = "status", spec = Equal.class),
|
||||||
@Spec(path = "createdAt", spec = Equal.class),
|
@Spec(path = "createdAt", spec = Equal.class),
|
||||||
|
@Spec(path = "billName", spec = LikeIgnoreCase.class),
|
||||||
@Spec(path = "providerLabel", spec = Equal.class),
|
@Spec(path = "providerLabel", spec = Equal.class),
|
||||||
@Spec(path = "billClientId", spec = Equal.class),
|
@Spec(path = "billClientId", spec = Equal.class),
|
||||||
@Spec(path = "productUid", spec = Equal.class),
|
@Spec(path = "productUid", spec = Equal.class),
|
||||||
@@ -72,20 +76,22 @@ public class TransactonController {
|
|||||||
@Spec(path = "amount", spec = Equal.class),
|
@Spec(path = "amount", spec = Equal.class),
|
||||||
@Spec(path = "reference", spec = Equal.class),
|
@Spec(path = "reference", spec = Equal.class),
|
||||||
@Spec(path = "totalAmount", spec = Equal.class),
|
@Spec(path = "totalAmount", spec = Equal.class),
|
||||||
@Spec(path = "partyType", spec = Equal.class),
|
@Spec(path = "partyType", params = "partyType", paramSeparator = ',', spec = In.class),
|
||||||
@Spec(path = "sdkActionId", spec = Equal.class),
|
@Spec(path = "sdkActionId", spec = Equal.class),
|
||||||
@Spec(path = "paymentProcessorLabel", spec = Equal.class),
|
@Spec(path = "paymentProcessorLabel", spec = Equal.class),
|
||||||
})
|
})
|
||||||
}, and = {
|
}, and = {
|
||||||
@Spec(path = "type", spec = Equal.class),
|
@Spec(path = "type", spec = Equal.class),
|
||||||
@Spec(path = "userId", defaultVal = "null", spec = Equal.class),
|
@Spec(path = "workspaceId", defaultVal = "null", spec = Equal.class),
|
||||||
}) Specification<Transaction> specification, Pageable pageable) {
|
}) Specification<Transaction> specification, Pageable pageable) {
|
||||||
return ResponseEntity.ok(transactionService.findAll(specification, pageable));
|
return ResponseEntity.ok(transactionService.findAll(specification, pageable));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<Transaction> getById(@PathVariable UUID id) {
|
public ResponseEntity<Transaction> getById(
|
||||||
return ResponseEntity.ok(transactionService.findById(id));
|
@PathVariable UUID id,
|
||||||
|
@RequestParam UUID workspaceId) {
|
||||||
|
return ResponseEntity.ok(transactionService.getTransaction(id, workspaceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/integration/{id}")
|
@GetMapping("/integration/{id}")
|
||||||
@@ -192,8 +198,8 @@ public class TransactonController {
|
|||||||
|
|
||||||
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||||
.setType(TransactionWorkflow.TYPE)
|
.setType(TransactionWorkflow.TYPE)
|
||||||
.setExternalId(transaction.getUserId() + "-" + new Instant().getMillis())
|
.setExternalId(transaction.getWorkspaceId() + "-" + new Instant().getMillis())
|
||||||
.setBusinessKey(transaction.getUserId())
|
.setBusinessKey(transaction.getWorkspaceId().toString())
|
||||||
.putStateVariable("confirmation", Utils.toJson(transaction))
|
.putStateVariable("confirmation", Utils.toJson(transaction))
|
||||||
.setNextActivation(DateTime.now())
|
.setNextActivation(DateTime.now())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package zw.qantra.tm.domain.controllers;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import zw.qantra.tm.domain.models.Uptime;
|
||||||
|
import zw.qantra.tm.domain.services.UptimeService;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/public/uptimes")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UptimeController {
|
||||||
|
|
||||||
|
private final UptimeService uptimeService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity getAllUptimes(Pageable pageable) {
|
||||||
|
return ResponseEntity.ok(uptimeService.getAllUptimes(pageable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity getUptime(@PathVariable UUID id) {
|
||||||
|
return uptimeService.getUptime(id)
|
||||||
|
.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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.domain.LikeIgnoreCase;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.And;
|
||||||
|
import net.kaczmarzyk.spring.data.jpa.web.annotation.Join;
|
||||||
|
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.models.User;
|
||||||
|
import zw.qantra.tm.domain.services.UserService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/users")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity findAllUsers(
|
||||||
|
@Join(path = "workspaces", alias = "w")
|
||||||
|
@And({
|
||||||
|
@Spec(path = "username", defaultVal = "null", spec = LikeIgnoreCase.class),
|
||||||
|
@Spec(path = "email", spec = EqualIgnoreCase.class),
|
||||||
|
@Spec(path = "firstName", spec = LikeIgnoreCase.class),
|
||||||
|
@Spec(path = "lastName", spec = LikeIgnoreCase.class),
|
||||||
|
@Spec(path = "phone", spec = EqualIgnoreCase.class),
|
||||||
|
@Spec(path = "w.id", params = "workspaceId", spec = Equal.class)
|
||||||
|
}) Specification<User> spec, Pageable pageable) {
|
||||||
|
return ResponseEntity.ok(userService.findAllUsers(spec, pageable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/workspace")
|
||||||
|
public ResponseEntity searchAllUsers(
|
||||||
|
@Join(path = "workspaces", alias = "w")
|
||||||
|
@And({
|
||||||
|
@Spec(path = "username", spec = LikeIgnoreCase.class),
|
||||||
|
@Spec(path = "email", spec = EqualIgnoreCase.class),
|
||||||
|
@Spec(path = "firstName", spec = LikeIgnoreCase.class),
|
||||||
|
@Spec(path = "lastName", spec = LikeIgnoreCase.class),
|
||||||
|
@Spec(path = "phone", spec = EqualIgnoreCase.class),
|
||||||
|
@Spec(path = "w.id", params = "workspaceId", defaultVal = "null", spec = Equal.class)
|
||||||
|
}) Specification<User> spec, Pageable pageable) {
|
||||||
|
return ResponseEntity.ok(userService.findAllUsers(spec, pageable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity getUser(@PathVariable UUID id) {
|
||||||
|
return userService.getUser(id)
|
||||||
|
.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity updateUser(@PathVariable UUID id, @RequestBody User user) {
|
||||||
|
try {
|
||||||
|
User updated = userService.updateUser(id, user);
|
||||||
|
return ResponseEntity.ok(updated);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity deleteUser(@PathVariable UUID id) {
|
||||||
|
if (userService.deleteUser(id)) {
|
||||||
|
return ResponseEntity.ok(Map.of("message", "User deleted successfully"));
|
||||||
|
}
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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.Join;
|
||||||
|
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.models.Workspace;
|
||||||
|
import zw.qantra.tm.domain.services.WorkspaceService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/workspaces")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WorkspaceController {
|
||||||
|
|
||||||
|
private final WorkspaceService workspaceService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity getAllWorkspaces(
|
||||||
|
@Join(path = "users", alias = "u")
|
||||||
|
@And({
|
||||||
|
@Spec(path = "name", spec = EqualIgnoreCase.class),
|
||||||
|
@Spec(path = "u.id", params = "userId", defaultVal = "null", spec = Equal.class)
|
||||||
|
}) Specification<Workspace> spec, Pageable pageable) {
|
||||||
|
return ResponseEntity.ok(workspaceService.getAllWorkspaces(spec, pageable));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity getWorkspace(@PathVariable UUID id) {
|
||||||
|
return workspaceService.getWorkspace(id)
|
||||||
|
.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ResponseEntity searchByName(@RequestParam String name) {
|
||||||
|
return ResponseEntity.ok(workspaceService.searchByName(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity createWorkspace(@RequestBody Workspace workspace) {
|
||||||
|
return ResponseEntity.ok(workspaceService.createWorkspace(workspace));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity updateWorkspace(@PathVariable UUID id, @RequestBody Workspace workspace) {
|
||||||
|
Workspace updated = workspaceService.updateWorkspace(id, workspace);
|
||||||
|
if (updated != null) {
|
||||||
|
return ResponseEntity.ok(updated);
|
||||||
|
}
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{workspaceId}/users/{userId}")
|
||||||
|
public ResponseEntity associateUser(@PathVariable UUID workspaceId, @PathVariable UUID userId) {
|
||||||
|
return workspaceService.associateUser(workspaceId, userId)
|
||||||
|
.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{workspaceId}/users/{userId}")
|
||||||
|
public ResponseEntity disassociateUser(@PathVariable UUID workspaceId, @PathVariable UUID userId) {
|
||||||
|
return workspaceService.disassociateUser(workspaceId, userId)
|
||||||
|
.map(ResponseEntity::ok)
|
||||||
|
.orElse(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{workspaceId}/users")
|
||||||
|
public ResponseEntity getWorkspaceUsers(@PathVariable UUID workspaceId) {
|
||||||
|
return ResponseEntity.ok(workspaceService.getWorkspaceUsers(workspaceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/by-user/{userId}")
|
||||||
|
public ResponseEntity getUserWorkspaces(@PathVariable UUID userId) {
|
||||||
|
return ResponseEntity.ok(workspaceService.getUserWorkspaces(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class CloudflarePurgeResponse {
|
||||||
|
|
||||||
|
private boolean success;
|
||||||
|
private List<CloudflareError> errors;
|
||||||
|
private List<String> messages;
|
||||||
|
private CloudflareResult result;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class CloudflareError {
|
||||||
|
private int code;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class CloudflareResult {
|
||||||
|
private String id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import java.util.UUID;
|
|||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class GroupBatchCreateRequest {
|
public class GroupBatchCreateRequest {
|
||||||
private UUID recipientGroupId;
|
private UUID recipientGroupId;
|
||||||
|
private UUID workspaceId;
|
||||||
private String userId;
|
private String userId;
|
||||||
private String description;
|
private String description;
|
||||||
private String requestedBy;
|
private String requestedBy;
|
||||||
@@ -21,4 +22,3 @@ public class GroupBatchCreateRequest {
|
|||||||
private String paymentProcessorImage;
|
private String paymentProcessorImage;
|
||||||
private Transaction transactionTemplate;
|
private Transaction transactionTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class GroupBatchItemUpdateRequest {
|
||||||
|
private UUID id;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String recipientName;
|
||||||
|
private String recipientPhone;
|
||||||
|
private String recipientAccount;
|
||||||
|
private String billClientId;
|
||||||
|
private String billName;
|
||||||
|
private String billProductName;
|
||||||
|
private String providerImage;
|
||||||
|
private String providerLabel;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public static class ListWrapper {
|
||||||
|
private List<GroupBatchItemUpdateRequest> items;
|
||||||
|
private UUID workspaceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@ package zw.qantra.tm.domain.dtos;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import zw.qantra.tm.domain.enums.AuthType;
|
import zw.qantra.tm.domain.enums.AuthType;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class GroupBatchTriggerRequest {
|
public class GroupBatchTriggerRequest {
|
||||||
private String userId;
|
private UUID workspaceId;
|
||||||
private String idempotencyKey;
|
private String idempotencyKey;
|
||||||
private AuthType authType;
|
private AuthType authType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import java.util.UUID;
|
|||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class GroupBatchUpdateRequest {
|
public class GroupBatchUpdateRequest {
|
||||||
private UUID id;
|
private UUID id;
|
||||||
private String userId;
|
private UUID workspaceId;
|
||||||
private String description;
|
private String description;
|
||||||
private String requestedBy;
|
private String requestedBy;
|
||||||
private String currency;
|
private String currency;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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 GroupCreateFromFileRequest {
|
||||||
|
private String groupName;
|
||||||
|
private String groupDescription;
|
||||||
|
private String description;
|
||||||
|
private String requestedBy;
|
||||||
|
private String currency;
|
||||||
|
private BigDecimal amount;
|
||||||
|
private String paymentProcessorLabel;
|
||||||
|
private String paymentProcessorName;
|
||||||
|
private String paymentProcessorImage;
|
||||||
|
private UUID userId;
|
||||||
|
private UUID workspaceId;
|
||||||
|
private Transaction transactionTemplate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import zw.qantra.tm.domain.models.GroupBatch;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class GroupCreateFromFileResult {
|
||||||
|
private GroupBatch batch;
|
||||||
|
private int totalRows;
|
||||||
|
private int successCount;
|
||||||
|
private int errorCount;
|
||||||
|
private List<RowError> errors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public static class RowError {
|
||||||
|
private int lineNumber;
|
||||||
|
private String account;
|
||||||
|
private String name;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import java.util.UUID;
|
|||||||
@Data
|
@Data
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class GroupMemberRequest {
|
public class GroupMemberRequest {
|
||||||
private String userId;
|
private UUID workspaceId;
|
||||||
private List<UUID> recipientIds;
|
private List<UUID> recipientIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.util.UUID;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class SBZProductDto {
|
public class ProductDto {
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
import zw.qantra.tm.domain.models.Recipient;
|
import zw.qantra.tm.domain.models.Recipient;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
@@ -12,6 +13,7 @@ public class RecipientGroupCreateRequest {
|
|||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String userId;
|
private String userId;
|
||||||
|
private UUID workspaceId;
|
||||||
private List<Recipient> recipients;
|
private List<Recipient> recipients;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package zw.qantra.tm.domain.dtos;
|
|||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class RecipientGroupUpdateRequest {
|
public class RecipientGroupUpdateRequest {
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String userId;
|
private UUID workspaceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos.sms;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SmsApiResponse {
|
||||||
|
private int status;
|
||||||
|
private String result;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos.sms;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SmsBatchRequest {
|
||||||
|
private String batchReference;
|
||||||
|
private List<SmsMessage> smsList;
|
||||||
|
}
|
||||||
|
|
||||||
17
src/main/java/zw/qantra/tm/domain/dtos/sms/SmsMessage.java
Normal file
17
src/main/java/zw/qantra/tm/domain/dtos/sms/SmsMessage.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package zw.qantra.tm.domain.dtos.sms;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SmsMessage {
|
||||||
|
private String reference;
|
||||||
|
private String message;
|
||||||
|
private String msisdn;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.OneToOne;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -17,15 +12,21 @@ import zw.qantra.tm.domain.enums.RequestType;
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "additional_data", indexes = {
|
||||||
|
@Index(name = "idx_additional_data_txn_id", columnList = "transaction_id"),
|
||||||
|
@Index(name = "idx_additional_data_txn_type", columnList = "transaction_id, request_type"),
|
||||||
|
})
|
||||||
@Builder
|
@Builder
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class AdditionalData extends BaseEntity {
|
public class AdditionalData extends BaseEntity {
|
||||||
|
@Column(name = "transaction_id")
|
||||||
private String transactionId;
|
private String transactionId;
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String jsonString;
|
private String jsonString;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "request_type")
|
||||||
private RequestType requestType;
|
private RequestType requestType;
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/main/java/zw/qantra/tm/domain/models/AppVersion.java
Normal file
19
src/main/java/zw/qantra/tm/domain/models/AppVersion.java
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class AppVersion extends BaseEntity{
|
||||||
|
private String description;
|
||||||
|
private Integer appBuildNumber;
|
||||||
|
private Integer webBuildNumber;
|
||||||
|
private Timestamp timestamp;
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.hibernate.annotations.SQLRestriction;
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "category", indexes = {
|
||||||
|
@Index(name = "idx_category_name", columnList = "name"),
|
||||||
|
})
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "charge", indexes = {
|
||||||
|
@Index(name = "idx_charge_currency", columnList = "currency"),
|
||||||
|
@Index(name = "idx_charge_label_currency", columnList = "charge_label, currency"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -32,6 +36,7 @@ public class Charge extends BaseEntity {
|
|||||||
private BigDecimal maximumAmount;
|
private BigDecimal maximumAmount;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "charge_label")
|
||||||
private ChargeLabelEnum chargeLabel;
|
private ChargeLabelEnum chargeLabel;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private CurrencyType currency;
|
private CurrencyType currency;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import zw.qantra.tm.domain.enums.ChargeConditionType;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "charge_condition", indexes = {
|
||||||
|
@Index(name = "idx_charge_condition_codes", columnList = "codes"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "currency", indexes = {
|
||||||
|
@Index(name = "idx_currency_iso_code", columnList = "iso_code"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Currency extends BaseEntity {
|
public class Currency extends BaseEntity {
|
||||||
|
@Column(name = "iso_code")
|
||||||
private String isoCode;
|
private String isoCode;
|
||||||
private String name;
|
private String name;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -18,6 +15,13 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "group_batch", indexes = {
|
||||||
|
@Index(name = "idx_group_batch_workspace_id", columnList = "workspace_id"),
|
||||||
|
@Index(name = "idx_group_batch_status", columnList = "status"),
|
||||||
|
@Index(name = "idx_group_batch_recipient_group_id", columnList = "recipient_group_id"),
|
||||||
|
@Index(name = "idx_group_batch_created_at", columnList = "created_at"),
|
||||||
|
@Index(name = "idx_group_batch_payment_ref", columnList = "payment_reference"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -26,7 +30,11 @@ import java.util.UUID;
|
|||||||
@SQLRestriction("deleted = false")
|
@SQLRestriction("deleted = false")
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class GroupBatch extends BaseEntity {
|
public class GroupBatch extends BaseEntity {
|
||||||
|
@Column(name = "recipient_group_id")
|
||||||
private UUID recipientGroupId;
|
private UUID recipientGroupId;
|
||||||
|
@Column(name = "workspace_id")
|
||||||
|
private UUID workspaceId;
|
||||||
|
@Column(name = "user_id")
|
||||||
private String userId;
|
private String userId;
|
||||||
private String description;
|
private String description;
|
||||||
private String requestedBy;
|
private String requestedBy;
|
||||||
@@ -35,8 +43,11 @@ public class GroupBatch extends BaseEntity {
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private GroupBatchStatus status;
|
private GroupBatchStatus status;
|
||||||
|
|
||||||
|
@Column(name = "payment_reference")
|
||||||
private String paymentReference;
|
private String paymentReference;
|
||||||
|
@Column(name = "payment_transaction_id")
|
||||||
private UUID paymentTransactionId;
|
private UUID paymentTransactionId;
|
||||||
|
@Column(name = "workflow_id")
|
||||||
private Long workflowId;
|
private Long workflowId;
|
||||||
|
|
||||||
private String confirmIdempotencyKey;
|
private String confirmIdempotencyKey;
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import java.math.BigDecimal;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "group_batch_item", indexes = {
|
||||||
|
@Index(name = "idx_gbi_batch_id", columnList = "group_batch_id"),
|
||||||
|
@Index(name = "idx_gbi_batch_confirm", columnList = "group_batch_id, confirm_status"),
|
||||||
|
@Index(name = "idx_gbi_batch_integ", columnList = "group_batch_id, integration_status"),
|
||||||
|
@Index(name = "idx_gbi_txn_id", columnList = "transaction_id"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -22,19 +28,26 @@ import java.util.UUID;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@SQLRestriction("deleted = false")
|
@SQLRestriction("deleted = false")
|
||||||
public class GroupBatchItem extends BaseEntity {
|
public class GroupBatchItem extends BaseEntity {
|
||||||
|
@Column(name = "group_batch_id")
|
||||||
private UUID groupBatchId;
|
private UUID groupBatchId;
|
||||||
|
@Column(name = "recipient_id")
|
||||||
private UUID recipientId;
|
private UUID recipientId;
|
||||||
|
@Column(name = "recipient_name")
|
||||||
private String recipientName;
|
private String recipientName;
|
||||||
|
@Column(name = "recipient_phone")
|
||||||
private String recipientPhone;
|
private String recipientPhone;
|
||||||
|
private String recipientAccount;
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private GroupBatchItemStatus status;
|
private GroupBatchItemStatus status;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "confirm_status")
|
||||||
private GroupBatchItemConfirmStatus confirmStatus;
|
private GroupBatchItemConfirmStatus confirmStatus;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "integration_status")
|
||||||
private GroupBatchItemIntegrationStatus integrationStatus;
|
private GroupBatchItemIntegrationStatus integrationStatus;
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
@@ -46,5 +59,12 @@ public class GroupBatchItem extends BaseEntity {
|
|||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "transaction_id")
|
@JoinColumn(name = "transaction_id")
|
||||||
private Transaction transaction;
|
private Transaction transaction;
|
||||||
|
|
||||||
|
private String billClientId;
|
||||||
|
private String billName;
|
||||||
|
private String billProductName;
|
||||||
|
private String providerImage;
|
||||||
|
private String providerLabel;
|
||||||
|
private String creditAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import zw.qantra.tm.domain.enums.Status;
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "otp", indexes = {
|
||||||
|
@Index(name = "idx_otp_username_type", columnList = "username, otp_type"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -17,6 +18,8 @@ public class Otp extends BaseEntity{
|
|||||||
private String username;
|
private String username;
|
||||||
private String otp;
|
private String otp;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "otp_status")
|
||||||
private Status otpStatus;
|
private Status otpStatus;
|
||||||
|
@Column(name = "otp_type")
|
||||||
private String otpType;
|
private String otpType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.hibernate.annotations.SQLRestriction;
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "payment_processor", indexes = {
|
||||||
|
@Index(name = "idx_payment_processor_label", columnList = "label"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -19,6 +22,8 @@ public class PaymentProcessor extends BaseEntity {
|
|||||||
private String type;
|
private String type;
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String image;
|
private String image;
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private CurrencyType currency;
|
||||||
private String accountFieldName;
|
private String accountFieldName;
|
||||||
private String accountFieldLabel;
|
private String accountFieldLabel;
|
||||||
private String authType;
|
private String authType;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.hibernate.annotations.SQLRestriction;
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "provider", indexes = {
|
||||||
|
@Index(name = "idx_provider_category", columnList = "category"),
|
||||||
|
@Index(name = "idx_provider_client_id", columnList = "client_id"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -17,6 +18,7 @@ import zw.qantra.tm.domain.enums.CurrencyType;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@SQLRestriction("deleted = false")
|
@SQLRestriction("deleted = false")
|
||||||
public class Provider extends BaseEntity {
|
public class Provider extends BaseEntity {
|
||||||
|
@Column(name = "client_id")
|
||||||
private String clientId;
|
private String clientId;
|
||||||
private String label;
|
private String label;
|
||||||
private String name;
|
private String name;
|
||||||
@@ -32,6 +34,7 @@ public class Provider extends BaseEntity {
|
|||||||
private Boolean requiresAmount;
|
private Boolean requiresAmount;
|
||||||
private Boolean requiresPhone;
|
private Boolean requiresPhone;
|
||||||
private Boolean requiresAccount;
|
private Boolean requiresAccount;
|
||||||
|
private Boolean requiresProducts;
|
||||||
private double percentageCommission;
|
private double percentageCommission;
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String meta;
|
private String meta;
|
||||||
|
|||||||
@@ -2,12 +2,20 @@ package zw.qantra.tm.domain.models;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.hibernate.annotations.SQLRestriction;
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "recipient", indexes = {
|
||||||
|
@Index(name = "idx_recipient_account_workspace", columnList = "account, workspace_id"),
|
||||||
|
@Index(name = "idx_recipient_workspace_id", columnList = "workspace_id"),
|
||||||
|
@Index(name = "idx_recipient_phone", columnList = "phone_number"),
|
||||||
|
@Index(name = "idx_recipient_email", columnList = "email"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -18,10 +26,15 @@ import org.hibernate.annotations.SQLRestriction;
|
|||||||
public class Recipient extends BaseEntity {
|
public class Recipient extends BaseEntity {
|
||||||
private String name;
|
private String name;
|
||||||
private String email;
|
private String email;
|
||||||
|
@Column(name = "phone_number")
|
||||||
private String phoneNumber;
|
private String phoneNumber;
|
||||||
private String address;
|
private String address;
|
||||||
private String account;
|
private String account;
|
||||||
private String initials;
|
private String initials;
|
||||||
|
@Column(name = "latest_provider_label")
|
||||||
private String latestProviderLabel;
|
private String latestProviderLabel;
|
||||||
|
@Column(name = "workspace_id")
|
||||||
|
private UUID workspaceId;
|
||||||
|
@Column(name = "user_id")
|
||||||
private String userId;
|
private String userId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -9,7 +9,12 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.hibernate.annotations.SQLRestriction;
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "recipient_group", indexes = {
|
||||||
|
@Index(name = "idx_recipient_group_workspace_id", columnList = "workspace_id"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -20,6 +25,9 @@ import org.hibernate.annotations.SQLRestriction;
|
|||||||
public class RecipientGroup extends BaseEntity {
|
public class RecipientGroup extends BaseEntity {
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
|
@Column(name = "workspace_id")
|
||||||
|
private UUID workspaceId;
|
||||||
|
@Column(name = "user_id")
|
||||||
private String userId;
|
private String userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.ManyToOne;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -14,6 +12,10 @@ import org.hibernate.annotations.SQLRestriction;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "recipient_group_member", indexes = {
|
||||||
|
@Index(name = "idx_rgm_group_id", columnList = "recipient_group_id"),
|
||||||
|
@Index(name = "idx_rgm_group_recipient", columnList = "recipient_group_id, recipient_id"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -22,12 +24,17 @@ import java.util.UUID;
|
|||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
@SQLRestriction("deleted = false")
|
@SQLRestriction("deleted = false")
|
||||||
public class RecipientGroupMember extends BaseEntity {
|
public class RecipientGroupMember extends BaseEntity {
|
||||||
|
@Column(name = "recipient_group_id")
|
||||||
private UUID recipientGroupId;
|
private UUID recipientGroupId;
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "recipient_id")
|
@JoinColumn(name = "recipient_id")
|
||||||
private Recipient recipient;
|
private Recipient recipient;
|
||||||
|
@Column(name = "recipient_uid")
|
||||||
private UUID recipientUid;
|
private UUID recipientUid;
|
||||||
private String account;
|
private String account;
|
||||||
|
@Column(name = "workspace_id")
|
||||||
|
private UUID workspaceId;
|
||||||
|
@Column(name = "user_id")
|
||||||
private String userId;
|
private String userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -8,12 +8,17 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "setting", indexes = {
|
||||||
|
@Index(name = "idx_setting_name", columnList = "setting_name"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Setting extends BaseEntity {
|
public class Setting extends BaseEntity {
|
||||||
|
@Column(name = "setting_name")
|
||||||
private String settingName;
|
private String settingName;
|
||||||
|
@Column(name = "setting_value")
|
||||||
private String settingValue;
|
private String settingValue;
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,18 @@ import java.math.BigDecimal;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "transaction", indexes = {
|
||||||
|
@Index(name = "idx_transaction_trace_type", columnList = "trace, type"),
|
||||||
|
@Index(name = "idx_transaction_reference", columnList = "reference"),
|
||||||
|
@Index(name = "idx_transaction_workspace_id", columnList = "workspace_id"),
|
||||||
|
@Index(name = "idx_transaction_confirm_pay_integ", columnList = "confirmation_status, payment_status, integration_status"),
|
||||||
|
@Index(name = "idx_transaction_pay_poll", columnList = "payment_status, polling_status"),
|
||||||
|
@Index(name = "idx_transaction_status", columnList = "status"),
|
||||||
|
@Index(name = "idx_transaction_created_at", columnList = "created_at"),
|
||||||
|
@Index(name = "idx_transaction_debit_phone", columnList = "debit_phone"),
|
||||||
|
@Index(name = "idx_transaction_credit_phone", columnList = "credit_phone"),
|
||||||
|
@Index(name = "idx_transaction_workflow_id", columnList = "workflow_id"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -19,10 +31,16 @@ import java.util.UUID;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
public class Transaction extends BaseEntity {
|
public class Transaction extends BaseEntity {
|
||||||
|
@Column(name = "workspace_id")
|
||||||
|
private UUID workspaceId;
|
||||||
|
@Column(name = "user_id")
|
||||||
private String userId;
|
private String userId;
|
||||||
|
private String username;
|
||||||
private String trace;
|
private String trace;
|
||||||
private String region;
|
private String region;
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
private PartyType partyType;
|
private PartyType partyType;
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
private BigDecimal charge;
|
private BigDecimal charge;
|
||||||
@@ -39,6 +57,7 @@ public class Transaction extends BaseEntity {
|
|||||||
private String rrn;
|
private String rrn;
|
||||||
private String channelName;
|
private String channelName;
|
||||||
private String channel;
|
private String channel;
|
||||||
|
@Column(name = "debit_phone")
|
||||||
private String debitPhone;
|
private String debitPhone;
|
||||||
private String debitAccount;
|
private String debitAccount;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@@ -47,6 +66,7 @@ public class Transaction extends BaseEntity {
|
|||||||
private String debitCard;
|
private String debitCard;
|
||||||
private String debitRef;
|
private String debitRef;
|
||||||
private String debitEmail;
|
private String debitEmail;
|
||||||
|
@Column(name = "credit_phone")
|
||||||
private String creditPhone;
|
private String creditPhone;
|
||||||
private String creditAccount;
|
private String creditAccount;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@@ -56,6 +76,8 @@ public class Transaction extends BaseEntity {
|
|||||||
private String creditCard;
|
private String creditCard;
|
||||||
private String creditRef;
|
private String creditRef;
|
||||||
private String creditEmail;
|
private String creditEmail;
|
||||||
|
private String creditAddress;
|
||||||
|
private String creditVoucher;
|
||||||
private String billClientId;
|
private String billClientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
private String aggregatorId;
|
private String aggregatorId;
|
||||||
@@ -65,21 +87,26 @@ public class Transaction extends BaseEntity {
|
|||||||
private String paymentProcessorName;
|
private String paymentProcessorName;
|
||||||
private String providerImage;
|
private String providerImage;
|
||||||
private String paymentProcessorImage;
|
private String paymentProcessorImage;
|
||||||
|
private UUID batchId;
|
||||||
|
private String batchDescription;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private Status authorizationStatus;
|
private Status authorizationStatus;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "confirmation_status")
|
||||||
private Status confirmationStatus;
|
private Status confirmationStatus;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "payment_status")
|
||||||
private Status paymentStatus;
|
private Status paymentStatus;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "integration_status")
|
||||||
private Status integrationStatus;
|
private Status integrationStatus;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private Status reversalStatus;
|
private Status reversalStatus;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "polling_status")
|
||||||
private Status pollingStatus;
|
private Status pollingStatus;
|
||||||
|
|
||||||
|
|
||||||
private String orderId;
|
private String orderId;
|
||||||
private String orderTrace;
|
private String orderTrace;
|
||||||
private String orderTransactionTrace;
|
private String orderTransactionTrace;
|
||||||
@@ -92,7 +119,7 @@ public class Transaction extends BaseEntity {
|
|||||||
private String erpPurchasePaymentRef;
|
private String erpPurchasePaymentRef;
|
||||||
private String erpCommissionJournalRef;
|
private String erpCommissionJournalRef;
|
||||||
|
|
||||||
@Column(nullable = true)
|
@Column(name = "workflow_id", nullable = true)
|
||||||
private long workflowId;
|
private long workflowId;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ package zw.qantra.tm.domain.models;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -14,6 +11,9 @@ import lombok.Setter;
|
|||||||
import zw.qantra.tm.domain.enums.Status;
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Table(name = "transaction_event", indexes = {
|
||||||
|
@Index(name = "idx_txn_event_txn_id", columnList = "transaction_id"),
|
||||||
|
})
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@@ -21,6 +21,7 @@ import zw.qantra.tm.domain.enums.Status;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
public class TransactionEvent extends BaseEntity {
|
public class TransactionEvent extends BaseEntity {
|
||||||
|
@Column(name = "transaction_id")
|
||||||
private String transactionId;
|
private String transactionId;
|
||||||
private String event;
|
private String event;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
|||||||
24
src/main/java/zw/qantra/tm/domain/models/Uptime.java
Normal file
24
src/main/java/zw/qantra/tm/domain/models/Uptime.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
|
public class Uptime extends BaseEntity {
|
||||||
|
private String name;
|
||||||
|
private boolean status;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package zw.qantra.tm.domain.models;
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@@ -9,10 +10,14 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users", indexes = {
|
||||||
|
@Index(name = "idx_user_phone", columnList = "phone"),
|
||||||
|
})
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@SQLRestriction("deleted = false")
|
@SQLRestriction("deleted = false")
|
||||||
@@ -21,6 +26,7 @@ public class User extends BaseEntity implements UserDetails {
|
|||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@@ -49,6 +55,10 @@ public class User extends BaseEntity implements UserDetails {
|
|||||||
@Column(name = "is_credentials_non_expired")
|
@Column(name = "is_credentials_non_expired")
|
||||||
private boolean credentialsNonExpired = true;
|
private boolean credentialsNonExpired = true;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ManyToMany(mappedBy = "users", fetch = FetchType.LAZY)
|
||||||
|
private Set<Workspace> workspaces = new HashSet<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
|
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
|
||||||
|
|||||||
39
src/main/java/zw/qantra/tm/domain/models/Workspace.java
Normal file
39
src/main/java/zw/qantra/tm/domain/models/Workspace.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package zw.qantra.tm.domain.models;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "workspace", indexes = {
|
||||||
|
@Index(name = "idx_workspace_name", columnList = "name"),
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
|
public class Workspace extends BaseEntity {
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
private String phone;
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(
|
||||||
|
name = "workspace_users",
|
||||||
|
joinColumns = @JoinColumn(name = "workspace_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "user_id")
|
||||||
|
)
|
||||||
|
@Builder.Default
|
||||||
|
private Set<User> users = new HashSet<>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package zw.qantra.tm.domain.repositories;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import zw.qantra.tm.domain.models.AppVersion;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface AppVersionRepository extends JpaRepository<AppVersion, UUID> {
|
||||||
|
|
||||||
|
Optional<AppVersion> findTopByOrderByAppBuildNumberDesc();
|
||||||
|
|
||||||
|
Optional<AppVersion> findTopByOrderByWebBuildNumberDesc();
|
||||||
|
|
||||||
|
Optional<AppVersion> findTopByOrderByTimestampDesc();
|
||||||
|
}
|
||||||
@@ -5,11 +5,13 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import zw.qantra.tm.domain.models.GroupBatch;
|
import zw.qantra.tm.domain.models.GroupBatch;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface GroupBatchRepository extends JpaRepository<GroupBatch, UUID>, JpaSpecificationExecutor<GroupBatch> {
|
public interface GroupBatchRepository extends JpaRepository<GroupBatch, UUID>, JpaSpecificationExecutor<GroupBatch> {
|
||||||
Optional<GroupBatch> findByIdAndUserId(UUID id, String userId);
|
Optional<GroupBatch> findByIdAndWorkspaceId(UUID id, UUID workspaceId);
|
||||||
|
List<GroupBatch> findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package zw.qantra.tm.domain.repositories;
|
package zw.qantra.tm.domain.repositories;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
import zw.qantra.tm.domain.models.PaymentProcessor;
|
import zw.qantra.tm.domain.models.PaymentProcessor;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface PaymentProcessorRepository extends JpaRepository<PaymentProcessor, UUID> {
|
public interface PaymentProcessorRepository extends JpaRepository<PaymentProcessor, UUID>,
|
||||||
|
JpaSpecificationExecutor<PaymentProcessor> {
|
||||||
boolean existsByLabel(String label);
|
boolean existsByLabel(String label);
|
||||||
|
boolean existsByLabelAndCurrency(String label, CurrencyType currencyType);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package zw.qantra.tm.domain.repositories;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
import zw.qantra.tm.domain.models.Provider;
|
import zw.qantra.tm.domain.models.Provider;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -12,5 +13,7 @@ import java.util.UUID;
|
|||||||
public interface ProviderRepository extends JpaRepository<Provider, UUID>, JpaSpecificationExecutor<Provider> {
|
public interface ProviderRepository extends JpaRepository<Provider, UUID>, JpaSpecificationExecutor<Provider> {
|
||||||
List<Provider> findByCategory(String category);
|
List<Provider> findByCategory(String category);
|
||||||
Provider findByClientId(String clientId);
|
Provider findByClientId(String clientId);
|
||||||
|
Provider findByLabel(String label);
|
||||||
|
Provider findByLabelAndCurrency(String label, CurrencyType currency);
|
||||||
boolean existsByClientId(String clientId);
|
boolean existsByClientId(String clientId);
|
||||||
}
|
}
|
||||||
@@ -13,5 +13,6 @@ public interface RecipientGroupMemberRepository extends JpaRepository<RecipientG
|
|||||||
List<RecipientGroupMember> findAllByRecipientGroupId(UUID recipientGroupId);
|
List<RecipientGroupMember> findAllByRecipientGroupId(UUID recipientGroupId);
|
||||||
boolean existsByRecipientGroupIdAndRecipientId(UUID recipientGroupId, UUID recipientId);
|
boolean existsByRecipientGroupIdAndRecipientId(UUID recipientGroupId, UUID recipientId);
|
||||||
void deleteAllByRecipientGroupId(UUID recipientGroupId);
|
void deleteAllByRecipientGroupId(UUID recipientGroupId);
|
||||||
|
List<RecipientGroupMember> findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import java.util.UUID;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface RecipientGroupRepository extends JpaRepository<RecipientGroup, UUID>, JpaSpecificationExecutor<RecipientGroup> {
|
public interface RecipientGroupRepository extends JpaRepository<RecipientGroup, UUID>, JpaSpecificationExecutor<RecipientGroup> {
|
||||||
List<RecipientGroup> findAllByUserId(String userId);
|
List<RecipientGroup> findAllByWorkspaceId(UUID workspaceId);
|
||||||
Optional<RecipientGroup> findByIdAndUserId(UUID id, String userId);
|
Optional<RecipientGroup> findByIdAndWorkspaceId(UUID id, UUID workspaceId);
|
||||||
|
List<RecipientGroup> findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ public interface RecipientRepository extends JpaRepository<Recipient, UUID>, Jpa
|
|||||||
List<Recipient> findByNameContainingIgnoreCase(String name);
|
List<Recipient> findByNameContainingIgnoreCase(String name);
|
||||||
List<Recipient> findByEmailContainingIgnoreCase(String email);
|
List<Recipient> findByEmailContainingIgnoreCase(String email);
|
||||||
List<Recipient> findByPhoneNumberContaining(String phoneNumber);
|
List<Recipient> findByPhoneNumberContaining(String phoneNumber);
|
||||||
List<Recipient> findByAccountAndUserId(String account, String userId);
|
List<Recipient> findByAccountAndWorkspaceId(String account, UUID workspaceId);
|
||||||
|
List<Recipient> findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,10 @@ import java.util.UUID;
|
|||||||
public interface TransactionRepository extends JpaRepository<Transaction, UUID>, JpaSpecificationExecutor<Transaction> {
|
public interface TransactionRepository extends JpaRepository<Transaction, UUID>, JpaSpecificationExecutor<Transaction> {
|
||||||
Transaction findByTraceAndType(String trace, RequestType type);
|
Transaction findByTraceAndType(String trace, RequestType type);
|
||||||
Optional<Transaction> findByReference(String reference);
|
Optional<Transaction> findByReference(String reference);
|
||||||
List<Transaction> findAllByUserId(String uid);
|
List<Transaction> findAllByUserId(String userId);
|
||||||
|
Optional<Transaction> findByIdAndWorkspaceId(UUID id, UUID workspaceId);
|
||||||
|
List<Transaction> findAllByWorkspaceId(UUID workspaceId);
|
||||||
|
List<Transaction> findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
List<Transaction> findAllByConfirmationStatusAndPaymentStatusAndIntegrationStatus(
|
List<Transaction> findAllByConfirmationStatusAndPaymentStatusAndIntegrationStatus(
|
||||||
Status s1, Status s2, Status s3);
|
Status s1, Status s2, Status s3);
|
||||||
List<Transaction> findAllByPaymentStatusAndPollingStatus(Status s1, Status s2);
|
List<Transaction> findAllByPaymentStatusAndPollingStatus(Status s1, Status s2);
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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.Uptime;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UptimeRepository extends JpaRepository<Uptime, UUID>, JpaSpecificationExecutor<Uptime> {
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package zw.qantra.tm.domain.repositories;
|
package zw.qantra.tm.domain.repositories;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import zw.qantra.tm.domain.models.User;
|
import zw.qantra.tm.domain.models.User;
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ import java.util.Optional;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||||
Optional<User> findById(UUID uid);
|
Optional<User> findById(UUID uid);
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByEmail(String email);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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.Workspace;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface WorkspaceRepository extends JpaRepository<Workspace, UUID>, JpaSpecificationExecutor<Workspace> {
|
||||||
|
List<Workspace> findByNameContainingIgnoreCase(String name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import zw.qantra.tm.domain.models.AppVersion;
|
||||||
|
import zw.qantra.tm.domain.repositories.AppVersionRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppVersionService {
|
||||||
|
|
||||||
|
private final AppVersionRepository appVersionRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a newer app build number exists than the one provided by the client.
|
||||||
|
*
|
||||||
|
* @param currentBuildNumber the build number the client currently has
|
||||||
|
* @return the latest AppVersion if a newer one exists, empty otherwise
|
||||||
|
*/
|
||||||
|
public Optional<AppVersion> checkForAppUpdate(Integer currentBuildNumber) {
|
||||||
|
Optional<AppVersion> latest = appVersionRepository.findTopByOrderByAppBuildNumberDesc();
|
||||||
|
if (latest.isPresent() && latest.get().getAppBuildNumber() > currentBuildNumber) {
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a newer web build number exists than the one provided by the client.
|
||||||
|
*
|
||||||
|
* @param currentBuildNumber the build number the client currently has
|
||||||
|
* @return the latest AppVersion if a newer one exists, empty otherwise
|
||||||
|
*/
|
||||||
|
public Optional<AppVersion> checkForWebUpdate(Integer currentBuildNumber) {
|
||||||
|
Optional<AppVersion> latest = appVersionRepository.findTopByOrderByWebBuildNumberDesc();
|
||||||
|
if (latest.isPresent() && latest.get().getWebBuildNumber() > currentBuildNumber) {
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the latest AppVersion record regardless of build number.
|
||||||
|
*
|
||||||
|
* @return the latest AppVersion by timestamp
|
||||||
|
*/
|
||||||
|
public Optional<AppVersion> getLatestVersion() {
|
||||||
|
return appVersionRepository.findTopByOrderByTimestampDesc();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppVersion save(AppVersion appVersion) {
|
||||||
|
return appVersionRepository.save(appVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,8 +52,8 @@ public class BatchReportService {
|
|||||||
private final GroupBatchItemRepository groupBatchItemRepository;
|
private final GroupBatchItemRepository groupBatchItemRepository;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public BatchReportResult generateBatchReport(UUID batchId, String userId) {
|
public BatchReportResult generateBatchReport(UUID batchId, UUID workspaceId) {
|
||||||
GroupBatch batch = groupBatchRepository.findByIdAndUserId(batchId, userId)
|
GroupBatch batch = groupBatchRepository.findByIdAndWorkspaceId(batchId, workspaceId)
|
||||||
.orElseThrow(() -> new ApiException("Group batch not found"));
|
.orElseThrow(() -> new ApiException("Group batch not found"));
|
||||||
|
|
||||||
if (!REPORTABLE_STATUSES.contains(batch.getStatus())) {
|
if (!REPORTABLE_STATUSES.contains(batch.getStatus())) {
|
||||||
@@ -121,7 +121,7 @@ public class BatchReportService {
|
|||||||
Cell titleCell = titleRow.createCell(0);
|
Cell titleCell = titleRow.createCell(0);
|
||||||
titleCell.setCellValue("Batch Report - " + (batch.getDescription() != null ? batch.getDescription() : batch.getId().toString()));
|
titleCell.setCellValue("Batch Report - " + (batch.getDescription() != null ? batch.getDescription() : batch.getId().toString()));
|
||||||
titleCell.setCellStyle(titleStyle);
|
titleCell.setCellStyle(titleStyle);
|
||||||
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 9));
|
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 11));
|
||||||
titleRow.setHeightInPoints(30);
|
titleRow.setHeightInPoints(30);
|
||||||
rowNum++;
|
rowNum++;
|
||||||
|
|
||||||
@@ -149,7 +149,8 @@ public class BatchReportService {
|
|||||||
String[] headers = {
|
String[] headers = {
|
||||||
"#", "Recipient Name", "Phone", "Amount",
|
"#", "Recipient Name", "Phone", "Amount",
|
||||||
"Status", "Confirm Status", "Integration Status",
|
"Status", "Confirm Status", "Integration Status",
|
||||||
"Transaction Ref", "Confirm Error", "Integration Error"
|
"Transaction Ref", "Confirm Error", "Integration Error",
|
||||||
|
"Credit Address", "Credit Voucher"
|
||||||
};
|
};
|
||||||
|
|
||||||
Row headerRow = sheet.createRow(rowNum++);
|
Row headerRow = sheet.createRow(rowNum++);
|
||||||
@@ -214,6 +215,22 @@ public class BatchReportService {
|
|||||||
Cell integrationErrCell = row.createCell(9);
|
Cell integrationErrCell = row.createCell(9);
|
||||||
integrationErrCell.setCellValue(item.getIntegrationError() != null ? item.getIntegrationError() : "");
|
integrationErrCell.setCellValue(item.getIntegrationError() != null ? item.getIntegrationError() : "");
|
||||||
integrationErrCell.setCellStyle(dataStyle);
|
integrationErrCell.setCellStyle(dataStyle);
|
||||||
|
|
||||||
|
Cell creditAddressCell = row.createCell(10);
|
||||||
|
if (item.getTransaction() != null && item.getTransaction().getCreditAddress() != null) {
|
||||||
|
creditAddressCell.setCellValue(item.getTransaction().getCreditAddress());
|
||||||
|
} else {
|
||||||
|
creditAddressCell.setCellValue("");
|
||||||
|
}
|
||||||
|
creditAddressCell.setCellStyle(dataStyle);
|
||||||
|
|
||||||
|
Cell creditVoucherCell = row.createCell(11);
|
||||||
|
if (item.getTransaction() != null && item.getTransaction().getCreditVoucher() != null) {
|
||||||
|
creditVoucherCell.setCellValue(item.getTransaction().getCreditVoucher());
|
||||||
|
} else {
|
||||||
|
creditVoucherCell.setCellValue("");
|
||||||
|
}
|
||||||
|
creditVoucherCell.setCellStyle(dataStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-size columns
|
// Auto-size columns
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import zw.qantra.tm.domain.dtos.CloudflarePurgeResponse;
|
||||||
|
import zw.qantra.tm.rest.RestService;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CloudflareCacheService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CloudflareCacheService.class);
|
||||||
|
|
||||||
|
private static final String CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
|
||||||
|
|
||||||
|
private final RestService restService;
|
||||||
|
|
||||||
|
@Value("${cloudflare-token}")
|
||||||
|
private String cloudflareToken;
|
||||||
|
|
||||||
|
@Value("${cloudflare-zone-id}")
|
||||||
|
private String cloudflareZoneId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purges the entire Cloudflare cache for the configured zone.
|
||||||
|
*
|
||||||
|
* @return CloudflarePurgeResponse containing success status and any errors
|
||||||
|
*/
|
||||||
|
public CloudflarePurgeResponse purgeEverything() {
|
||||||
|
logger.info("Initiating Cloudflare purge_everything for zone: {}", cloudflareZoneId);
|
||||||
|
|
||||||
|
String path = CLOUDFLARE_API_BASE + "/zones/" + cloudflareZoneId + "/purge_cache";
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setBearerAuth(cloudflareToken);
|
||||||
|
|
||||||
|
Map<String, Boolean> payload = Map.of("purge_everything", true);
|
||||||
|
|
||||||
|
CloudflarePurgeResponse response = restService.postWithExternalAuth(
|
||||||
|
path, payload, headers, CloudflarePurgeResponse.class
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response != null && response.isSuccess()) {
|
||||||
|
logger.info("Cloudflare cache purged successfully for zone: {}", cloudflareZoneId);
|
||||||
|
} else if (response != null) {
|
||||||
|
logger.error("Cloudflare cache purge failed for zone: {}. Errors: {}",
|
||||||
|
cloudflareZoneId, response.getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
package zw.qantra.tm.domain.services;
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.mail.SimpleMailMessage;
|
import org.springframework.mail.SimpleMailMessage;
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use {@link NotificationService} instead for all email sending.
|
||||||
|
* Email functionality has been moved to NotificationService which supports
|
||||||
|
* pretty HTML templates.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
@Component
|
@Component
|
||||||
public class EmailService {
|
public class EmailService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(EmailService.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private JavaMailSender emailSender;
|
private JavaMailSender emailSender;
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
|
@Deprecated
|
||||||
public void sendSimpleMessage(
|
public void sendSimpleMessage(
|
||||||
String to, String subject, String text) {
|
String to, String subject, String text) {
|
||||||
|
log.warn("EmailService.sendSimpleMessage() is deprecated. Use NotificationService instead.");
|
||||||
SimpleMailMessage message = new SimpleMailMessage();
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
message.setFrom("noreply@qantra.co.zw");
|
message.setFrom("noreply@qantra.co.zw");
|
||||||
message.setTo(to);
|
message.setTo(to);
|
||||||
|
|||||||
@@ -13,13 +13,10 @@ import org.springframework.data.jpa.domain.Specification;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
|
import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
|
||||||
|
import zw.qantra.tm.domain.dtos.GroupBatchItemUpdateRequest;
|
||||||
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
|
import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
|
||||||
import zw.qantra.tm.domain.dtos.StateResponse;
|
import zw.qantra.tm.domain.dtos.StateResponse;
|
||||||
import zw.qantra.tm.domain.enums.AuthType;
|
import zw.qantra.tm.domain.enums.*;
|
||||||
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.models.*;
|
||||||
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
|
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
|
||||||
import zw.qantra.tm.domain.repositories.GroupBatchRepository;
|
import zw.qantra.tm.domain.repositories.GroupBatchRepository;
|
||||||
@@ -30,10 +27,13 @@ import zw.qantra.tm.exceptions.ApiException;
|
|||||||
import zw.qantra.tm.utils.Utils;
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@@ -43,7 +43,8 @@ public class GroupBatchService {
|
|||||||
private static final String BATCH_WORKFLOW_TYPE = "batchWorkflow";
|
private static final String BATCH_WORKFLOW_TYPE = "batchWorkflow";
|
||||||
|
|
||||||
private static final Set<GroupBatchStatus> UPDATABLE_STATUSES = Set.of(
|
private static final Set<GroupBatchStatus> UPDATABLE_STATUSES = Set.of(
|
||||||
GroupBatchStatus.CREATED
|
GroupBatchStatus.CREATED,
|
||||||
|
GroupBatchStatus.CONFIRM_FAILED
|
||||||
);
|
);
|
||||||
|
|
||||||
private static final Set<GroupBatchStatus> DELETABLE_STATUSES = Set.of(
|
private static final Set<GroupBatchStatus> DELETABLE_STATUSES = Set.of(
|
||||||
@@ -66,21 +67,19 @@ public class GroupBatchService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public GroupBatch createBatch(GroupBatchCreateRequest request) {
|
public GroupBatch createBatch(GroupBatchCreateRequest request) {
|
||||||
RecipientGroup group = recipientGroupService.get(
|
RecipientGroup group = recipientGroupService.get(
|
||||||
request.getRecipientGroupId(), request.getUserId());
|
request.getRecipientGroupId(), request.getWorkspaceId());
|
||||||
|
|
||||||
List<RecipientGroupMember> members = recipientGroupService.members(
|
List<RecipientGroupMember> members = recipientGroupService.members(
|
||||||
group.getId(), request.getUserId());
|
group.getId(), request.getWorkspaceId());
|
||||||
|
|
||||||
if (members.isEmpty()) {
|
if (members.isEmpty()) {
|
||||||
throw new ApiException("Recipient group has no members");
|
throw new ApiException("Recipient group has no members");
|
||||||
}
|
}
|
||||||
if (request.getTransactionTemplate() == null) {
|
|
||||||
throw new ApiException("transactionTemplate is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupBatch batch = GroupBatch.builder()
|
GroupBatch batch = GroupBatch.builder()
|
||||||
.recipientGroupId(group.getId())
|
.recipientGroupId(group.getId())
|
||||||
.userId(request.getUserId())
|
.userId(request.getUserId())
|
||||||
|
.workspaceId(request.getWorkspaceId())
|
||||||
.description(request.getDescription())
|
.description(request.getDescription())
|
||||||
.requestedBy(request.getRequestedBy())
|
.requestedBy(request.getRequestedBy())
|
||||||
.currency(request.getCurrency())
|
.currency(request.getCurrency())
|
||||||
@@ -109,6 +108,7 @@ public class GroupBatchService {
|
|||||||
.recipientId(recipient.getId())
|
.recipientId(recipient.getId())
|
||||||
.recipientName(recipient.getName())
|
.recipientName(recipient.getName())
|
||||||
.recipientPhone(recipient.getPhoneNumber())
|
.recipientPhone(recipient.getPhoneNumber())
|
||||||
|
.recipientAccount(recipient.getAccount())
|
||||||
.amount(defaultAmount)
|
.amount(defaultAmount)
|
||||||
.status(GroupBatchItemStatus.NEW)
|
.status(GroupBatchItemStatus.NEW)
|
||||||
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
|
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
|
||||||
@@ -127,8 +127,8 @@ public class GroupBatchService {
|
|||||||
return groupBatchRepository.save(savedBatch);
|
return groupBatchRepository.save(savedBatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupBatch triggerConfirm(UUID batchId, String userId, String idempotencyKey) {
|
public GroupBatch triggerConfirm(UUID batchId, UUID workspaceId, String idempotencyKey) {
|
||||||
GroupBatch batch = getBatch(batchId, userId);
|
GroupBatch batch = getBatch(batchId, workspaceId);
|
||||||
|
|
||||||
if (batch.getConfirmIdempotencyKey() != null && batch.getConfirmIdempotencyKey().equals(idempotencyKey)) {
|
if (batch.getConfirmIdempotencyKey() != null && batch.getConfirmIdempotencyKey().equals(idempotencyKey)) {
|
||||||
return batch;
|
return batch;
|
||||||
@@ -141,7 +141,7 @@ public class GroupBatchService {
|
|||||||
if (batch.getWorkflowId() == null) {
|
if (batch.getWorkflowId() == null) {
|
||||||
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
var instance = workflowInstanceFactory.newWorkflowInstanceBuilder()
|
||||||
.setType(BatchWorkflow.TYPE)
|
.setType(BatchWorkflow.TYPE)
|
||||||
.setExternalId(batch.getUserId() + "-batch-" + new Instant().getMillis())
|
.setExternalId(batch.getWorkspaceId() + "-batch-" + new Instant().getMillis())
|
||||||
.setBusinessKey(batch.getId().toString())
|
.setBusinessKey(batch.getId().toString())
|
||||||
.putStateVariable("batchId", Utils.toJson(batch.getId().toString()))
|
.putStateVariable("batchId", Utils.toJson(batch.getId().toString()))
|
||||||
.setNextActivation(DateTime.now())
|
.setNextActivation(DateTime.now())
|
||||||
@@ -171,9 +171,9 @@ public class GroupBatchService {
|
|||||||
return batch;
|
return batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object triggerRequest(UUID batchId, String userId,
|
public Object triggerRequest(UUID batchId, UUID workspaceId,
|
||||||
AuthType authType, String idempotencyKey) {
|
AuthType authType, String idempotencyKey) {
|
||||||
GroupBatch batch = getBatch(batchId, userId);
|
GroupBatch batch = getBatch(batchId, workspaceId);
|
||||||
|
|
||||||
if (batch.getRequestIdempotencyKey() != null && batch.getRequestIdempotencyKey().equals(idempotencyKey)) {
|
if (batch.getRequestIdempotencyKey() != null && batch.getRequestIdempotencyKey().equals(idempotencyKey)) {
|
||||||
return batch;
|
return batch;
|
||||||
@@ -207,7 +207,7 @@ public class GroupBatchService {
|
|||||||
try {
|
try {
|
||||||
CompletableFuture<Object> future = workflowUtils
|
CompletableFuture<Object> future = workflowUtils
|
||||||
.waitForState(batch.getWorkflowId(), new String[]{"requestSuccess", "done", "failed"},
|
.waitForState(batch.getWorkflowId(), new String[]{"requestSuccess", "done", "failed"},
|
||||||
15000, 50);
|
45000, 50);
|
||||||
StateResponse response = (StateResponse) future.get();
|
StateResponse response = (StateResponse) future.get();
|
||||||
String id = response.getBody().toString(); // this returns Batch Transaction ID
|
String id = response.getBody().toString(); // this returns Batch Transaction ID
|
||||||
String cleaned = id.replaceAll("^\"|\"$", "");
|
String cleaned = id.replaceAll("^\"|\"$", "");
|
||||||
@@ -218,8 +218,8 @@ public class GroupBatchService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object triggerPoll(UUID batchId, String userId, String idempotencyKey) {
|
public Object triggerPoll(UUID batchId, UUID workspaceId, String idempotencyKey) {
|
||||||
GroupBatch batch = getBatch(batchId, userId);
|
GroupBatch batch = getBatch(batchId, workspaceId);
|
||||||
|
|
||||||
if (batch.getPollIdempotencyKey() != null && batch.getPollIdempotencyKey().equals(idempotencyKey)) {
|
if (batch.getPollIdempotencyKey() != null && batch.getPollIdempotencyKey().equals(idempotencyKey)) {
|
||||||
return batch;
|
return batch;
|
||||||
@@ -232,6 +232,7 @@ public class GroupBatchService {
|
|||||||
throw new ApiException("Batch workflow has not been started. Trigger confirm first.");
|
throw new ApiException("Batch workflow has not been started. Trigger confirm first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// STEP 1: TRIGGER POLL SYNCHRONOUSLY
|
||||||
WorkflowInstance update = new WorkflowInstance.Builder()
|
WorkflowInstance update = new WorkflowInstance.Builder()
|
||||||
.setId(batch.getWorkflowId())
|
.setId(batch.getWorkflowId())
|
||||||
.setState("poll")
|
.setState("poll")
|
||||||
@@ -250,20 +251,42 @@ public class GroupBatchService {
|
|||||||
workflowInstanceService.updateWorkflowInstance(update, action);
|
workflowInstanceService.updateWorkflowInstance(update, action);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// wait for poll to finish
|
||||||
CompletableFuture<Object> future = workflowUtils
|
CompletableFuture<Object> future = workflowUtils
|
||||||
.waitForState(batch.getWorkflowId(), new String[]{"done", "failed"},
|
.waitForState(batch.getWorkflowId(), new String[]{"pollSuccess", "failed"},
|
||||||
15000, 50);
|
45000, 50);
|
||||||
StateResponse response = (StateResponse) future.get();
|
StateResponse response = (StateResponse) future.get();
|
||||||
String id = response.getBody().toString(); // this returns Batch Transaction ID
|
String id = response.getBody().toString(); // this returns Batch Transaction ID
|
||||||
String cleaned = id.replaceAll("^\"|\"$", "");
|
String cleaned = id.replaceAll("^\"|\"$", "");
|
||||||
return transactionService.findById(UUID.fromString(cleaned));
|
Transaction transaction = transactionService.findById(UUID.fromString(cleaned));
|
||||||
|
|
||||||
|
// if failed then return
|
||||||
|
if(!transaction.getPollingStatus().equals(Status.SUCCESS)) {
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 2: TRIGGER INTEGRATION ASYNCHRONOUSLY
|
||||||
|
WorkflowInstance integrationUpdate = new WorkflowInstance.Builder()
|
||||||
|
.setId(batch.getWorkflowId())
|
||||||
|
.setState("integration")
|
||||||
|
.setNextActivation(DateTime.now())
|
||||||
|
.setStateText("Resumed from network trigger")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WorkflowInstanceAction integrationAction = new WorkflowInstanceAction.Builder(integrationUpdate)
|
||||||
|
.setType(WorkflowInstanceAction.WorkflowActionType.externalChange)
|
||||||
|
.setExecutionEnd(DateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
workflowInstanceService.updateWorkflowInstance(integrationUpdate, integrationAction);
|
||||||
|
return transaction;
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupBatch getBatch(UUID batchId, String userId) {
|
public GroupBatch getBatch(UUID batchId, UUID workspaceId) {
|
||||||
return groupBatchRepository.findByIdAndUserId(batchId, userId)
|
return groupBatchRepository.findByIdAndWorkspaceId(batchId, workspaceId)
|
||||||
.orElseThrow(() -> new ApiException("Group batch not found"));
|
.orElseThrow(() -> new ApiException("Group batch not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +300,7 @@ public class GroupBatchService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public GroupBatch updateBatch(GroupBatchUpdateRequest request) {
|
public GroupBatch updateBatch(GroupBatchUpdateRequest request) {
|
||||||
GroupBatch batch = getBatch(request.getId(), request.getUserId());
|
GroupBatch batch = getBatch(request.getId(), request.getWorkspaceId());
|
||||||
|
|
||||||
if (!UPDATABLE_STATUSES.contains(batch.getStatus())) {
|
if (!UPDATABLE_STATUSES.contains(batch.getStatus())) {
|
||||||
throw new ApiException(
|
throw new ApiException(
|
||||||
@@ -328,8 +351,82 @@ public class GroupBatchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteBatch(UUID batchId, String userId) {
|
public List<GroupBatchItem> updateBatchItems(UUID batchId, UUID workspaceId,
|
||||||
GroupBatch batch = getBatch(batchId, userId);
|
List<GroupBatchItemUpdateRequest> items) {
|
||||||
|
GroupBatch batch = getBatch(batchId, workspaceId);
|
||||||
|
|
||||||
|
if (!UPDATABLE_STATUSES.contains(batch.getStatus())) {
|
||||||
|
throw new ApiException(
|
||||||
|
"Batch items cannot be updated at current status: " + batch.getStatus()
|
||||||
|
+ ". Only batches in CREATED status can have items updated."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupBatchItem> existingItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||||
|
Map<UUID, GroupBatchItem> itemMap = existingItems.stream()
|
||||||
|
.collect(Collectors.toMap(GroupBatchItem::getId, item -> item));
|
||||||
|
|
||||||
|
List<GroupBatchItem> updatedItems = new ArrayList<>();
|
||||||
|
|
||||||
|
for (GroupBatchItemUpdateRequest update : items) {
|
||||||
|
if (update.getId() == null) {
|
||||||
|
throw new ApiException("Item id is required for each item update");
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupBatchItem item = itemMap.get(update.getId());
|
||||||
|
if (item == null) {
|
||||||
|
throw new ApiException("Group batch item not found: " + update.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update.getAmount() != null) {
|
||||||
|
item.setAmount(update.getAmount());
|
||||||
|
}
|
||||||
|
if (update.getRecipientName() != null) {
|
||||||
|
item.setRecipientName(update.getRecipientName());
|
||||||
|
}
|
||||||
|
if (update.getRecipientPhone() != null) {
|
||||||
|
item.setRecipientPhone(update.getRecipientPhone());
|
||||||
|
}
|
||||||
|
if (update.getRecipientAccount() != null) {
|
||||||
|
item.setRecipientAccount(update.getRecipientAccount());
|
||||||
|
}
|
||||||
|
if (update.getBillClientId() != null) {
|
||||||
|
item.setBillClientId(update.getBillClientId());
|
||||||
|
}
|
||||||
|
if (update.getBillName() != null) {
|
||||||
|
item.setBillName(update.getBillName());
|
||||||
|
}
|
||||||
|
if (update.getBillProductName() != null) {
|
||||||
|
item.setBillProductName(update.getBillProductName());
|
||||||
|
}
|
||||||
|
if (update.getProviderImage() != null) {
|
||||||
|
item.setProviderImage(update.getProviderImage());
|
||||||
|
}
|
||||||
|
if (update.getProviderLabel() != null) {
|
||||||
|
item.setProviderLabel(update.getProviderLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedItems.add(groupBatchItemRepository.save(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate batch totals if any amount changed
|
||||||
|
boolean amountChanged = items.stream().anyMatch(u -> u.getAmount() != null);
|
||||||
|
if (amountChanged) {
|
||||||
|
List<GroupBatchItem> allItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||||
|
BigDecimal newTotalValue = allItems.stream()
|
||||||
|
.map(GroupBatchItem::getAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
batch.setValue(newTotalValue);
|
||||||
|
batch.setCount(allItems.size());
|
||||||
|
groupBatchRepository.save(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteBatch(UUID batchId, UUID workspaceId) {
|
||||||
|
GroupBatch batch = getBatch(batchId, workspaceId);
|
||||||
|
|
||||||
if (!DELETABLE_STATUSES.contains(batch.getStatus())) {
|
if (!DELETABLE_STATUSES.contains(batch.getStatus())) {
|
||||||
throw new ApiException(
|
throw new ApiException(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import zw.qantra.tm.domain.repositories.GroupBatchRepository;
|
|||||||
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
||||||
import zw.qantra.tm.domain.services.processors.workflows.handlers.*;
|
import zw.qantra.tm.domain.services.processors.workflows.handlers.*;
|
||||||
import zw.qantra.tm.exceptions.ApiException;
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
|
import zw.qantra.tm.utils.SecurityUtils;
|
||||||
import zw.qantra.tm.utils.Utils;
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -32,6 +33,7 @@ public class GroupBatchWorkflowService {
|
|||||||
private final IntegrationHandler integrationHandler;
|
private final IntegrationHandler integrationHandler;
|
||||||
private final CalculateChargesHandler calculateChargesHandler;
|
private final CalculateChargesHandler calculateChargesHandler;
|
||||||
private final CleanupHandler cleanUpHandler;
|
private final CleanupHandler cleanUpHandler;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public GroupBatch processConfirm(UUID batchId) {
|
public GroupBatch processConfirm(UUID batchId) {
|
||||||
@@ -49,15 +51,10 @@ public class GroupBatchWorkflowService {
|
|||||||
batch.setConfirmStartedAt(LocalDateTime.now());
|
batch.setConfirmStartedAt(LocalDateTime.now());
|
||||||
groupBatchRepository.save(batch);
|
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<GroupBatchItem> items = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||||
for (GroupBatchItem item : items) {
|
for (GroupBatchItem item : items) {
|
||||||
futures.add(CompletableFuture.runAsync(() -> confirmItem(batch, item, template)));
|
futures.add(CompletableFuture.runAsync(() -> confirmItem(batch, item)));
|
||||||
}
|
}
|
||||||
futures.forEach(CompletableFuture::join);
|
futures.forEach(CompletableFuture::join);
|
||||||
|
|
||||||
@@ -98,17 +95,18 @@ public class GroupBatchWorkflowService {
|
|||||||
throw new ApiException("No confirmed batch items available for payment request");
|
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()
|
BigDecimal total = confirmedItems.stream()
|
||||||
.map(item -> item.getAmount() == null ? BigDecimal.ZERO : item.getAmount())
|
.map(item -> item.getAmount() == null ? BigDecimal.ZERO : item.getAmount())
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
User user = userService.getUserRepository().findById(UUID.fromString(batch.getUserId())).orElseThrow();
|
||||||
|
|
||||||
Transaction transaction = Utils.deepCopy(template, Transaction.class);
|
Transaction transaction = new Transaction();
|
||||||
transaction.setId(null);
|
transaction.setId(null);
|
||||||
|
transaction.setDebitPhone(user.getPhone());
|
||||||
|
transaction.setDebitName(user.getFirstName());
|
||||||
|
transaction.setDebitCurrency(CurrencyType.valueOf(batch.getCurrency()));
|
||||||
|
transaction.setRegion("ZW");
|
||||||
|
|
||||||
transaction.setPartyType(PartyType.GROUP);
|
transaction.setPartyType(PartyType.GROUP);
|
||||||
transaction.setType(RequestType.REQUEST);
|
transaction.setType(RequestType.REQUEST);
|
||||||
transaction.setAuthType(authType == null ? AuthType.REMOTE : authType);
|
transaction.setAuthType(authType == null ? AuthType.REMOTE : authType);
|
||||||
@@ -116,9 +114,13 @@ public class GroupBatchWorkflowService {
|
|||||||
transaction.setTotalAmount(total);
|
transaction.setTotalAmount(total);
|
||||||
transaction.setReference("batch-" + batch.getId());
|
transaction.setReference("batch-" + batch.getId());
|
||||||
transaction.setUserId(batch.getUserId());
|
transaction.setUserId(batch.getUserId());
|
||||||
|
transaction.setWorkspaceId(batch.getWorkspaceId());
|
||||||
transaction.setPaymentProcessorLabel(batch.getPaymentProcessorLabel());
|
transaction.setPaymentProcessorLabel(batch.getPaymentProcessorLabel());
|
||||||
transaction.setPaymentProcessorName(batch.getPaymentProcessorName());
|
transaction.setPaymentProcessorName(batch.getPaymentProcessorName());
|
||||||
transaction.setPaymentProcessorImage(batch.getPaymentProcessorImage());
|
transaction.setPaymentProcessorImage(batch.getPaymentProcessorImage());
|
||||||
|
transaction.setBatchId(batch.getId());
|
||||||
|
transaction.setBatchDescription(batch.getDescription());
|
||||||
|
transaction.setBillName("Batch - " + batch.getDescription());
|
||||||
|
|
||||||
transaction = calculateChargesHandler.process(transaction);
|
transaction = calculateChargesHandler.process(transaction);
|
||||||
transaction = (Transaction) cleanUpHandler.process(transaction);
|
transaction = (Transaction) cleanUpHandler.process(transaction);
|
||||||
@@ -194,6 +196,9 @@ public class GroupBatchWorkflowService {
|
|||||||
batch.setStatus(GroupBatchStatus.PARTIAL);
|
batch.setStatus(GroupBatchStatus.PARTIAL);
|
||||||
} else {
|
} else {
|
||||||
batch.setStatus(GroupBatchStatus.COMPLETED);
|
batch.setStatus(GroupBatchStatus.COMPLETED);
|
||||||
|
Transaction paymentTransaction = transactionService.findById(batch.getPaymentTransactionId());
|
||||||
|
paymentTransaction.setIntegrationStatus(Status.SUCCESS);
|
||||||
|
transactionService.save(paymentTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
batch.setCompletedAt(LocalDateTime.now());
|
batch.setCompletedAt(LocalDateTime.now());
|
||||||
@@ -203,12 +208,27 @@ public class GroupBatchWorkflowService {
|
|||||||
return groupBatchRepository.save(batch);
|
return groupBatchRepository.save(batch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void confirmItem(GroupBatch batch, GroupBatchItem item, Transaction template) {
|
private void confirmItem(GroupBatch batch, GroupBatchItem item) {
|
||||||
GroupBatchItem mutableItem = groupBatchItemRepository.findById(item.getId()).orElse(item);
|
GroupBatchItem mutableItem = groupBatchItemRepository.findById(item.getId()).orElse(item);
|
||||||
try {
|
try {
|
||||||
Transaction transaction = Utils.deepCopy(template, Transaction.class);
|
User user = userService.getUserRepository().findById(UUID.fromString(batch.getUserId())).orElseThrow();
|
||||||
|
Transaction transaction = new Transaction();
|
||||||
transaction.setId(null);
|
transaction.setId(null);
|
||||||
|
transaction.setDebitPhone(user.getPhone());
|
||||||
|
transaction.setDebitName(user.getFirstName());
|
||||||
|
transaction.setDebitCurrency(CurrencyType.valueOf(batch.getCurrency()));
|
||||||
|
transaction.setRegion("ZW");
|
||||||
|
|
||||||
|
transaction.setBillClientId(item.getBillClientId());
|
||||||
|
transaction.setBillName(item.getBillName());
|
||||||
|
transaction.setBillProductName(item.getBillProductName());
|
||||||
|
transaction.setProviderImage(item.getProviderImage());
|
||||||
|
transaction.setProviderLabel(item.getProviderLabel());
|
||||||
|
|
||||||
transaction.setUserId(batch.getUserId());
|
transaction.setUserId(batch.getUserId());
|
||||||
|
transaction.setBatchId(batch.getId());
|
||||||
|
transaction.setUsername(user.getUsername());
|
||||||
|
transaction.setWorkspaceId(batch.getWorkspaceId());
|
||||||
transaction.setType(RequestType.CONFIRM);
|
transaction.setType(RequestType.CONFIRM);
|
||||||
transaction.setPartyType(PartyType.BATCH);
|
transaction.setPartyType(PartyType.BATCH);
|
||||||
transaction.setAuthType(AuthType.REMOTE); // default to remote for now
|
transaction.setAuthType(AuthType.REMOTE); // default to remote for now
|
||||||
@@ -233,6 +253,8 @@ public class GroupBatchWorkflowService {
|
|||||||
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.CONFIRMED);
|
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.CONFIRMED);
|
||||||
mutableItem.setStatus(GroupBatchItemStatus.CONFIRMED);
|
mutableItem.setStatus(GroupBatchItemStatus.CONFIRMED);
|
||||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.PENDING);
|
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.PENDING);
|
||||||
|
if(result.getCreditAddress() != null)
|
||||||
|
mutableItem.setCreditAddress(result.getCreditAddress());
|
||||||
mutableItem.setConfirmError(null);
|
mutableItem.setConfirmError(null);
|
||||||
} else {
|
} else {
|
||||||
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);
|
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);
|
||||||
@@ -241,6 +263,7 @@ public class GroupBatchWorkflowService {
|
|||||||
mutableItem.setConfirmError(result.getErrorMessage());
|
mutableItem.setConfirmError(result.getErrorMessage());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);
|
mutableItem.setConfirmStatus(GroupBatchItemConfirmStatus.FAILED);
|
||||||
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
||||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.SKIPPED);
|
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.SKIPPED);
|
||||||
@@ -278,6 +301,7 @@ public class GroupBatchWorkflowService {
|
|||||||
mutableItem.setStatus(GroupBatchItemStatus.INTEGRATED);
|
mutableItem.setStatus(GroupBatchItemStatus.INTEGRATED);
|
||||||
mutableItem.setIntegrationError(null);
|
mutableItem.setIntegrationError(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.FAILED);
|
mutableItem.setIntegrationStatus(GroupBatchItemIntegrationStatus.FAILED);
|
||||||
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
mutableItem.setStatus(GroupBatchItemStatus.FAILED);
|
||||||
mutableItem.setIntegrationError(e.getMessage());
|
mutableItem.setIntegrationError(e.getMessage());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.springframework.cache.annotation.Cacheable;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import zw.qantra.tm.rest.RestService;
|
import zw.qantra.tm.rest.RestService;
|
||||||
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -21,6 +22,7 @@ public class HotRechargeBalanceService {
|
|||||||
Logger logger = LoggerFactory.getLogger(HotRechargeBalanceService.class);
|
Logger logger = LoggerFactory.getLogger(HotRechargeBalanceService.class);
|
||||||
|
|
||||||
private final RestService restService;
|
private final RestService restService;
|
||||||
|
private final SettingService settingService;
|
||||||
|
|
||||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||||
private String hotApiUrl;
|
private String hotApiUrl;
|
||||||
@@ -28,6 +30,10 @@ public class HotRechargeBalanceService {
|
|||||||
@Cacheable(value = "hotrechargeBalance", key = "#token + '_' + #amount.toString()")
|
@Cacheable(value = "hotrechargeBalance", key = "#token + '_' + #amount.toString()")
|
||||||
public boolean checkBalance(String token, BigDecimal amount, String accId) {
|
public boolean checkBalance(String token, BigDecimal amount, String accId) {
|
||||||
try {
|
try {
|
||||||
|
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
String balanceUrl = hotApiUrl + "/account/balance/" + accId;
|
String balanceUrl = hotApiUrl + "/account/balance/" + accId;
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setBearerAuth(token);
|
headers.setBearerAuth(token);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import io.nflow.engine.service.WorkflowInstanceService;
|
|||||||
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
import io.nflow.engine.workflow.instance.WorkflowInstance;
|
||||||
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
import io.nflow.engine.workflow.instance.WorkflowInstanceAction;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
|
import org.jobrunr.jobs.context.JobRunrDashboardLogger;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -22,10 +23,10 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
|
|
||||||
import static org.joda.time.DateTime.now;
|
import static org.joda.time.DateTime.now;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class JobService {
|
public class JobService {
|
||||||
Logger log = new JobRunrDashboardLogger(LoggerFactory.getLogger(JobService.class));
|
|
||||||
|
|
||||||
private final WorkflowInstanceService workflowInstanceService;
|
private final WorkflowInstanceService workflowInstanceService;
|
||||||
private final WorkflowUtils workflowUtils;
|
private final WorkflowUtils workflowUtils;
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import zw.qantra.tm.domain.dtos.sms.SmsApiResponse;
|
||||||
|
import zw.qantra.tm.domain.dtos.sms.SmsBatchRequest;
|
||||||
|
import zw.qantra.tm.domain.dtos.sms.SmsMessage;
|
||||||
|
import zw.qantra.tm.rest.RestService;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NotificationService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(NotificationService.class);
|
||||||
|
|
||||||
|
private final RestService restService;
|
||||||
|
private final JavaMailSender emailSender;
|
||||||
|
|
||||||
|
@Value("${zss.sms.api.url}")
|
||||||
|
private String smsApiUrl;
|
||||||
|
@Value("${zss.sms.api.auth}")
|
||||||
|
private String smsApiAuth;
|
||||||
|
@Value("${sms.debug}")
|
||||||
|
private Boolean smsDebug;
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendEmail(String to, String subject, String text) {
|
||||||
|
sendEmailWithHtml(to, subject, text, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendEmailWithHtml(String to, String subject, String htmlContent) {
|
||||||
|
sendEmailWithHtml(to, subject, null, htmlContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendEmailWithHtml(String to, String subject, String text, String htmlContent) {
|
||||||
|
try {
|
||||||
|
MimeMessage message = emailSender.createMimeMessage();
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||||
|
helper.setFrom("noreply@qantra.co.zw");
|
||||||
|
helper.setTo(to);
|
||||||
|
helper.setSubject(subject);
|
||||||
|
|
||||||
|
if (htmlContent != null) {
|
||||||
|
helper.setText(text != null ? text : "", htmlContent);
|
||||||
|
} else if (text != null) {
|
||||||
|
helper.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
emailSender.send(message);
|
||||||
|
logger.info("Email sent to {} - Subject: {}", to, subject);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to send email to {}: {}", to, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendOtpEmail(String to, String username, String otpCode) {
|
||||||
|
try {
|
||||||
|
// Load and populate the HTML template
|
||||||
|
ClassPathResource resource = new ClassPathResource("templates/email-otp.html");
|
||||||
|
String htmlTemplate;
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
htmlTemplate = reader.lines().collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple placeholder replacement
|
||||||
|
String htmlContent = htmlTemplate
|
||||||
|
.replace("{{username}}", username != null ? username : "")
|
||||||
|
.replace("{{otpCode}}", otpCode != null ? otpCode : "");
|
||||||
|
|
||||||
|
MimeMessage message = emailSender.createMimeMessage();
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||||
|
helper.setFrom("noreply@qantra.co.zw");
|
||||||
|
helper.setTo(to);
|
||||||
|
helper.setSubject("Velocity Pay verification code");
|
||||||
|
helper.setText("Your Velocity Pay verification code is: " + otpCode, htmlContent);
|
||||||
|
|
||||||
|
emailSender.send(message);
|
||||||
|
logger.info("OTP email sent to {} for user: {}", to, username);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to send OTP email to {}: {}", to, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void sendSms(String to, String message) {
|
||||||
|
if(smsDebug) {
|
||||||
|
logger.info("Sending SMS to {}: {}", to, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SmsMessage smsMessage = SmsMessage.builder()
|
||||||
|
.reference(UUID.randomUUID().toString())
|
||||||
|
.message(message)
|
||||||
|
.msisdn(to)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
SmsBatchRequest batchRequest = SmsBatchRequest.builder()
|
||||||
|
.batchReference(UUID.randomUUID().toString())
|
||||||
|
.smsList(Collections.singletonList(smsMessage))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Authorization", smsApiAuth);
|
||||||
|
|
||||||
|
SmsApiResponse response = restService.postWithExternalAuth(
|
||||||
|
smsApiUrl,
|
||||||
|
batchRequest,
|
||||||
|
headers,
|
||||||
|
SmsApiResponse.class
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info("SMS sent to {} - Status: {}, Result: {}", to, response.getStatus(), response.getResult());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to send SMS to {}: {}", to, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package zw.qantra.tm.domain.services;
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
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.stereotype.Service;
|
||||||
|
import zw.qantra.tm.domain.models.PaymentProcessor;
|
||||||
import zw.qantra.tm.domain.repositories.PaymentProcessorRepository;
|
import zw.qantra.tm.domain.repositories.PaymentProcessorRepository;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -12,4 +16,8 @@ public class PaymentProcessorService {
|
|||||||
public PaymentProcessorRepository getPaymentProcessorRepository() {
|
public PaymentProcessorRepository getPaymentProcessorRepository() {
|
||||||
return paymentProcessorRepository;
|
return paymentProcessorRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Page<PaymentProcessor> getAllPaymentProcessors(Specification<PaymentProcessor> specification, Pageable pageable) {
|
||||||
|
return paymentProcessorRepository.findAll(specification, pageable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,16 +6,13 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import zw.qantra.tm.domain.dtos.ProductDto;
|
||||||
|
|
||||||
import zw.qantra.tm.domain.dtos.SBZProductDto;
|
|
||||||
import zw.qantra.tm.domain.models.Provider;
|
import zw.qantra.tm.domain.models.Provider;
|
||||||
import zw.qantra.tm.domain.repositories.ProviderRepository;
|
import zw.qantra.tm.domain.repositories.ProviderRepository;
|
||||||
import zw.qantra.tm.exceptions.ApiException;
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
@@ -35,61 +32,41 @@ public class ProviderService {
|
|||||||
private final RestService restService;
|
private final RestService restService;
|
||||||
private final HotRechargeTokenService hotRechargeTokenService;
|
private final HotRechargeTokenService hotRechargeTokenService;
|
||||||
|
|
||||||
|
private Map<String, List<ProductDto>> providerProductsCache = new HashMap<>();
|
||||||
|
|
||||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||||
private String hotApiUrl;
|
private String hotApiUrl;
|
||||||
|
|
||||||
@Value("${sbz.aggregator.url}")
|
@Value("${sbz.aggregator.url}")
|
||||||
private String aggregatorUrl;
|
private String aggregatorUrl;
|
||||||
|
|
||||||
public SBZProductDto getProviderProduct(UUID providerId, UUID productId) {
|
public ProductDto getProviderProduct(UUID providerId, String productName) {
|
||||||
return getProviderProducts(providerId.toString()).stream()
|
List<ProductDto> products = getProviderProducts(providerId.toString());
|
||||||
.filter(product -> product.getUid().equals(productId))
|
ProductDto productResult = products.stream()
|
||||||
|
.filter(product -> product.getName().equals(productName))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
|
log.info("Product: {}", productResult);
|
||||||
|
return productResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
public List<ProductDto> getProviderProducts(String providerUid) {
|
||||||
public List<SBZProductDto> getProviderProducts(UUID providerId) {
|
|
||||||
String token = restService.getMerchantToken();
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
|
||||||
headers.setBearerAuth(token);
|
|
||||||
|
|
||||||
LinkedHashMap response;
|
|
||||||
try {
|
try {
|
||||||
response = this.restService.getAs(
|
List<ProductDto> cachedProducts = providerProductsCache.get(providerUid);
|
||||||
aggregatorUrl + "/merchant/aggregation/products?size=20&clientUid=" + providerId.toString(),
|
if (cachedProducts != null) {
|
||||||
headers,
|
log.info("Returning cached products for provider: {}, number of products: {}", providerUid,
|
||||||
LinkedHashMap.class
|
cachedProducts.size());
|
||||||
);
|
return cachedProducts;
|
||||||
} catch (Exception e) {
|
}
|
||||||
log.error("Error fetching products for provider " + providerId, e);
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkedHashMap body = (LinkedHashMap) response.get("body");
|
|
||||||
List<LinkedHashMap> content = (List<LinkedHashMap>) body.get("content");
|
|
||||||
|
|
||||||
List<SBZProductDto> products = new ArrayList<>();
|
|
||||||
for (LinkedHashMap product : content) {
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
SBZProductDto productDto = mapper.convertValue(product, SBZProductDto.class);
|
|
||||||
products.add(productDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
return products;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
Provider provider = providerRepository.findById(UUID.fromString(providerUid)).orElse(null);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
if(provider.getRequiresProducts().equals(false)){
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> meta = Utils.fromJson(provider.getMeta(), Map.class);
|
Map<String, String> meta = Utils.fromJson(provider.getMeta(), Map.class);
|
||||||
String providerId = meta.get("hotProductId");
|
String providerId = meta.get("hotProductId");
|
||||||
|
|
||||||
@@ -107,25 +84,28 @@ public class ProviderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List stock = (List) response.get("stock");
|
List stock = (List) response.get("stock");
|
||||||
List<SBZProductDto> products = new ArrayList<>();
|
List<ProductDto> products = new ArrayList<>();
|
||||||
for (Object item : stock) {
|
for (Object item : stock) {
|
||||||
LinkedHashMap<String, Object> stockItem = (LinkedHashMap<String, Object>) item;
|
LinkedHashMap<String, Object> stockItem = (LinkedHashMap<String, Object>) item;
|
||||||
int stockProviderId = (int) stockItem.get("productId"); // providerId is referred to as productId in the stock response
|
int stockProviderId = (int) stockItem.get("productId"); // providerId is referred to as productId in the stock response
|
||||||
if (String.valueOf(stockProviderId).equals(providerId)) {
|
if (String.valueOf(stockProviderId).equals(providerId)) {
|
||||||
log.info("Found matching provider in stock: " + stockItem);
|
|
||||||
|
|
||||||
SBZProductDto sbzProductDto = new SBZProductDto();
|
ProductDto productDto = new ProductDto();
|
||||||
sbzProductDto.setName((String) stockItem.get("name"));
|
productDto.setName((String) stockItem.get("name"));
|
||||||
sbzProductDto.setDescription((String) stockItem.get("description"));
|
productDto.setDescription((String) stockItem.get("description"));
|
||||||
sbzProductDto.setDisplayName((String) stockItem.get("name"));
|
productDto.setDisplayName((String) stockItem.get("name"));
|
||||||
sbzProductDto.setDefaultAmount(new BigDecimal(stockItem.get("amount").toString()));
|
productDto.setDefaultAmount(new BigDecimal(stockItem.get("amount").toString()));
|
||||||
sbzProductDto.setRequiresAmount(true); // always true for now
|
productDto.setRequiresAmount(true); // always true for now
|
||||||
sbzProductDto.setUid(UUID.fromString(Utils.getUid()));
|
productDto.setUid(UUID.fromString(Utils.getUid()));
|
||||||
sbzProductDto.setExternalId((String) stockItem.get("productCode"));
|
productDto.setExternalId((String) stockItem.get("productCode"));
|
||||||
products.add(sbzProductDto);
|
products.add(productDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
providerProductsCache.put(providerUid, products);
|
||||||
|
log.info("Cached products for provider: {}, number of products: {}", providerUid, products.size());
|
||||||
|
log.info("Provider products cache size: {}", providerProductsCache.size());
|
||||||
|
|
||||||
return products;
|
return products;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.error("Error fetching products: {}", providerUid);
|
log.error("Error fetching products: {}", providerUid);
|
||||||
@@ -144,19 +124,17 @@ public class ProviderService {
|
|||||||
return meta.get("hotProductId");
|
return meta.get("hotProductId");
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExternalId(String providerId, String productUuid) {
|
public String getExternalId(String providerId, String productName) {
|
||||||
try {
|
try {
|
||||||
UUID productUid = UUID.fromString(productUuid);
|
List<ProductDto> products = getProviderProducts(providerId);
|
||||||
|
|
||||||
List<SBZProductDto> products = getProviderProducts(providerId);
|
|
||||||
|
|
||||||
return products.stream()
|
return products.stream()
|
||||||
.filter(product -> product.getUid().equals(productUid))
|
.filter(product -> product.getName().equals(productName))
|
||||||
.map(SBZProductDto::getExternalId)
|
.map(ProductDto::getExternalId)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (Exception e) {
|
||||||
log.error("Invalid UUID format: " + productUuid, e);
|
log.error("Failed to fetch external Id: " + productName, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,25 @@ import org.springframework.data.domain.Pageable;
|
|||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import zw.qantra.tm.domain.models.Recipient;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import zw.qantra.tm.domain.models.RecipientGroup;
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileRequest;
|
||||||
import zw.qantra.tm.domain.models.RecipientGroupMember;
|
import zw.qantra.tm.domain.dtos.GroupCreateFromFileResult;
|
||||||
|
import zw.qantra.tm.domain.dtos.ProductDto;
|
||||||
|
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.RecipientGroupMemberRepository;
|
import zw.qantra.tm.domain.repositories.RecipientGroupMemberRepository;
|
||||||
import zw.qantra.tm.domain.repositories.RecipientGroupRepository;
|
import zw.qantra.tm.domain.repositories.RecipientGroupRepository;
|
||||||
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
||||||
import zw.qantra.tm.exceptions.ApiException;
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -24,35 +35,376 @@ public class RecipientGroupService {
|
|||||||
private final RecipientGroupRepository recipientGroupRepository;
|
private final RecipientGroupRepository recipientGroupRepository;
|
||||||
private final RecipientGroupMemberRepository recipientGroupMemberRepository;
|
private final RecipientGroupMemberRepository recipientGroupMemberRepository;
|
||||||
private final RecipientRepository recipientRepository;
|
private final RecipientRepository recipientRepository;
|
||||||
|
private final GroupBatchRepository groupBatchRepository;
|
||||||
|
private final GroupBatchItemRepository groupBatchItemRepository;
|
||||||
|
private final ProviderService providerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a recipient group, creates a batch from it, then updates batch items
|
||||||
|
* with bill/provider fields from the CSV file, and returns the new batch
|
||||||
|
* along with any per-row errors.
|
||||||
|
*
|
||||||
|
* CSV file structure: name, account, phone, billName, billProductName
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Parse CSV rows, collecting per-row validation errors
|
||||||
|
* 2. Create recipient group from all valid rows
|
||||||
|
* 3. Create group batch from the group
|
||||||
|
* 4. Update batch items with billName & billProductName from CSV, skipping rows
|
||||||
|
* where the provider cannot be resolved and recording those errors
|
||||||
|
* 5. Return the batch + any row-level errors
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public GroupCreateFromFileResult createFromFile(MultipartFile file, GroupCreateFromFileRequest request) {
|
||||||
|
ParseResult parseResult = parseCsv(file);
|
||||||
|
|
||||||
|
if(!parseResult.parseErrors.isEmpty()) {
|
||||||
|
return GroupCreateFromFileResult.builder()
|
||||||
|
.totalRows(parseResult.rows.size() + parseResult.parseErrors.size())
|
||||||
|
.errorCount(parseResult.parseErrors.size())
|
||||||
|
.errors(parseResult.parseErrors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseResult.rows.isEmpty()) {
|
||||||
|
throw new ApiException("CSV file has no valid data rows after validation");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: build recipient list from valid CSV rows only
|
||||||
|
List<Recipient> recipients = new ArrayList<>();
|
||||||
|
for (CsvRow row : parseResult.rows) {
|
||||||
|
Recipient recipient = Recipient.builder()
|
||||||
|
.name(row.name)
|
||||||
|
.account(row.account)
|
||||||
|
.phoneNumber(row.phone)
|
||||||
|
.userId(request.getUserId().toString())
|
||||||
|
.workspaceId(request.getWorkspaceId())
|
||||||
|
.build();
|
||||||
|
recipients.add(recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: create recipient group with recipients
|
||||||
|
RecipientGroup group = RecipientGroup.builder()
|
||||||
|
.name(request.getGroupName())
|
||||||
|
.description(request.getDescription())
|
||||||
|
.userId(request.getUserId().toString())
|
||||||
|
.workspaceId(request.getWorkspaceId())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RecipientGroup savedGroup = recipientGroupRepository.save(group);
|
||||||
|
|
||||||
|
List<RecipientGroupMember> members = addMembers(savedGroup.getId(), request.getWorkspaceId(), recipients);
|
||||||
|
|
||||||
|
// Step 3: create group batch (inline to avoid circular dependency with GroupBatchService)
|
||||||
|
GroupBatch batch = createBatchInline(savedGroup.getId(), request.getWorkspaceId(), request, members);
|
||||||
|
|
||||||
|
// Step 4: update batch items with bill/provider fields from CSV
|
||||||
|
List<GroupBatchItem> batchItems = groupBatchItemRepository.findAllByGroupBatchId(batch.getId());
|
||||||
|
|
||||||
|
List<GroupBatchItemUpdate> itemUpdates = new ArrayList<>();
|
||||||
|
List<GroupCreateFromFileResult.RowError> rowErrors = new ArrayList<>(parseResult.parseErrors);
|
||||||
|
|
||||||
|
for (int i = 0; i < batchItems.size() && i < parseResult.rows.size(); i++) {
|
||||||
|
GroupBatchItem batchItem = batchItems.get(i);
|
||||||
|
CsvRow row = parseResult.rows.get(i);
|
||||||
|
|
||||||
|
// Attempt to resolve provider by label
|
||||||
|
Provider provider = providerService.getProviderRepository().findByLabelAndCurrency(
|
||||||
|
row.billLabel, CurrencyType.valueOf(request.getCurrency()));
|
||||||
|
if (provider == null) {
|
||||||
|
rowErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(row.lineNumber)
|
||||||
|
.account(row.account)
|
||||||
|
.name(row.name)
|
||||||
|
.message("Provider not found for bill label: " + row.billLabel)
|
||||||
|
.build());
|
||||||
|
// Skip this row - item remains with default values from batch creation
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemUpdates.add(new GroupBatchItemUpdate(batchItem.getId(), row, provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply successful item updates directly
|
||||||
|
if (!itemUpdates.isEmpty()) {
|
||||||
|
List<GroupCreateFromFileResult.RowError> providerErrors = updateBatchItemsInline(batch.getId(), itemUpdates);
|
||||||
|
if (!providerErrors.isEmpty()) {
|
||||||
|
return GroupCreateFromFileResult.builder()
|
||||||
|
.totalRows(parseResult.rows.size() + parseResult.parseErrors.size())
|
||||||
|
.errorCount(providerErrors.size())
|
||||||
|
.errors(providerErrors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the updated batch + errors
|
||||||
|
GroupBatch resultBatch = groupBatchRepository.findByIdAndWorkspaceId(batch.getId(), request.getWorkspaceId())
|
||||||
|
.orElseThrow(() -> new ApiException("Batch not found after creation"));
|
||||||
|
|
||||||
|
return GroupCreateFromFileResult.builder()
|
||||||
|
.batch(resultBatch)
|
||||||
|
.totalRows(parseResult.rows.size() + parseResult.parseErrors.size())
|
||||||
|
.successCount(itemUpdates.size())
|
||||||
|
.errorCount(rowErrors.size())
|
||||||
|
.errors(rowErrors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GroupBatch and its items directly (mirrors GroupBatchService.createBatch logic
|
||||||
|
* but without circular dependency).
|
||||||
|
*/
|
||||||
|
private GroupBatch createBatchInline(UUID recipientGroupId, UUID workspaceId,
|
||||||
|
GroupCreateFromFileRequest request,
|
||||||
|
List<RecipientGroupMember> members) {
|
||||||
|
GroupBatch batch = GroupBatch.builder()
|
||||||
|
.recipientGroupId(recipientGroupId)
|
||||||
|
.userId(request.getUserId().toString())
|
||||||
|
.workspaceId(workspaceId)
|
||||||
|
.description(request.getDescription())
|
||||||
|
.requestedBy(request.getRequestedBy())
|
||||||
|
.currency(request.getCurrency())
|
||||||
|
.status(GroupBatchStatus.CREATED)
|
||||||
|
.transactionTemplate(Utils.toJson(request.getTransactionTemplate()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
GroupBatch savedBatch = groupBatchRepository.save(batch);
|
||||||
|
|
||||||
|
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())
|
||||||
|
.recipientAccount(recipient.getAccount())
|
||||||
|
.status(GroupBatchItemStatus.NEW)
|
||||||
|
.confirmStatus(GroupBatchItemConfirmStatus.PENDING)
|
||||||
|
.integrationStatus(GroupBatchItemIntegrationStatus.PENDING)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
itemCount++;
|
||||||
|
groupBatchItemRepository.save(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
savedBatch.setCount(itemCount);
|
||||||
|
savedBatch.setValue(BigDecimal.ZERO);
|
||||||
|
savedBatch.setSuccessfulCount(0);
|
||||||
|
savedBatch.setFailedCount(0);
|
||||||
|
return groupBatchRepository.save(savedBatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update batch items with CSV data directly (mirrors GroupBatchService.updateBatchItems logic
|
||||||
|
* but without circular dependency).
|
||||||
|
*/
|
||||||
|
private List<GroupCreateFromFileResult.RowError> updateBatchItemsInline(UUID batchId, List<GroupBatchItemUpdate> updates) {
|
||||||
|
List<GroupBatchItem> existingItems = groupBatchItemRepository.findAllByGroupBatchId(batchId);
|
||||||
|
java.util.Map<UUID, GroupBatchItem> itemMap = new java.util.HashMap<>();
|
||||||
|
for (GroupBatchItem item : existingItems) {
|
||||||
|
itemMap.put(item.getId(), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GroupBatchItem> updatedItems = new ArrayList<>();
|
||||||
|
|
||||||
|
List<GroupCreateFromFileResult.RowError> providerErrors = new ArrayList<>();
|
||||||
|
for (GroupBatchItemUpdate update : updates) {
|
||||||
|
GroupBatchItem item = itemMap.get(update.batchItemId);
|
||||||
|
if (item == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
item.setRecipientName(update.row.name);
|
||||||
|
item.setRecipientPhone(update.row.phone);
|
||||||
|
item.setRecipientAccount(update.row.account);
|
||||||
|
item.setBillName(update.provider.getName());
|
||||||
|
item.setBillProductName(update.row.billProductName);
|
||||||
|
item.setProviderLabel(update.row.billLabel);
|
||||||
|
item.setProviderImage(update.provider.getImage());
|
||||||
|
item.setBillClientId(update.provider.getClientId());
|
||||||
|
|
||||||
|
if(update.row.billProductName.isBlank() && update.row.amount.compareTo(BigDecimal.ZERO) != 0) {
|
||||||
|
item.setAmount(update.row.amount);
|
||||||
|
} else if (!update.row.billProductName.isBlank()) {
|
||||||
|
ProductDto productDto = providerService.getProviderProduct(
|
||||||
|
update.provider.getId(), update.row.billProductName);
|
||||||
|
if( productDto == null) {
|
||||||
|
throw new ApiException("Product not found for provider: " + update.provider.getName() +
|
||||||
|
", product name: " + update.row.billProductName);
|
||||||
|
}
|
||||||
|
item.setAmount(productDto.getDefaultAmount());
|
||||||
|
}
|
||||||
|
updatedItems.add(groupBatchItemRepository.save(item));
|
||||||
|
|
||||||
|
// Recalculate batch totals
|
||||||
|
BigDecimal newTotalValue = updatedItems.stream()
|
||||||
|
.map(GroupBatchItem::getAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
GroupBatch batch = groupBatchRepository.findById(batchId)
|
||||||
|
.orElseThrow(() -> new ApiException("Batch not found"));
|
||||||
|
batch.setValue(newTotalValue);
|
||||||
|
batch.setCount(updatedItems.size());
|
||||||
|
groupBatchRepository.save(batch);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
providerErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(update.row.lineNumber)
|
||||||
|
.account(update.row.account)
|
||||||
|
.name(update.row.name)
|
||||||
|
.message("Failed to update batch item: " + e.getMessage())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record GroupBatchItemUpdate(UUID batchItemId, CsvRow row, Provider provider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse CSV rows, collecting validation errors per row rather than failing fast.
|
||||||
|
*/
|
||||||
|
private ParseResult parseCsv(MultipartFile file) {
|
||||||
|
List<CsvRow> rows = new ArrayList<>();
|
||||||
|
List<GroupCreateFromFileResult.RowError> parseErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
try (Reader reader = new InputStreamReader(file.getInputStream());
|
||||||
|
BufferedReader br = new BufferedReader(reader)) {
|
||||||
|
|
||||||
|
String headerLine = br.readLine(); // skip header
|
||||||
|
if (headerLine == null) {
|
||||||
|
throw new ApiException("CSV file is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
String line;
|
||||||
|
int lineNum = 1;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
lineNum++;
|
||||||
|
if (line.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] parts = parseCsvLine(line);
|
||||||
|
if (parts.length < 5) {
|
||||||
|
parseErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(lineNum)
|
||||||
|
.message("Invalid CSV format: expected at least 5 columns (name, account, phone, billName, billProductName) but got " + parts.length)
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = parts[0].trim();
|
||||||
|
String account = parts[1].trim();
|
||||||
|
String phone = parts[2].trim();
|
||||||
|
String billLabel = parts[3].trim();
|
||||||
|
String billProductName = parts[4].trim();
|
||||||
|
String amount = parts[5].trim();
|
||||||
|
|
||||||
|
if (name.isEmpty() || account.isEmpty()) {
|
||||||
|
parseErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(lineNum)
|
||||||
|
.account(account)
|
||||||
|
.name(name)
|
||||||
|
.message("name and account are required")
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(amount.isEmpty() && billProductName.isEmpty()) {
|
||||||
|
parseErrors.add(GroupCreateFromFileResult.RowError.builder()
|
||||||
|
.lineNumber(lineNum)
|
||||||
|
.account(account)
|
||||||
|
.name(name)
|
||||||
|
.message("amount or billProductName is required")
|
||||||
|
.build());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CsvRow row = new CsvRow();
|
||||||
|
row.lineNumber = lineNum;
|
||||||
|
row.name = name;
|
||||||
|
row.account = account;
|
||||||
|
row.phone = phone;
|
||||||
|
row.billLabel = billLabel;
|
||||||
|
row.billProductName = billProductName;
|
||||||
|
row.amount = !amount.isEmpty() ? new BigDecimal(amount) : BigDecimal.ZERO;
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.isEmpty() && parseErrors.isEmpty()) {
|
||||||
|
throw new ApiException("CSV file has no data rows");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ApiException("Failed to read CSV file: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return new ParseResult(rows, parseErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a CSV line respecting simple double-quoted fields.
|
||||||
|
*/
|
||||||
|
private String[] parseCsvLine(String line) {
|
||||||
|
List<String> fields = new ArrayList<>();
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
boolean inQuotes = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < line.length(); i++) {
|
||||||
|
char c = line.charAt(i);
|
||||||
|
if (c == '"') {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
} else if (c == ',' && !inQuotes) {
|
||||||
|
fields.add(current.toString());
|
||||||
|
current = new StringBuilder();
|
||||||
|
} else {
|
||||||
|
current.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.add(current.toString());
|
||||||
|
return fields.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CsvRow {
|
||||||
|
int lineNumber;
|
||||||
|
String name;
|
||||||
|
String account;
|
||||||
|
String phone;
|
||||||
|
String billLabel;
|
||||||
|
String billProductName;
|
||||||
|
BigDecimal amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParseResult(List<CsvRow> rows, List<GroupCreateFromFileResult.RowError> parseErrors) {}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public RecipientGroup create(RecipientGroup group, List<Recipient> recipients) {
|
public RecipientGroup create(RecipientGroup group, List<Recipient> recipients) {
|
||||||
RecipientGroup savedGroup = recipientGroupRepository.save(group);
|
RecipientGroup savedGroup = recipientGroupRepository.save(group);
|
||||||
|
|
||||||
if (recipients != null && !recipients.isEmpty()) {
|
if (recipients != null && !recipients.isEmpty()) {
|
||||||
addMembers(savedGroup.getId(), savedGroup.getUserId(), recipients);
|
addMembers(savedGroup.getId(), savedGroup.getWorkspaceId(), recipients);
|
||||||
}
|
}
|
||||||
|
|
||||||
return savedGroup;
|
return savedGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<RecipientGroupMember> addMembers(UUID groupId, String userId, List<Recipient> recipients) {
|
public List<RecipientGroupMember> addMembers(UUID groupId, UUID workspaceId, List<Recipient> recipients) {
|
||||||
get(groupId, userId);
|
get(groupId, workspaceId);
|
||||||
List<RecipientGroupMember> added = new ArrayList<>();
|
List<RecipientGroupMember> added = new ArrayList<>();
|
||||||
for (Recipient incomingRecipient : recipients) {
|
for (Recipient incomingRecipient : recipients) {
|
||||||
Recipient recipient = resolveOrCreateRecipient(incomingRecipient, userId);
|
Recipient recipient = resolveOrCreateRecipient(incomingRecipient, workspaceId);
|
||||||
|
|
||||||
if (recipientGroupMemberRepository.existsByRecipientGroupIdAndRecipientId(groupId, recipient.getId())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecipientGroupMember member = RecipientGroupMember.builder()
|
RecipientGroupMember member = RecipientGroupMember.builder()
|
||||||
.recipientGroupId(groupId)
|
.recipientGroupId(groupId)
|
||||||
.recipientUid(recipient.getId())
|
.recipientUid(recipient.getId())
|
||||||
.recipient(recipient)
|
.recipient(recipient)
|
||||||
.account(recipient.getAccount())
|
.account(recipient.getAccount())
|
||||||
.userId(userId)
|
.workspaceId(workspaceId)
|
||||||
|
.userId(incomingRecipient.getUserId())
|
||||||
.build();
|
.build();
|
||||||
added.add(recipientGroupMemberRepository.save(member));
|
added.add(recipientGroupMemberRepository.save(member));
|
||||||
}
|
}
|
||||||
@@ -63,25 +415,25 @@ public class RecipientGroupService {
|
|||||||
return recipientGroupRepository.findAll(spec, pageable);
|
return recipientGroupRepository.findAll(spec, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<RecipientGroup> list(String userId) {
|
public List<RecipientGroup> list(UUID workspaceId) {
|
||||||
return recipientGroupRepository.findAllByUserId(userId);
|
return recipientGroupRepository.findAllByWorkspaceId(workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientGroup get(UUID groupId, String userId) {
|
public RecipientGroup get(UUID groupId, UUID workspaceId) {
|
||||||
return recipientGroupRepository.findByIdAndUserId(groupId, userId)
|
return recipientGroupRepository.findByIdAndWorkspaceId(groupId, workspaceId)
|
||||||
.orElseThrow(() -> new ApiException("Recipient group not found"));
|
.orElseThrow(() -> new ApiException("Recipient group not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipientGroup update(UUID groupId, String userId, String name, String description) {
|
public RecipientGroup update(UUID groupId, UUID workspaceId, String name, String description) {
|
||||||
RecipientGroup group = get(groupId, userId);
|
RecipientGroup group = get(groupId, workspaceId);
|
||||||
group.setName(name);
|
group.setName(name);
|
||||||
group.setDescription(description);
|
group.setDescription(description);
|
||||||
return recipientGroupRepository.save(group);
|
return recipientGroupRepository.save(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void delete(UUID groupId, String userId) {
|
public void delete(UUID groupId, UUID workspaceId) {
|
||||||
RecipientGroup group = get(groupId, userId);
|
RecipientGroup group = get(groupId, workspaceId);
|
||||||
group.setDeleted(true);
|
group.setDeleted(true);
|
||||||
recipientGroupRepository.save(group);
|
recipientGroupRepository.save(group);
|
||||||
|
|
||||||
@@ -91,14 +443,14 @@ public class RecipientGroupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<RecipientGroupMember> addMembersByIds(UUID groupId, String userId, List<UUID> recipientIds) {
|
public List<RecipientGroupMember> addMembersByIds(UUID groupId, UUID workspaceId, List<UUID> recipientIds) {
|
||||||
get(groupId, userId);
|
get(groupId, workspaceId);
|
||||||
List<RecipientGroupMember> added = new ArrayList<>();
|
List<RecipientGroupMember> added = new ArrayList<>();
|
||||||
for (UUID recipientId : recipientIds) {
|
for (UUID recipientId : recipientIds) {
|
||||||
Recipient recipient = recipientRepository.findById(recipientId)
|
Recipient recipient = recipientRepository.findById(recipientId)
|
||||||
.orElseThrow(() -> new ApiException("Recipient not found: " + recipientId));
|
.orElseThrow(() -> new ApiException("Recipient not found: " + recipientId));
|
||||||
if (!userId.equals(recipient.getUserId())) {
|
if (!workspaceId.equals(recipient.getWorkspaceId())) {
|
||||||
throw new ApiException("Recipient does not belong to user");
|
throw new ApiException("Recipient does not belong to workspace");
|
||||||
}
|
}
|
||||||
if (recipientGroupMemberRepository.existsByRecipientGroupIdAndRecipientId(groupId, recipientId)) {
|
if (recipientGroupMemberRepository.existsByRecipientGroupIdAndRecipientId(groupId, recipientId)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -108,35 +460,38 @@ public class RecipientGroupService {
|
|||||||
.recipientUid(recipientId)
|
.recipientUid(recipientId)
|
||||||
.recipient(recipient)
|
.recipient(recipient)
|
||||||
.account(recipient.getAccount())
|
.account(recipient.getAccount())
|
||||||
.userId(userId)
|
.workspaceId(workspaceId)
|
||||||
.build();
|
.build();
|
||||||
added.add(recipientGroupMemberRepository.save(member));
|
added.add(recipientGroupMemberRepository.save(member));
|
||||||
}
|
}
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Recipient resolveOrCreateRecipient(Recipient incomingRecipient, String fallbackUserId) {
|
private Recipient resolveOrCreateRecipient(Recipient incomingRecipient, UUID fallbackWorkspaceId) {
|
||||||
if (incomingRecipient == null || incomingRecipient.getAccount() == null || incomingRecipient.getAccount().isBlank()) {
|
if (incomingRecipient == null || incomingRecipient.getAccount() == null || incomingRecipient.getAccount().isBlank()) {
|
||||||
throw new ApiException("Recipient account is required");
|
throw new ApiException("Recipient account is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
String recipientUserId = incomingRecipient.getUserId() == null || incomingRecipient.getUserId().isBlank()
|
UUID recipientWorkspaceId = incomingRecipient.getWorkspaceId() == null
|
||||||
? fallbackUserId
|
? fallbackWorkspaceId
|
||||||
: incomingRecipient.getUserId();
|
: incomingRecipient.getWorkspaceId();
|
||||||
|
|
||||||
List<Recipient> existing = recipientRepository.findByAccountAndUserId(incomingRecipient.getAccount(), recipientUserId);
|
List<Recipient> existing = recipientRepository.findByAccountAndWorkspaceId(incomingRecipient.getAccount(), recipientWorkspaceId);
|
||||||
if (!existing.isEmpty()) {
|
if (!existing.isEmpty()) {
|
||||||
return existing.get(0);
|
Recipient recipient = existing.get(0);
|
||||||
|
recipient.setName(incomingRecipient.getName());
|
||||||
|
recipient.setPhoneNumber(incomingRecipient.getPhoneNumber());
|
||||||
|
return recipientRepository.save(recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
incomingRecipient.setId(null);
|
incomingRecipient.setId(null);
|
||||||
incomingRecipient.setUserId(recipientUserId);
|
incomingRecipient.setWorkspaceId(recipientWorkspaceId);
|
||||||
return recipientRepository.save(incomingRecipient);
|
return recipientRepository.save(incomingRecipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void removeMember(UUID groupId, UUID recipientId, String userId) {
|
public void removeMember(UUID groupId, UUID recipientId, UUID workspaceId) {
|
||||||
get(groupId, userId);
|
get(groupId, workspaceId);
|
||||||
List<RecipientGroupMember> members = recipientGroupMemberRepository.findAllByRecipientGroupId(groupId);
|
List<RecipientGroupMember> members = recipientGroupMemberRepository.findAllByRecipientGroupId(groupId);
|
||||||
RecipientGroupMember target = members.stream()
|
RecipientGroupMember target = members.stream()
|
||||||
.filter(item -> recipientId.equals(item.getRecipientUid()))
|
.filter(item -> recipientId.equals(item.getRecipientUid()))
|
||||||
@@ -146,8 +501,8 @@ public class RecipientGroupService {
|
|||||||
recipientGroupMemberRepository.save(target);
|
recipientGroupMemberRepository.save(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<RecipientGroupMember> members(UUID groupId, String userId) {
|
public List<RecipientGroupMember> members(UUID groupId, UUID workspaceId) {
|
||||||
get(groupId, userId);
|
get(groupId, workspaceId);
|
||||||
return recipientGroupMemberRepository.findAllByRecipientGroupId(groupId);
|
return recipientGroupMemberRepository.findAllByRecipientGroupId(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,4 +510,3 @@ public class RecipientGroupService {
|
|||||||
return recipientGroupMemberRepository.findAll(spec, pageable);
|
return recipientGroupMemberRepository.findAll(spec, pageable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package zw.qantra.tm.domain.services;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import zw.qantra.tm.domain.models.Recipient;
|
import zw.qantra.tm.domain.models.Recipient;
|
||||||
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
import zw.qantra.tm.domain.repositories.RecipientRepository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -21,8 +22,8 @@ public class RecipientService {
|
|||||||
return recipientRepository.findAll();
|
return recipientRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Recipient> getRecipient(String account, String userId) {
|
public List<Recipient> getRecipient(String account, UUID workspaceId) {
|
||||||
return recipientRepository.findByAccountAndUserId(account, userId);
|
return recipientRepository.findByAccountAndWorkspaceId(account, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recipient save(Recipient recipient) {
|
public Recipient save(Recipient recipient) {
|
||||||
@@ -45,7 +46,7 @@ public class RecipientService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Recipient> searchRecipients(Specification<Recipient> specification) {
|
public Page<Recipient> findAllRecipients(Specification<Recipient> specification, Pageable pageable) {
|
||||||
return recipientRepository.findAll(specification);
|
return recipientRepository.findAll(specification, pageable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import zw.qantra.tm.domain.enums.PartyType;
|
||||||
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
|
import zw.qantra.tm.domain.repositories.TransactionRepository;
|
||||||
|
|
||||||
|
import jakarta.persistence.criteria.Predicate;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TransactionReportService {
|
||||||
|
|
||||||
|
private static final String REPORT_FILENAME_PREFIX = "transaction-report-";
|
||||||
|
private static final String REPORT_FILENAME_SUFFIX = ".xlsx";
|
||||||
|
private static final short DATA_FONT_SIZE = 12;
|
||||||
|
private static final short HEADER_FONT_SIZE = 14;
|
||||||
|
private static final short TITLE_FONT_SIZE = 16;
|
||||||
|
|
||||||
|
@Value("${app.reports.storage-path:./reports}")
|
||||||
|
private String reportsStoragePath;
|
||||||
|
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a transaction report filtered by date range and party type.
|
||||||
|
* The default filter restricts results to transactions where partyType is
|
||||||
|
* either INDIVIDUAL or GROUP (excluding BATCH).
|
||||||
|
*
|
||||||
|
* @param startDate the start of the date range (inclusive)
|
||||||
|
* @param endDate the end of the date range (inclusive)
|
||||||
|
* @return list of transactions matching the criteria
|
||||||
|
*/
|
||||||
|
public List<Transaction> generateReport(LocalDateTime startDate, LocalDateTime endDate) {
|
||||||
|
return generateReport(startDate, endDate, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a transaction report filtered by date range, party type, and an
|
||||||
|
* optional workspace ID.
|
||||||
|
*
|
||||||
|
* @param startDate the start of the date range (inclusive)
|
||||||
|
* @param endDate the end of the date range (inclusive)
|
||||||
|
* @param workspaceId optional workspace ID to further filter transactions
|
||||||
|
* @return list of transactions matching the criteria
|
||||||
|
*/
|
||||||
|
public List<Transaction> generateReport(LocalDateTime startDate, LocalDateTime endDate,
|
||||||
|
String workspaceId) {
|
||||||
|
Specification<Transaction> spec = (root, query, cb) -> {
|
||||||
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
|
// Default filter: partyType must be INDIVIDUAL or GROUP
|
||||||
|
predicates.add(root.get("partyType").in(PartyType.INDIVIDUAL, PartyType.GROUP));
|
||||||
|
|
||||||
|
// Date range filter on createdAt
|
||||||
|
if (startDate != null) {
|
||||||
|
predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), startDate));
|
||||||
|
}
|
||||||
|
if (endDate != null) {
|
||||||
|
predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional workspace filter
|
||||||
|
if (workspaceId != null && !workspaceId.isEmpty()) {
|
||||||
|
predicates.add(cb.equal(root.get("workspaceId").as(String.class), workspaceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb.and(predicates.toArray(new Predicate[0]));
|
||||||
|
};
|
||||||
|
|
||||||
|
return transactionRepository.findAll(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a transaction report and returns a summary with counts and totals.
|
||||||
|
*
|
||||||
|
* @param startDate the start of the date range (inclusive)
|
||||||
|
* @param endDate the end of the date range (inclusive)
|
||||||
|
* @return a report summary containing transactions and aggregated data
|
||||||
|
*/
|
||||||
|
public TransactionReportSummary generateReportSummary(LocalDateTime startDate, LocalDateTime endDate) {
|
||||||
|
return generateReportSummary(startDate, endDate, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a transaction report summary with an optional workspace filter.
|
||||||
|
*
|
||||||
|
* @param startDate the start of the date range (inclusive)
|
||||||
|
* @param endDate the end of the date range (inclusive)
|
||||||
|
* @param workspaceId optional workspace ID to further filter transactions
|
||||||
|
* @return a report summary containing transactions and aggregated data
|
||||||
|
*/
|
||||||
|
public TransactionReportSummary generateReportSummary(LocalDateTime startDate, LocalDateTime endDate,
|
||||||
|
String workspaceId) {
|
||||||
|
List<Transaction> transactions = generateReport(startDate, endDate, workspaceId);
|
||||||
|
|
||||||
|
int totalCount = transactions.size();
|
||||||
|
int individualCount = 0;
|
||||||
|
int groupCount = 0;
|
||||||
|
|
||||||
|
for (Transaction txn : transactions) {
|
||||||
|
if (txn.getPartyType() == PartyType.INDIVIDUAL) {
|
||||||
|
individualCount++;
|
||||||
|
} else if (txn.getPartyType() == PartyType.GROUP) {
|
||||||
|
groupCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TransactionReportSummary(
|
||||||
|
transactions,
|
||||||
|
totalCount,
|
||||||
|
individualCount,
|
||||||
|
groupCount,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an Excel workbook from a transaction report summary and returns the bytes.
|
||||||
|
*
|
||||||
|
* @param summary the report summary with transaction data
|
||||||
|
* @return byte array of the Excel file
|
||||||
|
*/
|
||||||
|
public byte[] generateExcelBytes(TransactionReportSummary summary) {
|
||||||
|
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
try (Workbook workbook = new XSSFWorkbook()) {
|
||||||
|
Sheet sheet = workbook.createSheet("Transaction Report");
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
CellStyle titleStyle = workbook.createCellStyle();
|
||||||
|
Font titleFont = workbook.createFont();
|
||||||
|
titleFont.setBold(true);
|
||||||
|
titleFont.setFontHeightInPoints(TITLE_FONT_SIZE);
|
||||||
|
titleStyle.setFont(titleFont);
|
||||||
|
titleStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
titleStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
|
||||||
|
CellStyle headerStyle = workbook.createCellStyle();
|
||||||
|
headerStyle.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
|
||||||
|
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
Font headerFont = workbook.createFont();
|
||||||
|
headerFont.setBold(true);
|
||||||
|
headerFont.setFontHeightInPoints(HEADER_FONT_SIZE);
|
||||||
|
headerFont.setColor(IndexedColors.WHITE.getIndex());
|
||||||
|
headerStyle.setFont(headerFont);
|
||||||
|
headerStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
|
||||||
|
CellStyle dataStyle = workbook.createCellStyle();
|
||||||
|
Font dataFont = workbook.createFont();
|
||||||
|
dataFont.setFontHeightInPoints(DATA_FONT_SIZE);
|
||||||
|
dataStyle.setFont(dataFont);
|
||||||
|
dataStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||||
|
dataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
dataStyle.setBorderTop(BorderStyle.THIN);
|
||||||
|
dataStyle.setBorderBottom(BorderStyle.THIN);
|
||||||
|
dataStyle.setBorderLeft(BorderStyle.THIN);
|
||||||
|
dataStyle.setBorderRight(BorderStyle.THIN);
|
||||||
|
|
||||||
|
CellStyle currencyStyle = workbook.createCellStyle();
|
||||||
|
currencyStyle.cloneStyleFrom(dataStyle);
|
||||||
|
currencyStyle.setDataFormat(workbook.createDataFormat().getFormat("#,##0.00"));
|
||||||
|
|
||||||
|
CellStyle summaryLabelStyle = workbook.createCellStyle();
|
||||||
|
summaryLabelStyle.setFont(dataFont);
|
||||||
|
summaryLabelStyle.setAlignment(HorizontalAlignment.RIGHT);
|
||||||
|
summaryLabelStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
|
||||||
|
CellStyle summaryValueStyle = workbook.createCellStyle();
|
||||||
|
Font summaryValueFont = workbook.createFont();
|
||||||
|
summaryValueFont.setBold(true);
|
||||||
|
summaryValueFont.setFontHeightInPoints(DATA_FONT_SIZE);
|
||||||
|
summaryValueStyle.setFont(summaryValueFont);
|
||||||
|
summaryValueStyle.setAlignment(HorizontalAlignment.LEFT);
|
||||||
|
summaryValueStyle.setVerticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
|
||||||
|
int rowNum = 0;
|
||||||
|
|
||||||
|
// Title row
|
||||||
|
Row titleRow = sheet.createRow(rowNum++);
|
||||||
|
Cell titleCell = titleRow.createCell(0);
|
||||||
|
String title = String.format("Transaction Report (%s - %s)",
|
||||||
|
summary.startDate().format(dateFormatter),
|
||||||
|
summary.endDate().format(dateFormatter));
|
||||||
|
titleCell.setCellValue(title);
|
||||||
|
titleCell.setCellStyle(titleStyle);
|
||||||
|
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 11));
|
||||||
|
titleRow.setHeightInPoints(30);
|
||||||
|
|
||||||
|
rowNum++;
|
||||||
|
|
||||||
|
// Summary rows
|
||||||
|
writeSummaryRow(sheet, rowNum++, "Total Transactions:", String.valueOf(summary.totalCount()), summaryLabelStyle, summaryValueStyle);
|
||||||
|
writeSummaryRow(sheet, rowNum++, "INDIVIDUAL:", String.valueOf(summary.individualCount()), summaryLabelStyle, summaryValueStyle);
|
||||||
|
writeSummaryRow(sheet, rowNum++, "GROUP:", String.valueOf(summary.groupCount()), summaryLabelStyle, summaryValueStyle);
|
||||||
|
|
||||||
|
rowNum++;
|
||||||
|
|
||||||
|
// Header row
|
||||||
|
String[] headers = {
|
||||||
|
"#", "Reference", "Party Type", "Amount", "Charge",
|
||||||
|
"Total Amount", "Currency", "Payment Status", "Integration Status", "Payment Processor Ref",
|
||||||
|
"Username", "Credit Phone",
|
||||||
|
"Velocity Ref", "Credit Name", "Provider", "Error Message", "Date", "Batch Name"
|
||||||
|
};
|
||||||
|
|
||||||
|
Row headerRow = sheet.createRow(rowNum++);
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
Cell cell = headerRow.createCell(i);
|
||||||
|
cell.setCellValue(headers[i]);
|
||||||
|
cell.setCellStyle(headerStyle);
|
||||||
|
}
|
||||||
|
headerRow.setHeightInPoints(25);
|
||||||
|
|
||||||
|
// Data rows
|
||||||
|
int index = 1;
|
||||||
|
for (Transaction txn : summary.transactions()) {
|
||||||
|
Row row = sheet.createRow(rowNum++);
|
||||||
|
|
||||||
|
writeCell(row, 0, index++, dataStyle);
|
||||||
|
writeCell(row, 1, txn.getReference() != null ? txn.getReference() : "", dataStyle);
|
||||||
|
writeCell(row, 2, txn.getPartyType() != null ? txn.getPartyType().name() : "", dataStyle);
|
||||||
|
writeCellAmount(row, 3, txn.getAmount(), currencyStyle);
|
||||||
|
writeCellAmount(row, 4, txn.getGatewayCharge(), currencyStyle);
|
||||||
|
writeCellAmount(row, 5, txn.getTotalAmount(), currencyStyle);
|
||||||
|
writeCell(row, 6, txn.getDebitCurrency().name(), dataStyle);
|
||||||
|
writeCell(row, 7, txn.getPollingStatus() != null ? txn.getPollingStatus().name() : "", dataStyle);
|
||||||
|
writeCell(row, 8, txn.getIntegrationStatus() != null ? txn.getIntegrationStatus().name() : "", dataStyle);
|
||||||
|
writeCell(row, 9, txn.getPaymentProcessorRef() != null ? txn.getPaymentProcessorRef() : "", dataStyle);
|
||||||
|
writeCell(row, 10, txn.getUsername() != null ? txn.getUsername() : txn.getDebitPhone(), dataStyle);
|
||||||
|
writeCell(row, 11, txn.getCreditPhone() != null ? txn.getCreditPhone() : "", dataStyle);
|
||||||
|
writeCell(row, 12, txn.getErpSalesPaymentRef() != null ? txn.getErpSalesPaymentRef() : "", dataStyle);
|
||||||
|
writeCell(row, 13, txn.getCreditName() != null ? txn.getCreditName() : "", dataStyle);
|
||||||
|
writeCell(row, 14, txn.getProviderLabel() != null ? txn.getProviderLabel() : "", dataStyle);
|
||||||
|
writeCell(row, 15, txn.getErrorMessage() != null ? txn.getErrorMessage() : "", dataStyle);
|
||||||
|
writeCell(row, 16, txn.getCreatedAt() != null ? txn.getCreatedAt().format(dateFormatter) : "", dataStyle);
|
||||||
|
writeCell(row, 17, txn.getBatchDescription() != null ? txn.getBatchDescription() : "", dataStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-size columns
|
||||||
|
for (int i = 0; i < headers.length; i++) {
|
||||||
|
sheet.autoSizeColumn(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
workbook.write(outputStream);
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to generate transaction report Excel file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a transaction report Excel file, saves it to disk, and returns the file URL.
|
||||||
|
*
|
||||||
|
* @param startDate the start of the date range (inclusive)
|
||||||
|
* @param endDate the end of the date range (inclusive)
|
||||||
|
* @param workspaceId optional workspace ID filter
|
||||||
|
* @return result containing the file URL and byte contents
|
||||||
|
*/
|
||||||
|
public TransactionReportFileResult generateReportFile(LocalDateTime startDate, LocalDateTime endDate,
|
||||||
|
String workspaceId) {
|
||||||
|
TransactionReportSummary summary = generateReportSummary(startDate, endDate, workspaceId);
|
||||||
|
byte[] fileBytes = generateExcelBytes(summary);
|
||||||
|
|
||||||
|
String datePart = startDate.format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
|
||||||
|
String filename = REPORT_FILENAME_PREFIX + datePart + REPORT_FILENAME_SUFFIX;
|
||||||
|
java.nio.file.Path reportsDir = java.nio.file.Path.of(reportsStoragePath).toAbsolutePath();
|
||||||
|
java.nio.file.Path filePath = reportsDir.resolve(filename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.nio.file.Files.createDirectories(reportsDir);
|
||||||
|
try (java.io.OutputStream os = java.nio.file.Files.newOutputStream(filePath)) {
|
||||||
|
os.write(fileBytes);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to persist transaction report file", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TransactionReportFileResult(filename, fileBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeSummaryRow(Sheet sheet, int rowNum, String label, String value,
|
||||||
|
CellStyle labelStyle, CellStyle valueStyle) {
|
||||||
|
Row row = sheet.createRow(rowNum);
|
||||||
|
Cell labelCell = row.createCell(0);
|
||||||
|
labelCell.setCellValue(label);
|
||||||
|
labelCell.setCellStyle(labelStyle);
|
||||||
|
|
||||||
|
Cell valueCell = row.createCell(1);
|
||||||
|
valueCell.setCellValue(value);
|
||||||
|
valueCell.setCellStyle(valueStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCell(Row row, int col, String value, CellStyle style) {
|
||||||
|
Cell cell = row.createCell(col);
|
||||||
|
cell.setCellValue(value);
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCell(Row row, int col, int value, CellStyle style) {
|
||||||
|
Cell cell = row.createCell(col);
|
||||||
|
cell.setCellValue(value);
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCellAmount(Row row, int col, java.math.BigDecimal value, CellStyle style) {
|
||||||
|
Cell cell = row.createCell(col);
|
||||||
|
cell.setCellValue(value != null ? value.doubleValue() : 0.0);
|
||||||
|
cell.setCellStyle(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a transaction report file generation request.
|
||||||
|
*/
|
||||||
|
public record TransactionReportFileResult(
|
||||||
|
String filename,
|
||||||
|
byte[] fileBytes
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary of a transaction report.
|
||||||
|
*/
|
||||||
|
public record TransactionReportSummary(
|
||||||
|
List<Transaction> transactions,
|
||||||
|
int totalCount,
|
||||||
|
int individualCount,
|
||||||
|
int groupCount,
|
||||||
|
LocalDateTime startDate,
|
||||||
|
LocalDateTime endDate
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -38,25 +38,44 @@ public class TransactionService {
|
|||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
Currency currency = currencyService.findCurrencyByIsoCode(transaction.getCreditCurrency().name());
|
Currency debitCurrency = currencyService.findCurrencyByIsoCode(transaction.getDebitCurrency().name());
|
||||||
|
Currency creditCurrency = currencyService.findCurrencyByIsoCode(transaction.getCreditCurrency().name());
|
||||||
|
|
||||||
BigDecimal bankRate = new BigDecimal(currency.getBankRate());
|
BigDecimal debitBankRate = new BigDecimal(debitCurrency.getBankRate());
|
||||||
BigDecimal creditAmount = transaction.getAmount().multiply(bankRate).setScale(4, RoundingMode.HALF_EVEN);
|
BigDecimal creditBankRate = new BigDecimal(creditCurrency.getBankRate());
|
||||||
|
|
||||||
|
BigDecimal bankRate = creditBankRate.divide(debitBankRate, 4, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
BigDecimal creditAmount = transaction.getAmount().multiply(bankRate).setScale(4, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
transaction.setCreditAmount(creditAmount);
|
transaction.setCreditAmount(creditAmount);
|
||||||
return creditAmount;
|
return creditAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reassignTransactions(String oldUid, String newUid){
|
public void reassignTransactions(UUID oldWorkspaceId, UUID newWorkspaceId){
|
||||||
List<Transaction> transactions = transactionRepository.findAllByUserId(oldUid);
|
List<Transaction> transactions = transactionRepository.findAllByWorkspaceId(oldWorkspaceId);
|
||||||
transactions.forEach(transaction -> {
|
transactions.forEach(transaction -> {
|
||||||
transaction.setUserId(newUid);
|
transaction.setWorkspaceId(newWorkspaceId);
|
||||||
transactionRepository.save(transaction);
|
transactionRepository.save(transaction);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reassignTransactions(String oldUserId, String newUserId){
|
||||||
|
List<Transaction> transactions = transactionRepository.findAllByUserId(oldUserId);
|
||||||
|
transactions.forEach(transaction -> {
|
||||||
|
transaction.setUserId(newUserId);
|
||||||
|
transactionRepository.save(transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transaction getTransaction(UUID id, UUID workspaceId) {
|
||||||
|
return transactionRepository.findByIdAndWorkspaceId(id, workspaceId)
|
||||||
|
.orElseThrow(() -> new ApiException("Transaction not found"));
|
||||||
|
}
|
||||||
|
|
||||||
public Transaction findById(UUID id) {
|
public Transaction findById(UUID id) {
|
||||||
Transaction transaction = transactionRepository.findById(id).orElseThrow(() -> new ApiException("Transaction not found for ID: " + id));
|
Transaction transaction = transactionRepository.findById(id).orElseThrow(() ->
|
||||||
|
new ApiException("Transaction not found for ID: " + id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
AdditionalData additionalData = additionalDataService.getAdditionalDataRepository()
|
AdditionalData additionalData = additionalDataService.getAdditionalDataRepository()
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import zw.qantra.tm.domain.models.Uptime;
|
||||||
|
import zw.qantra.tm.domain.repositories.UptimeRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UptimeService {
|
||||||
|
private final UptimeRepository uptimeRepository;
|
||||||
|
|
||||||
|
public Page<Uptime> getAllUptimes(Pageable pageable) {
|
||||||
|
return uptimeRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Uptime> getUptime(UUID id) {
|
||||||
|
return uptimeRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uptime createUptime(Uptime uptime) {
|
||||||
|
return uptimeRepository.save(uptime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uptime updateUptime(UUID id, Uptime uptime) {
|
||||||
|
if (uptimeRepository.existsById(id)) {
|
||||||
|
uptime.setId(id);
|
||||||
|
return uptimeRepository.save(uptime);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteUptime(UUID id) {
|
||||||
|
if (uptimeRepository.existsById(id)) {
|
||||||
|
uptimeRepository.deleteById(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,27 @@ package zw.qantra.tm.domain.services;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
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.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import zw.qantra.tm.domain.dtos.AuthResponse;
|
import zw.qantra.tm.domain.dtos.AuthResponse;
|
||||||
import zw.qantra.tm.domain.dtos.LoginRequest;
|
import zw.qantra.tm.domain.dtos.LoginRequest;
|
||||||
import zw.qantra.tm.domain.dtos.RegisterRequest;
|
import zw.qantra.tm.domain.dtos.RegisterRequest;
|
||||||
import zw.qantra.tm.domain.models.User;
|
import zw.qantra.tm.domain.models.User;
|
||||||
|
import zw.qantra.tm.domain.models.Workspace;
|
||||||
import zw.qantra.tm.domain.repositories.UserRepository;
|
import zw.qantra.tm.domain.repositories.UserRepository;
|
||||||
import zw.qantra.tm.utils.JwtUtils;
|
import zw.qantra.tm.utils.JwtUtils;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserService implements UserDetailsService {
|
public class UserService implements UserDetailsService {
|
||||||
@@ -23,6 +32,7 @@ public class UserService implements UserDetailsService {
|
|||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final JwtUtils jwtUtils;
|
private final JwtUtils jwtUtils;
|
||||||
private final TransactionService transactionService;
|
private final TransactionService transactionService;
|
||||||
|
private final WorkspaceService workspaceService;
|
||||||
|
|
||||||
public User fetchUserByUsername(String username) {
|
public User fetchUserByUsername(String username) {
|
||||||
return userRepository.findByUsername(username)
|
return userRepository.findByUsername(username)
|
||||||
@@ -83,4 +93,52 @@ public class UserService implements UserDetailsService {
|
|||||||
return new AuthResponse(token, "Bearer", user.getUsername(),
|
return new AuthResponse(token, "Bearer", user.getUsername(),
|
||||||
user.getEmail(), user.getPhone(), user.getFirstName(), user.getLastName(), user.getId());
|
user.getEmail(), user.getPhone(), user.getFirstName(), user.getLastName(), user.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Page<User> findAllUsers(Specification<User> spec, Pageable pageable) {
|
||||||
|
return userRepository.findAll(spec, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<User> getUser(UUID id) {
|
||||||
|
return userRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User createUser(User user) {
|
||||||
|
user.setPassword(passwordEncoder.encode(user.getPassword()));
|
||||||
|
user.setDeleted(false);
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User updateUser(UUID id, User userDetails) {
|
||||||
|
return userRepository.findById(id)
|
||||||
|
.map(existingUser -> {
|
||||||
|
existingUser.setFirstName(userDetails.getFirstName());
|
||||||
|
existingUser.setLastName(userDetails.getLastName());
|
||||||
|
existingUser.setEmail(userDetails.getEmail());
|
||||||
|
existingUser.setPhone(userDetails.getPhone());
|
||||||
|
existingUser.setPhotoUrl(userDetails.getPhotoUrl());
|
||||||
|
existingUser.setWorkflowId(userDetails.getWorkflowId());
|
||||||
|
existingUser.setEnabled(userDetails.isEnabled());
|
||||||
|
existingUser.setAccountNonExpired(userDetails.isAccountNonExpired());
|
||||||
|
existingUser.setAccountNonLocked(userDetails.isAccountNonLocked());
|
||||||
|
existingUser.setCredentialsNonExpired(userDetails.isCredentialsNonExpired());
|
||||||
|
if (userDetails.getPassword() != null && !userDetails.getPassword().isEmpty()) {
|
||||||
|
existingUser.setPassword(passwordEncoder.encode(userDetails.getPassword()));
|
||||||
|
}
|
||||||
|
return userRepository.save(existingUser);
|
||||||
|
})
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public boolean deleteUser(UUID id) {
|
||||||
|
return userRepository.findById(id)
|
||||||
|
.map(user -> {
|
||||||
|
user.setDeleted(true);
|
||||||
|
userRepository.save(user);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import zw.qantra.tm.domain.dtos.RegisterRequest;
|
|||||||
import zw.qantra.tm.domain.dtos.erp.*;
|
import zw.qantra.tm.domain.dtos.erp.*;
|
||||||
import zw.qantra.tm.domain.enums.CurrencyType;
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
import zw.qantra.tm.domain.enums.Status;
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
|
import zw.qantra.tm.domain.models.GroupBatchItem;
|
||||||
import zw.qantra.tm.domain.models.Transaction;
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
|
import zw.qantra.tm.domain.repositories.GroupBatchItemRepository;
|
||||||
import zw.qantra.tm.domain.repositories.TransactionRepository;
|
import zw.qantra.tm.domain.repositories.TransactionRepository;
|
||||||
import zw.qantra.tm.configs.VelocityApiProperties;
|
import zw.qantra.tm.configs.VelocityApiProperties;
|
||||||
import zw.qantra.tm.exceptions.AlreadyExistsException;
|
import zw.qantra.tm.exceptions.AlreadyExistsException;
|
||||||
@@ -27,12 +29,14 @@ import java.math.BigDecimal;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VelocityPaymentService {
|
public class VelocityPaymentService {
|
||||||
private final TransactionRepository transactionRepository;
|
private final TransactionRepository transactionRepository;
|
||||||
|
private final GroupBatchItemRepository groupBatchItemRepository;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final RestService restService;
|
private final RestService restService;
|
||||||
private final VelocityApiProperties velocityApiProperties;
|
private final VelocityApiProperties velocityApiProperties;
|
||||||
@@ -101,19 +105,47 @@ public class VelocityPaymentService {
|
|||||||
public Transaction createSalesOrder(Transaction transaction) {
|
public Transaction createSalesOrder(Transaction transaction) {
|
||||||
BigDecimal amount = transaction.getTotalAmount();
|
BigDecimal amount = transaction.getTotalAmount();
|
||||||
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
List<VelocitySalesOrderDto.ItemDto> items;
|
||||||
|
if (transaction.getBatchId() != null) {
|
||||||
|
// Fetch GroupBatchItems for this batch and summarize by providerLabel
|
||||||
|
List<GroupBatchItem> batchItems = groupBatchItemRepository.findAllByGroupBatchId(transaction.getBatchId());
|
||||||
|
Map<String, BigDecimal> totalsByProvider = batchItems.stream()
|
||||||
|
.filter(item -> item.getProviderLabel() != null && item.getAmount() != null)
|
||||||
|
.collect(Collectors.groupingBy(
|
||||||
|
GroupBatchItem::getProviderLabel,
|
||||||
|
Collectors.mapping(
|
||||||
|
GroupBatchItem::getAmount,
|
||||||
|
Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
items = totalsByProvider.entrySet().stream()
|
||||||
|
.map(entry -> VelocitySalesOrderDto.ItemDto.builder()
|
||||||
|
.name(transaction.getBillName())
|
||||||
|
.itemCode("VLT-" + entry.getKey())
|
||||||
|
.qty(BigDecimal.ONE)
|
||||||
|
.unitPrice(entry.getValue())
|
||||||
|
.amount(entry.getValue())
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
items = Collections.singletonList(VelocitySalesOrderDto.ItemDto.builder()
|
||||||
|
.name(transaction.getBillName())
|
||||||
|
.itemCode("VLT-" + transaction.getProviderLabel())
|
||||||
|
.qty(BigDecimal.ONE)
|
||||||
|
.unitPrice(amount)
|
||||||
|
.amount(amount)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
VelocitySalesOrderDto payload = VelocitySalesOrderDto.builder()
|
VelocitySalesOrderDto payload = VelocitySalesOrderDto.builder()
|
||||||
.currencyCodeString(transaction.getDebitCurrency().name())
|
.currencyCodeString(transaction.getDebitCurrency().name())
|
||||||
.orderDate(LocalDate.now().format(fmt))
|
.orderDate(LocalDate.now().format(fmt))
|
||||||
.dueDate(LocalDate.now().plusDays(7).format(fmt))
|
.dueDate(LocalDate.now().plusDays(7).format(fmt))
|
||||||
.notes(transaction.getReference())
|
.notes("Transaction trace - " + transaction.getTrace())
|
||||||
.authorized(Boolean.TRUE)
|
.authorized(Boolean.TRUE)
|
||||||
.items(Collections.singletonList(VelocitySalesOrderDto.ItemDto.builder()
|
.items(items)
|
||||||
.name(transaction.getBillName())
|
|
||||||
.itemCode("VLT-" + transaction.getProviderLabel())
|
|
||||||
.qty(BigDecimal.ONE)
|
|
||||||
.unitPrice(amount)
|
|
||||||
.amount(amount)
|
|
||||||
.build()))
|
|
||||||
.charges(Collections.emptyList())
|
.charges(Collections.emptyList())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -149,7 +181,7 @@ public class VelocityPaymentService {
|
|||||||
? "VMC" : transaction.getPaymentProcessorLabel();
|
? "VMC" : transaction.getPaymentProcessorLabel();
|
||||||
VelocityTransactionDto payload = VelocityTransactionDto.builder()
|
VelocityTransactionDto payload = VelocityTransactionDto.builder()
|
||||||
.salesOrderId(transaction.getOrderId())
|
.salesOrderId(transaction.getOrderId())
|
||||||
.amount(transaction.getTotalAmount())
|
.amount(transaction.getCreditAmount())
|
||||||
.debitPhone(transaction.getDebitPhone())
|
.debitPhone(transaction.getDebitPhone())
|
||||||
.debitRegion("ZW")
|
.debitRegion("ZW")
|
||||||
.type("REQUEST")
|
.type("REQUEST")
|
||||||
@@ -164,7 +196,8 @@ public class VelocityPaymentService {
|
|||||||
|
|
||||||
if ("MPGS".equalsIgnoreCase(transaction.getPaymentProcessorLabel())) {
|
if ("MPGS".equalsIgnoreCase(transaction.getPaymentProcessorLabel())) {
|
||||||
payload.setCancelUrl(velocityApiProperties.getCancelUrl() + "/" + transaction.getOrderId());
|
payload.setCancelUrl(velocityApiProperties.getCancelUrl() + "/" + transaction.getOrderId());
|
||||||
payload.setSuccessUrl(velocityApiProperties.getSuccessUrl() + "/" + transaction.getOrderId());
|
// not sending successUrl for now
|
||||||
|
// payload.setSuccessUrl(velocityApiProperties.getSuccessUrl() + "/" + transaction.getOrderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -184,6 +217,7 @@ public class VelocityPaymentService {
|
|||||||
String redirectUrl = body.containsKey("redirectUrl") ? (String) body.get("redirectUrl") : null;
|
String redirectUrl = body.containsKey("redirectUrl") ? (String) body.get("redirectUrl") : null;
|
||||||
|
|
||||||
if ("SUCCESS".equalsIgnoreCase(transactionStatus)) {
|
if ("SUCCESS".equalsIgnoreCase(transactionStatus)) {
|
||||||
|
transaction.setStatus(Status.SUCCESS);
|
||||||
transaction.setErpSalesPaymentRef(transactionName);
|
transaction.setErpSalesPaymentRef(transactionName);
|
||||||
transaction.setOrderTransactionTrace(tranTrace);
|
transaction.setOrderTransactionTrace(tranTrace);
|
||||||
transaction.setOrderTransactionId(tranId);
|
transaction.setOrderTransactionId(tranId);
|
||||||
@@ -225,7 +259,6 @@ public class VelocityPaymentService {
|
|||||||
throw new IllegalStateException("Error initiating payment: " + errorMessage);
|
throw new IllegalStateException("Error initiating payment: " + errorMessage);
|
||||||
}
|
}
|
||||||
transaction.setStatus(Status.valueOf((String) body.get("status")));
|
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.setPollingStatus(Status.valueOf((String) body.get("pollStatus")));
|
||||||
transaction.setDebitRef((String) body.get("debitRef"));
|
transaction.setDebitRef((String) body.get("debitRef"));
|
||||||
transaction.setPaymentProcessorRef((String) body.get("paymentProcessorRef"));
|
transaction.setPaymentProcessorRef((String) body.get("paymentProcessorRef"));
|
||||||
@@ -259,6 +292,11 @@ public class VelocityPaymentService {
|
|||||||
|
|
||||||
@Async
|
@Async
|
||||||
public void updateSalesOrderWorkflow(Transaction transaction) {
|
public void updateSalesOrderWorkflow(Transaction transaction) {
|
||||||
|
if(transaction.getOrderTrace() == null) {
|
||||||
|
log.error("Order trace is null for transaction: {}", transaction.getTrace());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LinkedHashMap response = makeRequest(
|
LinkedHashMap response = makeRequest(
|
||||||
HttpMethod.PUT,
|
HttpMethod.PUT,
|
||||||
VELOCITY_API_UPDATE_WORKFLOW_PATH.replace("{traceId}", transaction.getOrderTrace()),
|
VELOCITY_API_UPDATE_WORKFLOW_PATH.replace("{traceId}", transaction.getOrderTrace()),
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package zw.qantra.tm.domain.services;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import zw.qantra.tm.domain.models.*;
|
||||||
|
import zw.qantra.tm.domain.repositories.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WorkspaceMigrationService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WorkspaceMigrationService.class);
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final WorkspaceRepository workspaceRepository;
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
private final RecipientRepository recipientRepository;
|
||||||
|
private final RecipientGroupRepository recipientGroupRepository;
|
||||||
|
private final RecipientGroupMemberRepository recipientGroupMemberRepository;
|
||||||
|
private final GroupBatchRepository groupBatchRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public MigrationResult migrate() {
|
||||||
|
Map<String, Integer> recordsUpdated = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// Step 1: Create workspaces for users without one
|
||||||
|
int workspacesCreated = createWorkspacesForUsers();
|
||||||
|
|
||||||
|
// Build a userId → workspaceId lookup map
|
||||||
|
Map<String, UUID> userToWorkspace = buildUserToWorkspaceMap();
|
||||||
|
|
||||||
|
// Step 2: Backfill workspaceId on all 5 entities
|
||||||
|
recordsUpdated.put("Transaction", backfillTransactions(userToWorkspace));
|
||||||
|
recordsUpdated.put("Recipient", backfillRecipients(userToWorkspace));
|
||||||
|
recordsUpdated.put("RecipientGroup", backfillRecipientGroups(userToWorkspace));
|
||||||
|
recordsUpdated.put("RecipientGroupMember", backfillRecipientGroupMembers(userToWorkspace));
|
||||||
|
recordsUpdated.put("GroupBatch", backfillGroupBatches(userToWorkspace));
|
||||||
|
|
||||||
|
return MigrationResult.builder()
|
||||||
|
.workspacesCreated(workspacesCreated)
|
||||||
|
.recordsUpdated(recordsUpdated)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1: Identify users without any workspace and create a default workspace for each.
|
||||||
|
* Returns the number of workspaces created.
|
||||||
|
*/
|
||||||
|
private int createWorkspacesForUsers() {
|
||||||
|
List<User> allUsers = userRepository.findAll();
|
||||||
|
int created = 0;
|
||||||
|
|
||||||
|
for (User user : allUsers) {
|
||||||
|
if (user.getWorkspaces() == null || user.getWorkspaces().isEmpty()) {
|
||||||
|
Workspace workspace = Workspace.builder()
|
||||||
|
.name(user.getUsername() + "'s Workspace")
|
||||||
|
.description("Default workspace for " + user.getUsername())
|
||||||
|
.users(new HashSet<>())
|
||||||
|
.build();
|
||||||
|
workspace.getUsers().add(user);
|
||||||
|
workspaceRepository.save(workspace);
|
||||||
|
created++;
|
||||||
|
log.info("Created workspace '{}' for user '{}'", workspace.getName(), user.getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Workspaces created: {}", created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a lookup map: userId (String) → workspaceId (UUID).
|
||||||
|
* Each user is mapped to their first workspace.
|
||||||
|
*/
|
||||||
|
private Map<String, UUID> buildUserToWorkspaceMap() {
|
||||||
|
Map<String, UUID> map = new HashMap<>();
|
||||||
|
List<User> allUsers = userRepository.findAll();
|
||||||
|
|
||||||
|
for (User user : allUsers) {
|
||||||
|
if (user.getWorkspaces() != null && !user.getWorkspaces().isEmpty()) {
|
||||||
|
Workspace firstWorkspace = user.getWorkspaces().iterator().next();
|
||||||
|
map.put(user.getId().toString(), firstWorkspace.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Built user→workspace map with {} entries", map.size());
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill workspaceId on Transaction records that have userId but null workspaceId.
|
||||||
|
*/
|
||||||
|
private int backfillTransactions(Map<String, UUID> userToWorkspace) {
|
||||||
|
List<Transaction> records = transactionRepository.findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
for (Transaction record : records) {
|
||||||
|
UUID workspaceId = userToWorkspace.get(record.getUserId());
|
||||||
|
if (workspaceId != null) {
|
||||||
|
record.setWorkspaceId(workspaceId);
|
||||||
|
transactionRepository.save(record);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Transactions backfilled: {}/{}", updated, records.size());
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill workspaceId on Recipient records that have userId but null workspaceId.
|
||||||
|
*/
|
||||||
|
private int backfillRecipients(Map<String, UUID> userToWorkspace) {
|
||||||
|
List<Recipient> records = recipientRepository.findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
for (Recipient record : records) {
|
||||||
|
UUID workspaceId = userToWorkspace.get(record.getUserId());
|
||||||
|
if (workspaceId != null) {
|
||||||
|
record.setWorkspaceId(workspaceId);
|
||||||
|
recipientRepository.save(record);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Recipients backfilled: {}/{}", updated, records.size());
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill workspaceId on RecipientGroup records that have userId but null workspaceId.
|
||||||
|
*/
|
||||||
|
private int backfillRecipientGroups(Map<String, UUID> userToWorkspace) {
|
||||||
|
List<RecipientGroup> records = recipientGroupRepository.findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
for (RecipientGroup record : records) {
|
||||||
|
UUID workspaceId = userToWorkspace.get(record.getUserId());
|
||||||
|
if (workspaceId != null) {
|
||||||
|
record.setWorkspaceId(workspaceId);
|
||||||
|
recipientGroupRepository.save(record);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("RecipientGroups backfilled: {}/{}", updated, records.size());
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill workspaceId on RecipientGroupMember records that have userId but null workspaceId.
|
||||||
|
*/
|
||||||
|
private int backfillRecipientGroupMembers(Map<String, UUID> userToWorkspace) {
|
||||||
|
List<RecipientGroupMember> records = recipientGroupMemberRepository.findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
for (RecipientGroupMember record : records) {
|
||||||
|
UUID workspaceId = userToWorkspace.get(record.getUserId());
|
||||||
|
if (workspaceId != null) {
|
||||||
|
record.setWorkspaceId(workspaceId);
|
||||||
|
recipientGroupMemberRepository.save(record);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("RecipientGroupMembers backfilled: {}/{}", updated, records.size());
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backfill workspaceId on GroupBatch records that have userId but null workspaceId.
|
||||||
|
*/
|
||||||
|
private int backfillGroupBatches(Map<String, UUID> userToWorkspace) {
|
||||||
|
List<GroupBatch> records = groupBatchRepository.findAllByWorkspaceIdIsNullAndUserIdIsNotNull();
|
||||||
|
int updated = 0;
|
||||||
|
|
||||||
|
for (GroupBatch record : records) {
|
||||||
|
UUID workspaceId = userToWorkspace.get(record.getUserId());
|
||||||
|
if (workspaceId != null) {
|
||||||
|
record.setWorkspaceId(workspaceId);
|
||||||
|
groupBatchRepository.save(record);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("GroupBatches backfilled: {}/{}", updated, records.size());
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public static class MigrationResult {
|
||||||
|
private int workspacesCreated;
|
||||||
|
private Map<String, Integer> recordsUpdated;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java
Normal file
106
src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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.User;
|
||||||
|
import zw.qantra.tm.domain.models.Workspace;
|
||||||
|
import zw.qantra.tm.domain.repositories.UserRepository;
|
||||||
|
import zw.qantra.tm.domain.repositories.WorkspaceRepository;
|
||||||
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
|
import zw.qantra.tm.utils.SecurityUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WorkspaceService {
|
||||||
|
|
||||||
|
private final WorkspaceRepository workspaceRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public Page<Workspace> getAllWorkspaces(Specification<Workspace> specification, Pageable pageable) {
|
||||||
|
return workspaceRepository.findAll(specification, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Workspace> getWorkspace(UUID id) {
|
||||||
|
return workspaceRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Workspace> searchByName(String name) {
|
||||||
|
return workspaceRepository.findByNameContainingIgnoreCase(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Workspace createWorkspace(Workspace workspace) {
|
||||||
|
return workspaceRepository.save(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Workspace updateWorkspace(UUID id, Workspace workspace) {
|
||||||
|
if (workspaceRepository.existsById(id)) {
|
||||||
|
workspace.setId(id);
|
||||||
|
return workspaceRepository.save(workspace);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteWorkspace(UUID id) {
|
||||||
|
if (workspaceRepository.existsById(id)) {
|
||||||
|
workspaceRepository.deleteById(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Optional<Workspace> associateUser(UUID workspaceId, UUID userId) {
|
||||||
|
Workspace workspace = workspaceRepository.findById(workspaceId).orElse(null);
|
||||||
|
User user = userRepository.findById(userId).orElse(null);
|
||||||
|
if (workspace == null || user == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
workspace.getUsers().add(user);
|
||||||
|
workspaceRepository.save(workspace);
|
||||||
|
return Optional.of(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Optional<Workspace> disassociateUser(UUID workspaceId, UUID userId) {
|
||||||
|
// Prevent a user from disassociating themselves from their own workspace
|
||||||
|
String currentUsername = SecurityUtils.getCurrentUsername();
|
||||||
|
if (currentUsername != null) {
|
||||||
|
User currentUser = userRepository.findByUsername(currentUsername).orElse(null);
|
||||||
|
if (currentUser != null && currentUser.getId().equals(userId)) {
|
||||||
|
throw new ApiException("You cannot remove yourself from the workspace");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Workspace workspace = workspaceRepository.findById(workspaceId).orElse(null);
|
||||||
|
User user = userRepository.findById(userId).orElse(null);
|
||||||
|
if (workspace == null || user == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
workspace.getUsers().remove(user);
|
||||||
|
workspaceRepository.save(workspace);
|
||||||
|
return Optional.of(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Set<User> getWorkspaceUsers(UUID workspaceId) {
|
||||||
|
return workspaceRepository.findById(workspaceId)
|
||||||
|
.map(Workspace::getUsers)
|
||||||
|
.orElse(Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Set<Workspace> getUserWorkspaces(UUID userId) {
|
||||||
|
return userRepository.findById(userId)
|
||||||
|
.map(User::getWorkspaces)
|
||||||
|
.orElse(Set.of());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ public class NetoneConfirmationHotProcessor implements TransactionProcessorInter
|
|||||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount(), "3")) {
|
if (!hotRechargeBalanceService.checkBalance(token, transaction.getAmount(), "3")) {
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
transaction.setResponseCode("98");
|
transaction.setResponseCode("98");
|
||||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
transaction.setErrorMessage("We can't process that transaction right now. Please try again later.");
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,27 +39,14 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
|||||||
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
@Value("${hot.api.url:https://ssl.hot.co.zw/api/v3}")
|
||||||
private String hotApiUrl;
|
private String hotApiUrl;
|
||||||
|
|
||||||
|
String token;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object process(Transaction transaction) throws Exception {
|
public Object process(Transaction transaction) throws Exception {
|
||||||
logger.info("Processing ZESA confirmation from HotRecharge");
|
logger.info("Processing ZESA confirmation from HotRecharge");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// do this if we not simulating success, if simulating success, we skip balance check and just return success
|
Map<String, Object> customerData = checkCustomer(transaction);
|
||||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
|
||||||
transaction.setStatus(Status.SUCCESS);
|
|
||||||
transaction.setResponseCode("02");
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
String token = hotRechargeTokenService.getToken();
|
|
||||||
if (token == null || token.isEmpty()) {
|
|
||||||
transaction.setStatus(Status.FAILED);
|
|
||||||
transaction.setResponseCode("91");
|
|
||||||
transaction.setErrorMessage("Failed to authenticate with HotRecharge");
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> customerData = checkCustomer(token, transaction);
|
|
||||||
if (customerData == null) {
|
if (customerData == null) {
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
transaction.setResponseCode("90");
|
transaction.setResponseCode("90");
|
||||||
@@ -84,15 +71,17 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
|||||||
|
|
||||||
transaction.setCreditAmount(transactionService.calculateCreditAmount(transaction));
|
transaction.setCreditAmount(transactionService.calculateCreditAmount(transaction));
|
||||||
|
|
||||||
|
// token should've been updated by checkCustomer
|
||||||
if (!hotRechargeBalanceService.checkBalance(token, transaction.getCreditAmount(), accId)) {
|
if (!hotRechargeBalanceService.checkBalance(token, transaction.getCreditAmount(), accId)) {
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
transaction.setResponseCode("92");
|
transaction.setResponseCode("92");
|
||||||
transaction.setErrorMessage("There's a technical issue on our end. Please try again later.");
|
transaction.setErrorMessage("We can't process that transaction right now. Please try again later.");
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.setStatus(Status.SUCCESS);
|
transaction.setStatus(Status.SUCCESS);
|
||||||
transaction.setResponseCode("00");
|
transaction.setResponseCode("00");
|
||||||
|
transaction.setCreditPhone(transaction.getDebitPhone());
|
||||||
|
|
||||||
List<Map<String, String>> additionalDataList = buildAdditionalData(customerData);
|
List<Map<String, String>> additionalDataList = buildAdditionalData(customerData);
|
||||||
transaction.setAdditionalData(additionalDataList);
|
transaction.setAdditionalData(additionalDataList);
|
||||||
@@ -104,6 +93,17 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
|||||||
.build();
|
.build();
|
||||||
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
||||||
|
|
||||||
|
// update customer address in creditAddress field
|
||||||
|
Map<String, Object> details = (Map<String, Object>) customerData.get("details");
|
||||||
|
if (details != null) {
|
||||||
|
for (Map.Entry<String, Object> entry : details.entrySet()) {
|
||||||
|
if (entry.getKey().equalsIgnoreCase("AccountName")) {
|
||||||
|
transaction.setCreditAddress(entry.getValue().toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error processing ZESA confirmation: " + e.getMessage(), e);
|
logger.error("Error processing ZESA confirmation: " + e.getMessage(), e);
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
@@ -122,7 +122,7 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
|||||||
Map<String, Object> details = (Map<String, Object>) customerData.get("details");
|
Map<String, Object> details = (Map<String, Object>) customerData.get("details");
|
||||||
if (details != null) {
|
if (details != null) {
|
||||||
for (Map.Entry<String, Object> entry : details.entrySet()) {
|
for (Map.Entry<String, Object> entry : details.entrySet()) {
|
||||||
// don't show customer ZWG value. It leads to confusion
|
// don't show customer ZWG value. It causes confusion
|
||||||
if(entry.getValue().toString().equals("ZWG"))
|
if(entry.getValue().toString().equals("ZWG"))
|
||||||
continue;
|
continue;
|
||||||
additionalDataList.add(Map.of("name", entry.getKey(), "value", entry.getValue().toString()));
|
additionalDataList.add(Map.of("name", entry.getKey(), "value", entry.getValue().toString()));
|
||||||
@@ -150,7 +150,25 @@ public class ZesaConfirmationHotProcessor implements TransactionProcessorInterfa
|
|||||||
return currency == null ? "" : currency.toString().trim();
|
return currency == null ? "" : currency.toString().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> checkCustomer(String token, Transaction transaction) {
|
private Map<String, Object> checkCustomer(Transaction transaction) {
|
||||||
|
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||||
|
logger.info("Simulating HotRecharge success");
|
||||||
|
return Utils.fromJson("{\n" +
|
||||||
|
" \"accountNumber\": \"07088597534\",\n" +
|
||||||
|
" \"details\": {\n" +
|
||||||
|
" \"AccountName\": \"CHIRINDO LETWINA\\n2144 MARLBOROUGH\",\n" +
|
||||||
|
" \"Status\": \"Active\",\n" +
|
||||||
|
" \"Currency\": \"ZWG\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}", LinkedHashMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
token = hotRechargeTokenService.getToken();
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
throw new ApiException("Failed to authenticate with HotRecharge");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String hotProductId = providerService.getProviderId(transaction.getBillClientId());
|
String hotProductId = providerService.getProviderId(transaction.getBillClientId());
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import zw.qantra.tm.domain.enums.CurrencyType;
|
|||||||
import zw.qantra.tm.domain.enums.RequestType;
|
import zw.qantra.tm.domain.enums.RequestType;
|
||||||
import zw.qantra.tm.domain.enums.Status;
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
import zw.qantra.tm.domain.models.AdditionalData;
|
import zw.qantra.tm.domain.models.AdditionalData;
|
||||||
|
import zw.qantra.tm.domain.models.Provider;
|
||||||
import zw.qantra.tm.domain.models.Transaction;
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
import zw.qantra.tm.domain.services.*;
|
import zw.qantra.tm.domain.services.*;
|
||||||
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
||||||
@@ -54,29 +55,12 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
|
||||||
transaction.setStatus(Status.SUCCESS);
|
|
||||||
transaction.setResponseCode("02");
|
|
||||||
transaction.setCreditRef("Simulated Integration Success");
|
|
||||||
transaction.setErrorMessage(null);
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info("Processing HotRecharge integration");
|
logger.info("Processing HotRecharge integration");
|
||||||
String token = hotRechargeTokenService.getToken();
|
|
||||||
|
|
||||||
if (token == null || token.isEmpty()) {
|
|
||||||
transaction.setStatus(Status.FAILED);
|
|
||||||
transaction.setResponseCode("91");
|
|
||||||
transaction.setErrorMessage("Failed to authenticate with HotRecharge");
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: a balance check might be necessary here. Removing
|
// todo: a balance check might be necessary here. Removing
|
||||||
// for now so not to spam hotrecharge with requests
|
// for now so not to spam hotrecharge with requests
|
||||||
|
|
||||||
LinkedHashMap<String, Object> rechargeResponse = performRecharge(token, transaction);
|
LinkedHashMap<String, Object> rechargeResponse = performRecharge(transaction);
|
||||||
if (rechargeResponse == null) {
|
if (rechargeResponse == null) {
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
transaction.setResponseCode("99");
|
transaction.setResponseCode("99");
|
||||||
@@ -104,6 +88,18 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
.jsonString(Utils.toJson(additionalDataList))
|
.jsonString(Utils.toJson(additionalDataList))
|
||||||
.build();
|
.build();
|
||||||
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
additionalDataService.getAdditionalDataRepository().save(additionalData);
|
||||||
|
|
||||||
|
// if provider is zesa, update customer token in creditVoucher field
|
||||||
|
String providerId = providerService.getProviderId(transaction.getBillClientId());
|
||||||
|
if(providerId.equals("41") || providerId.equals("24")) {
|
||||||
|
for (Map.Entry<String, Object> entry : rechargeData.entrySet()) {
|
||||||
|
if (entry.getKey().equalsIgnoreCase("Token")) {
|
||||||
|
transaction.setCreditVoucher(entry.getValue().toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transaction.setStatus(Status.FAILED);
|
transaction.setStatus(Status.FAILED);
|
||||||
@@ -124,8 +120,23 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private LinkedHashMap<String, Object> performRecharge(String token, Transaction transaction) {
|
private LinkedHashMap<String, Object> performRecharge(Transaction transaction) {
|
||||||
|
if(settingService.getBooleanSetting("SIMULATE_HOT_SUCCESS")){
|
||||||
|
try {
|
||||||
|
Thread.sleep(3);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
logger.info("Simulating HotRecharge success");
|
||||||
|
return getZesaPayload();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
String token = hotRechargeTokenService.getToken();
|
||||||
|
|
||||||
|
if (token == null || token.isEmpty()) {
|
||||||
|
throw new Exception("Failed to authenticate with HotRecharge");
|
||||||
|
}
|
||||||
String rechargeUrl = hotApiUrl + "/products/recharge";
|
String rechargeUrl = hotApiUrl + "/products/recharge";
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setBearerAuth(token);
|
headers.setBearerAuth(token);
|
||||||
@@ -137,30 +148,26 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
request.put("ProductId", providerId);
|
request.put("ProductId", providerId);
|
||||||
request.put("Target", transaction.getCreditAccount());
|
request.put("Target", transaction.getCreditAccount());
|
||||||
request.put("Amount", transaction.getCreditAmount());
|
request.put("Amount", transaction.getCreditAmount());
|
||||||
|
request.put("CustomerSMS", "%COMPANYNAME% topped up your account with $%AMOUNT%.");
|
||||||
// 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
|
// do this for zesa only
|
||||||
if(providerId.equals("41")) {
|
if(providerId.equals("41") || providerId.equals("24")) {
|
||||||
// update product to zwg product id
|
// override sms for zesa
|
||||||
|
request.put("CustomerSMS", "A ZETDC token was purchased for " +
|
||||||
|
"%ACCOUNTNAME% (%METERNUMBER%) that resulted in %KWH% units.");
|
||||||
|
|
||||||
|
// if necessary, update product to zwg product id
|
||||||
if(transaction.getCreditCurrency().equals(CurrencyType.ZWG)) {
|
if(transaction.getCreditCurrency().equals(CurrencyType.ZWG)) {
|
||||||
String zwgProviderId = providerService.getProviderId(ZESA_ZWG_CLIENT);
|
String zwgProviderId = providerService.getProviderId(ZESA_ZWG_CLIENT);
|
||||||
request.put("ProductId", zwgProviderId);
|
request.put("ProductId", zwgProviderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
||||||
if (transaction.getDebitPhone() != null) {
|
if (transaction.getCreditPhone() != null) {
|
||||||
Map<String, String> option = new LinkedHashMap<>();
|
Map<String, String> option = new LinkedHashMap<>();
|
||||||
option.put("Name", "NotifyNumber");
|
option.put("Name", "NotifyNumber");
|
||||||
option.put("ParameterType", "String");
|
option.put("ParameterType", "String");
|
||||||
option.put("Value", transaction.getDebitPhone());
|
option.put("Value", transaction.getCreditPhone());
|
||||||
rechargeOptions.add(option);
|
rechargeOptions.add(option);
|
||||||
}
|
}
|
||||||
if (!rechargeOptions.isEmpty()) {
|
if (!rechargeOptions.isEmpty()) {
|
||||||
@@ -168,15 +175,19 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// do this for Econet bundles
|
// do this for providers with bundles
|
||||||
if(providerId.equals("111")) {
|
Provider provider = providerService.getProviderRepository()
|
||||||
String productCode = providerService.getExternalId(providerId, transaction.getProductUid());
|
.findByClientId(transaction.getBillClientId());
|
||||||
|
if(provider.getRequiresProducts().equals(true)) {
|
||||||
|
String productCode = providerService.getExternalId(
|
||||||
|
provider.getId().toString(), transaction.getBillProductName());
|
||||||
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
List<Map<String, String>> rechargeOptions = new ArrayList<>();
|
||||||
Map<String, String> option = new LinkedHashMap<>();
|
Map<String, String> option = new LinkedHashMap<>();
|
||||||
option.put("Name", "ProductCode");
|
option.put("Name", "ProductCode");
|
||||||
option.put("ParameterType", "String");
|
option.put("ParameterType", "String");
|
||||||
option.put("Value", productCode);
|
option.put("Value", productCode);
|
||||||
rechargeOptions.add(option);
|
rechargeOptions.add(option);
|
||||||
|
request.put("RechargeOptions", rechargeOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Sending recharge request: {}", request);
|
logger.info("Sending recharge request: {}", request);
|
||||||
@@ -229,6 +240,32 @@ public class HotRechargeIntegrationProcessor implements TransactionProcessorInte
|
|||||||
return additionalDataList;
|
return additionalDataList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LinkedHashMap<String, Object> getZesaPayload() {
|
||||||
|
return Utils.fromJson("{\n" +
|
||||||
|
" \"successful\": true,\n" +
|
||||||
|
" \"rechargeId\": 1.9080854E8,\n" +
|
||||||
|
" \"amount\": 265.0,\n" +
|
||||||
|
" \"discount\": 1.2,\n" +
|
||||||
|
" \"balance\": {\n" +
|
||||||
|
" \"accountTypeId\": 2.0,\n" +
|
||||||
|
" \"name\": \"Utility ZWG\",\n" +
|
||||||
|
" \"balance\": 3306.32\n" +
|
||||||
|
" },\n" +
|
||||||
|
" \"message\": \"Transaction processed successfully\",\n" +
|
||||||
|
" \"rechargeData\": {\n" +
|
||||||
|
" \"Network\": \"ZESA\",\n" +
|
||||||
|
" \"Target\": \"07088597534\",\n" +
|
||||||
|
" \"Cost\": \"261.8200\",\n" +
|
||||||
|
" \"Token\": \"6616 0517 2063 6675 8254\",\n" +
|
||||||
|
" \"Units\": \"40.92\",\n" +
|
||||||
|
" \"NetAmount\": \"250\",\n" +
|
||||||
|
" \"TaxAmount\": \"0\",\n" +
|
||||||
|
" \"Levy\": \"15\",\n" +
|
||||||
|
" \"Arrears\": \"0\"\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}", LinkedHashMap.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object reverse(Transaction transaction) {
|
public Object reverse(Transaction transaction) {
|
||||||
return transaction;
|
return transaction;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import zw.qantra.tm.domain.enums.Status;
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
import zw.qantra.tm.domain.models.Transaction;
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
@@ -12,11 +11,6 @@ import zw.qantra.tm.domain.services.SettingService;
|
|||||||
import zw.qantra.tm.domain.services.TransactionService;
|
import zw.qantra.tm.domain.services.TransactionService;
|
||||||
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
||||||
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
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")
|
@Service("REQUEST_WALLET_REMOTE")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -32,6 +26,7 @@ public class CityWalletRemotePaymentProcessor implements TransactionProcessorInt
|
|||||||
|
|
||||||
if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) {
|
if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) {
|
||||||
transaction.setStatus(Status.SUCCESS);
|
transaction.setStatus(Status.SUCCESS);
|
||||||
|
transaction.setPaymentStatus(Status.SUCCESS);
|
||||||
transaction.setResponseCode("02");
|
transaction.setResponseCode("02");
|
||||||
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
||||||
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
||||||
@@ -40,6 +35,8 @@ public class CityWalletRemotePaymentProcessor implements TransactionProcessorInt
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
transaction = velocityPaymentService.processPayment(transaction);
|
transaction = velocityPaymentService.processPayment(transaction);
|
||||||
|
|
||||||
|
transaction.setPaymentStatus(Status.SUCCESS);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
||||||
@@ -63,6 +60,7 @@ public class CityWalletRemotePaymentProcessor implements TransactionProcessorInt
|
|||||||
|
|
||||||
if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) {
|
if(settingService.getBooleanSetting("SIMULATE_WALLET_SUCCESS")) {
|
||||||
transaction.setStatus(Status.SUCCESS);
|
transaction.setStatus(Status.SUCCESS);
|
||||||
|
transaction.setPollingStatus(Status.SUCCESS);
|
||||||
transaction.setResponseCode("02");
|
transaction.setResponseCode("02");
|
||||||
transaction.setTargetUrl("/home");
|
transaction.setTargetUrl("/home");
|
||||||
return transaction;
|
return transaction;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -17,7 +16,6 @@ import zw.qantra.tm.domain.services.SettingService;
|
|||||||
import zw.qantra.tm.domain.services.TransactionService;
|
import zw.qantra.tm.domain.services.TransactionService;
|
||||||
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
||||||
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
||||||
import zw.qantra.tm.rest.RestService;
|
|
||||||
|
|
||||||
@Service("REQUEST_ECOCASH_REMOTE")
|
@Service("REQUEST_ECOCASH_REMOTE")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -33,6 +31,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
|
|||||||
|
|
||||||
if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")) {
|
if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")) {
|
||||||
transaction.setStatus(Status.SUCCESS);
|
transaction.setStatus(Status.SUCCESS);
|
||||||
|
transaction.setPaymentStatus(Status.SUCCESS);
|
||||||
transaction.setResponseCode("02");
|
transaction.setResponseCode("02");
|
||||||
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
||||||
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
||||||
@@ -41,6 +40,8 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
transaction = velocityPaymentService.processPayment(transaction);
|
transaction = velocityPaymentService.processPayment(transaction);
|
||||||
|
|
||||||
|
transaction.setPaymentStatus(Status.SUCCESS);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
||||||
@@ -64,6 +65,7 @@ public class EcocashRemotePaymentProcessor implements TransactionProcessorInterf
|
|||||||
|
|
||||||
if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")) {
|
if(settingService.getBooleanSetting("SIMULATE_ECOCASH_SUCCESS")) {
|
||||||
transaction.setStatus(Status.SUCCESS);
|
transaction.setStatus(Status.SUCCESS);
|
||||||
|
transaction.setPollingStatus(Status.SUCCESS);
|
||||||
transaction.setResponseCode("02");
|
transaction.setResponseCode("02");
|
||||||
transaction.setTargetUrl("/home");
|
transaction.setTargetUrl("/home");
|
||||||
return transaction;
|
return transaction;
|
||||||
|
|||||||
@@ -4,28 +4,17 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import zw.qantra.tm.domain.dtos.SdkActionDto;
|
|
||||||
import zw.qantra.tm.domain.enums.Status;
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
import zw.qantra.tm.domain.models.Transaction;
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
import zw.qantra.tm.domain.services.SettingService;
|
import zw.qantra.tm.domain.services.SettingService;
|
||||||
import zw.qantra.tm.domain.services.TransactionService;
|
import zw.qantra.tm.domain.services.TransactionService;
|
||||||
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
||||||
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
||||||
import zw.qantra.tm.rest.RestService;
|
|
||||||
import zw.qantra.tm.utils.Utils;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
@Service("REQUEST_MPGS_WEB")
|
@Service("REQUEST_MPGS_WEB")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -41,6 +30,7 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
|
|||||||
|
|
||||||
if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) {
|
if(settingService.getBooleanSetting("SIMULATE_MPGS_SUCCESS")) {
|
||||||
transaction.setStatus(Status.SUCCESS);
|
transaction.setStatus(Status.SUCCESS);
|
||||||
|
transaction.setPaymentStatus(Status.SUCCESS);
|
||||||
transaction.setResponseCode("02");
|
transaction.setResponseCode("02");
|
||||||
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
transaction.setPaymentProcessorRef(RandomStringUtils.randomNumeric(10));
|
||||||
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
transaction.setDebitRef(RandomStringUtils.randomNumeric(10));
|
||||||
@@ -50,6 +40,10 @@ public class MPGSWebPaymentProcessor implements TransactionProcessorInterface {
|
|||||||
try {
|
try {
|
||||||
// velocity expects VMC
|
// velocity expects VMC
|
||||||
transaction = velocityPaymentService.processPayment(transaction);
|
transaction = velocityPaymentService.processPayment(transaction);
|
||||||
|
|
||||||
|
// payment status comes back as pending for VMC,
|
||||||
|
// set it to SUCCESS
|
||||||
|
transaction.setPaymentStatus(Status.SUCCESS);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
logger.error("Network error during payment processing: {}", e.getMessage(), e);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class BatchWorkflow extends WorkflowDefinition {
|
|||||||
public static final State REQUEST = new State("request");
|
public static final State REQUEST = new State("request");
|
||||||
public static final State REQUEST_SUCCESS = new State("requestSuccess", WorkflowStateType.manual);
|
public static final State REQUEST_SUCCESS = new State("requestSuccess", WorkflowStateType.manual);
|
||||||
public static final State POLL = new State("poll");
|
public static final State POLL = new State("poll");
|
||||||
|
public static final State POLL_SUCCESS = new State("pollSuccess", WorkflowStateType.manual);
|
||||||
public static final State INTEGRATION = new State("integration");
|
public static final State INTEGRATION = new State("integration");
|
||||||
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
||||||
public static final State DONE = new State("done", WorkflowStateType.end);
|
public static final State DONE = new State("done", WorkflowStateType.end);
|
||||||
@@ -81,12 +82,16 @@ public class BatchWorkflow extends WorkflowDefinition {
|
|||||||
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
||||||
GroupBatch batch = groupBatchWorkflowService.processPoll(batchId);
|
GroupBatch batch = groupBatchWorkflowService.processPoll(batchId);
|
||||||
execution.setVariable("batchStatus", batch.getStatus().name());
|
execution.setVariable("batchStatus", batch.getStatus().name());
|
||||||
return NextAction.moveToState(INTEGRATION, "Batch poll complete");
|
return NextAction.moveToState(POLL_SUCCESS, "Batch poll complete");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return NextAction.moveToState(FAILED, e.getMessage());
|
return NextAction.moveToState(FAILED, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void pollSuccess(StateExecution execution) {
|
||||||
|
log.info("Batch poll succeeded, paused for integration trigger");
|
||||||
|
}
|
||||||
|
|
||||||
public NextAction integration(StateExecution execution) {
|
public NextAction integration(StateExecution execution) {
|
||||||
try {
|
try {
|
||||||
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
UUID batchId = UUID.fromString(execution.getVariable("batchId", String.class));
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ import zw.qantra.tm.domain.dtos.*;
|
|||||||
import zw.qantra.tm.domain.dtos.erp.VelocityCustomerResponseDto;
|
import zw.qantra.tm.domain.dtos.erp.VelocityCustomerResponseDto;
|
||||||
import zw.qantra.tm.domain.models.Otp;
|
import zw.qantra.tm.domain.models.Otp;
|
||||||
import zw.qantra.tm.domain.models.User;
|
import zw.qantra.tm.domain.models.User;
|
||||||
import zw.qantra.tm.domain.services.EmailService;
|
import zw.qantra.tm.domain.models.Workspace;
|
||||||
import zw.qantra.tm.domain.services.OtpService;
|
import zw.qantra.tm.domain.services.*;
|
||||||
import zw.qantra.tm.domain.services.UserService;
|
|
||||||
import io.nflow.engine.workflow.curated.State;
|
import io.nflow.engine.workflow.curated.State;
|
||||||
|
|
||||||
import jakarta.validation.ConstraintViolation;
|
import jakarta.validation.ConstraintViolation;
|
||||||
import jakarta.validation.Validator;
|
import jakarta.validation.Validator;
|
||||||
import zw.qantra.tm.domain.services.VelocityPaymentService;
|
|
||||||
import zw.qantra.tm.exceptions.AlreadyExistsException;
|
import zw.qantra.tm.exceptions.AlreadyExistsException;
|
||||||
import zw.qantra.tm.rest.RestService;
|
import zw.qantra.tm.rest.RestService;
|
||||||
|
|
||||||
@@ -48,9 +46,11 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RestService restService;
|
private RestService restService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private EmailService emailService;
|
private NotificationService notificationService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private VelocityPaymentService velocityPaymentService;
|
private VelocityPaymentService velocityPaymentService;
|
||||||
|
@Autowired
|
||||||
|
private WorkspaceService workspaceService;
|
||||||
|
|
||||||
public static final String TYPE = "registrationWorkflow";
|
public static final String TYPE = "registrationWorkflow";
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
public static final State VERIFY_OTP = new State("verifyOtp");
|
public static final State VERIFY_OTP = new State("verifyOtp");
|
||||||
public static final State RESEND_OTP = new State("resendOtp", WorkflowStateType.manual);
|
public static final State RESEND_OTP = new State("resendOtp", WorkflowStateType.manual);
|
||||||
public static final State OTP_SUCCESS = new State("otpSuccess");
|
public static final State OTP_SUCCESS = new State("otpSuccess");
|
||||||
|
public static final State CREATE_WORKSPACE = new State("createWorkspace");
|
||||||
public static final State UPDATE_ERP = new State("updateErp");
|
public static final State UPDATE_ERP = new State("updateErp");
|
||||||
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
public static final State FAILED = new State("failed", WorkflowStateType.manual);
|
||||||
public static final State DONE = new State("done", WorkflowStateType.end);
|
public static final State DONE = new State("done", WorkflowStateType.end);
|
||||||
@@ -72,7 +73,8 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
permit(SEND_OTP, VERIFY_OTP);
|
permit(SEND_OTP, VERIFY_OTP);
|
||||||
permit(RESEND_OTP, VERIFY_OTP);
|
permit(RESEND_OTP, VERIFY_OTP);
|
||||||
permit(VERIFY_OTP, OTP_SUCCESS, FAILED);
|
permit(VERIFY_OTP, OTP_SUCCESS, FAILED);
|
||||||
permit(OTP_SUCCESS, UPDATE_ERP, FAILED);
|
permit(OTP_SUCCESS, CREATE_WORKSPACE, FAILED);
|
||||||
|
permit(CREATE_WORKSPACE, UPDATE_ERP, FAILED);
|
||||||
permit(UPDATE_ERP, DONE);
|
permit(UPDATE_ERP, DONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +119,9 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
// Generate OTP
|
// Generate OTP
|
||||||
Otp otp = otpService.generateOtp(username, "REGISTRATION");
|
Otp otp = otpService.generateOtp(username, "REGISTRATION");
|
||||||
|
|
||||||
String string = String.format("Your Velocity Pay verification code is: %s", otp.getOtp());
|
notificationService.sendOtpEmail(email, username, otp.getOtp());
|
||||||
emailService.sendSimpleMessage(email, "Velocity Pay verification code", string);
|
notificationService.sendSms(registerRequest.getPhone(),
|
||||||
// LinkedHashMap response = restService.postAs(infobipUrl, string, headers, LinkedHashMap.class);
|
"Your Velocity Pay verification code is: " + otp.getOtp());
|
||||||
|
|
||||||
log.info("otp generated successfully: {}", otp.getOtp());
|
log.info("otp generated successfully: {}", otp.getOtp());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -142,10 +144,12 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
public void resendOtp(StateExecution execution) {
|
public void resendOtp(StateExecution execution) {
|
||||||
OtpRequest otpRequest = execution.getVariable("resend", OtpRequest.class);
|
OtpRequest otpRequest = execution.getVariable("resend", OtpRequest.class);
|
||||||
Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION");
|
Otp otp = otpService.generateOtp(otpRequest.getUsername(), "REGISTRATION");
|
||||||
String string = String.format("Your Velocity Pay verification code is: %s", otp.getOtp());
|
// The username field in OtpRequest may actually contain the email; use it directly
|
||||||
emailService.sendSimpleMessage(otpRequest.getUsername(), "Velocity Pay verification code", string);
|
notificationService.sendOtpEmail(otpRequest.getUsername(), otpRequest.getUsername(), otp.getOtp());
|
||||||
|
notificationService.sendSms(otpRequest.getPhone(),
|
||||||
|
"Your Velocity Pay verification code is: " + otp.getOtp());
|
||||||
|
|
||||||
log.info("otp generated successfully: {}", otp.getOtp());
|
log.info("otp resent successfully: {}", otp.getOtp());
|
||||||
}
|
}
|
||||||
|
|
||||||
public NextAction otpSuccess(StateExecution execution) {
|
public NextAction otpSuccess(StateExecution execution) {
|
||||||
@@ -165,9 +169,28 @@ public class RegistrationWorkflow extends WorkflowDefinition {
|
|||||||
return NextAction.moveToState(FAILED, "Failed to register user: " + e.getMessage());
|
return NextAction.moveToState(FAILED, "Failed to register user: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextAction.moveToState(UPDATE_ERP, "User creation successfully for: " + request.getUsername());
|
return NextAction.moveToState(CREATE_WORKSPACE, "User creation successfully for: " + request.getUsername());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NextAction createWorkspace(StateExecution execution) {
|
||||||
|
log.info("Begin create workspace");
|
||||||
|
RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class);
|
||||||
|
Workspace workspace = new Workspace();
|
||||||
|
workspace.setName(registerRequest.getFirstName() + "'s Workspace");
|
||||||
|
workspace.setDescription("Default Workspace");
|
||||||
|
workspace.setPhone(registerRequest.getPhone());
|
||||||
|
workspace.setEmail(registerRequest.getEmail());
|
||||||
|
workspace = workspaceService.createWorkspace(workspace);
|
||||||
|
|
||||||
|
User user = userService.fetchUserByUsername(registerRequest.getUsername());
|
||||||
|
workspace.setUsers(Set.of(user));
|
||||||
|
workspaceService.updateWorkspace(workspace.getId(), workspace);
|
||||||
|
log.info("Workspace created successfully");
|
||||||
|
|
||||||
|
return NextAction.moveToState(UPDATE_ERP, "Workspace creation successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public NextAction updateErp(StateExecution execution) {
|
public NextAction updateErp(StateExecution execution) {
|
||||||
RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class);
|
RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class);
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ public class WorkflowUtils {
|
|||||||
CompletableFuture<Object> future = new CompletableFuture<>();
|
CompletableFuture<Object> future = new CompletableFuture<>();
|
||||||
|
|
||||||
while(!future.isDone()){ // check if the future is done
|
while(!future.isDone()){ // check if the future is done
|
||||||
WorkflowInstance instance = workflowInstanceService.getWorkflowInstance(
|
|
||||||
workflowId,
|
|
||||||
EnumSet.of(ACTIONS, ACTION_STATE_VARIABLES, CURRENT_STATE_VARIABLES),
|
|
||||||
1L);
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(pollingInterval);
|
Thread.sleep(pollingInterval);
|
||||||
|
WorkflowInstance instance = workflowInstanceService.getWorkflowInstance(
|
||||||
|
workflowId,
|
||||||
|
EnumSet.of(ACTIONS, ACTION_STATE_VARIABLES, CURRENT_STATE_VARIABLES),
|
||||||
|
1L);
|
||||||
List<String> stateList = Arrays.stream(states).toList();
|
List<String> stateList = Arrays.stream(states).toList();
|
||||||
List<WorkflowInstance.WorkflowInstanceStatus> statusList =
|
List<WorkflowInstance.WorkflowInstanceStatus> statusList =
|
||||||
Arrays.asList(WorkflowInstance.WorkflowInstanceStatus.manual,
|
Arrays.asList(WorkflowInstance.WorkflowInstanceStatus.manual,
|
||||||
@@ -40,13 +40,13 @@ public class WorkflowUtils {
|
|||||||
statusList.contains(instance.status)){
|
statusList.contains(instance.status)){
|
||||||
future.complete(getStateResponse(instance, "success"));
|
future.complete(getStateResponse(instance, "success"));
|
||||||
}
|
}
|
||||||
|
timeout -= pollingInterval;
|
||||||
|
if(timeout <= 0){
|
||||||
|
future.complete(getStateResponse(instance, "Request timed out"));
|
||||||
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
future.completeExceptionally(e);
|
future.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
timeout -= pollingInterval;
|
|
||||||
if(timeout <= 0){
|
|
||||||
future.complete(getStateResponse(instance, "Request timed out"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return future;
|
return future;
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ import com.google.i18n.phonenumbers.Phonenumber;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import zw.qantra.tm.domain.enums.PartyType;
|
import zw.qantra.tm.domain.enums.PartyType;
|
||||||
import zw.qantra.tm.domain.enums.RequestType;
|
import zw.qantra.tm.domain.enums.RequestType;
|
||||||
import zw.qantra.tm.domain.enums.Status;
|
import zw.qantra.tm.domain.enums.Status;
|
||||||
import zw.qantra.tm.domain.models.Provider;
|
import zw.qantra.tm.domain.models.Provider;
|
||||||
import zw.qantra.tm.domain.models.Transaction;
|
import zw.qantra.tm.domain.models.Transaction;
|
||||||
|
import zw.qantra.tm.domain.models.User;
|
||||||
import zw.qantra.tm.domain.services.ProviderService;
|
import zw.qantra.tm.domain.services.ProviderService;
|
||||||
|
import zw.qantra.tm.domain.services.UserService;
|
||||||
import zw.qantra.tm.exceptions.ApiException;
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
import zw.qantra.tm.utils.Utils;
|
import zw.qantra.tm.utils.Utils;
|
||||||
|
|
||||||
@@ -24,6 +28,7 @@ public class CleanupHandler implements HandlerInterface {
|
|||||||
private final Logger logger = LoggerFactory.getLogger(CleanupHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(CleanupHandler.class);
|
||||||
|
|
||||||
private final ProviderService providerService;
|
private final ProviderService providerService;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object process(Transaction transaction) throws Exception {
|
public Object process(Transaction transaction) throws Exception {
|
||||||
@@ -39,21 +44,28 @@ public class CleanupHandler implements HandlerInterface {
|
|||||||
transaction.setPartyType(PartyType.INDIVIDUAL);
|
transaction.setPartyType(PartyType.INDIVIDUAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(transaction.getUsername() == null) {
|
||||||
|
userService.getUserRepository().findById(UUID.fromString(transaction.getUserId()))
|
||||||
|
.ifPresent(user -> transaction.setUsername(user.getUsername()));
|
||||||
|
}
|
||||||
|
|
||||||
// UPDATE TRAN WITH CONFIRMATION HANDLER
|
// UPDATE TRAN WITH CONFIRMATION HANDLER
|
||||||
// =======================
|
// =======================
|
||||||
Provider provider = providerService.getProviderRepository()
|
if(transaction.getType().equals(RequestType.CONFIRM)) {
|
||||||
.findByClientId(transaction.getBillClientId());
|
Provider provider = providerService.getProviderRepository()
|
||||||
if(provider == null){
|
.findByClientId(transaction.getBillClientId());
|
||||||
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
|
if(provider == null){
|
||||||
}
|
throw new ApiException("Provider not found for billClientId: " + transaction.getBillClientId());
|
||||||
String label =
|
}
|
||||||
transaction.getType() + "_" +
|
String label =
|
||||||
provider.getLabel() + "_" +
|
transaction.getType() + "_" +
|
||||||
provider.getIntegrationProcessorLabel();
|
provider.getLabel() + "_" +
|
||||||
|
provider.getIntegrationProcessorLabel();
|
||||||
|
|
||||||
transaction.setConfirmationProcessorLabel(label);
|
transaction.setConfirmationProcessorLabel(label);
|
||||||
transaction.setProviderLabel(provider.getLabel());
|
transaction.setProviderLabel(provider.getLabel());
|
||||||
logger.info("label: {}", label);
|
logger.info("label: {}", label);
|
||||||
|
}
|
||||||
|
|
||||||
// FORMAT FIELDS CORRECTLY
|
// FORMAT FIELDS CORRECTLY
|
||||||
// =======================
|
// =======================
|
||||||
@@ -82,7 +94,7 @@ public class CleanupHandler implements HandlerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(creditPhoneNumber != null && !creditPhoneNumber.isEmpty()){
|
if(creditPhoneNumber != null && !creditPhoneNumber.isEmpty()){
|
||||||
transaction.setCreditAccount(Utils.formatPhoneNumber(creditPhoneNumber, "ZW"));
|
transaction.setCreditPhone(Utils.formatPhoneNumber(creditPhoneNumber, "ZW"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return transaction;
|
return transaction;
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ public class GatewayPaymentHandler implements HandlerInterface {
|
|||||||
new ApiException("Transaction not found for ID: " + incomingTransaction.getId()));
|
new ApiException("Transaction not found for ID: " + incomingTransaction.getId()));
|
||||||
transaction.setType(incomingTransaction.getType()); // type should be REQUEST here
|
transaction.setType(incomingTransaction.getType()); // type should be REQUEST here
|
||||||
transaction.setAuthType(incomingTransaction.getAuthType());
|
transaction.setAuthType(incomingTransaction.getAuthType());
|
||||||
|
transaction.setErrorMessage(null);
|
||||||
|
transaction.setSystemErrorMessage(null);
|
||||||
|
transaction.setStatus(Status.PROCESSING);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we've done this before then return
|
// if we've done this before then return
|
||||||
@@ -58,9 +61,9 @@ public class GatewayPaymentHandler implements HandlerInterface {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// MAIN TRANSACTION LOGIC!
|
// MAIN TRANSACTION LOGIC!
|
||||||
transactionProcessorInterface.process(transaction);
|
transaction = (Transaction) transactionProcessorInterface.process(transaction);
|
||||||
transaction.setPaymentStatus(transaction.getStatus());
|
|
||||||
transaction.setPollingStatus(Status.PENDING);
|
transaction.setPollingStatus(Status.PENDING);
|
||||||
|
transactionService.save(transaction);
|
||||||
|
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package zw.qantra.tm.domain.services.processors.workflows.handlers;
|
package zw.qantra.tm.domain.services.processors.workflows.handlers;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -13,6 +14,7 @@ import zw.qantra.tm.domain.services.factories.PaymentProcessorFactory;
|
|||||||
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
import zw.qantra.tm.domain.services.processors.TransactionProcessorInterface;
|
||||||
import zw.qantra.tm.exceptions.ApiException;
|
import zw.qantra.tm.exceptions.ApiException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service("pollStatus")
|
@Service("pollStatus")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PollStatusHandler implements HandlerInterface {
|
public class PollStatusHandler implements HandlerInterface {
|
||||||
@@ -24,7 +26,14 @@ public class PollStatusHandler implements HandlerInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object process(Transaction transaction) throws Exception {
|
public Object process(Transaction transaction) throws Exception {
|
||||||
Logger logger = LoggerFactory.getLogger(TransactionService.class);
|
|
||||||
|
// don't poll unless payment was success
|
||||||
|
if(transaction.getPaymentStatus() != Status.SUCCESS) {
|
||||||
|
transaction.setPollingStatus(Status.PENDING);
|
||||||
|
transaction.setErrorMessage("Transaction not yet successful");
|
||||||
|
transactionService.save(transaction);
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
|
|
||||||
// if the transaction is already successful, return it
|
// if the transaction is already successful, return it
|
||||||
if(transaction.getPollingStatus() == Status.SUCCESS){
|
if(transaction.getPollingStatus() == Status.SUCCESS){
|
||||||
@@ -40,23 +49,21 @@ public class PollStatusHandler implements HandlerInterface {
|
|||||||
|
|
||||||
// no persistence happens during polling unless the transaction status changes
|
// no persistence happens during polling unless the transaction status changes
|
||||||
String label = "REQUEST_" + transaction.getPaymentProcessorLabel() + "_" + transaction.getAuthType();
|
String label = "REQUEST_" + transaction.getPaymentProcessorLabel() + "_" + transaction.getAuthType();
|
||||||
logger.info("label: {}", label);
|
log.info("label: {}", label);
|
||||||
|
|
||||||
TransactionProcessorInterface transactionProcessorInterface =
|
TransactionProcessorInterface transactionProcessorInterface =
|
||||||
(TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label);
|
(TransactionProcessorInterface) paymentProcessorFactory.getPaymentProcessor(label);
|
||||||
|
|
||||||
// find out if the gateway tran was successful
|
// find out if the gateway tran was successful
|
||||||
transactionProcessorInterface.poll(transaction);
|
transaction = (Transaction) transactionProcessorInterface.poll(transaction);
|
||||||
|
|
||||||
transactionEvent.setStatus(transaction.getStatus());
|
transactionEvent.setStatus(transaction.getStatus());
|
||||||
transactionEvent.setMessage(transaction.getErrorMessage());
|
transactionEvent.setMessage(transaction.getErrorMessage());
|
||||||
transactionEventService.save(transactionEvent);
|
transactionEventService.save(transactionEvent);
|
||||||
|
|
||||||
transaction.setPollingStatus(transaction.getStatus());
|
|
||||||
transaction.setPaymentStatus(transaction.getStatus());
|
|
||||||
|
|
||||||
// if transaction wasn't successful, throw and halt workflow
|
// if transaction wasn't successful, throw and halt workflow
|
||||||
if(transaction.getStatus() != Status.SUCCESS){
|
if(transaction.getPollingStatus() != Status.SUCCESS){
|
||||||
|
log.info("Verification pending: {}", transaction.getErrorMessage());
|
||||||
transactionService.save(transaction);
|
transactionService.save(transaction);
|
||||||
throw new ApiException("Verification pending: " + transaction.getErrorMessage() );
|
throw new ApiException("Verification pending: " + transaction.getErrorMessage() );
|
||||||
}else {
|
}else {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class RecipientsUpdateHandler implements HandlerInterface {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if(transaction.getStatus() == Status.SUCCESS){
|
if(transaction.getStatus() == Status.SUCCESS){
|
||||||
List<Recipient> recipients = recipientService.getRecipient(transaction.getCreditAccount(), transaction.getUserId());
|
List<Recipient> recipients = recipientService.getRecipient(transaction.getCreditAccount(), transaction.getWorkspaceId());
|
||||||
|
|
||||||
Recipient recipient = null;
|
Recipient recipient = null;
|
||||||
if(!recipients.isEmpty()){
|
if(!recipients.isEmpty()){
|
||||||
@@ -30,6 +30,7 @@ public class RecipientsUpdateHandler implements HandlerInterface {
|
|||||||
recipient = new Recipient();
|
recipient = new Recipient();
|
||||||
recipient.setAccount(transaction.getCreditAccount());
|
recipient.setAccount(transaction.getCreditAccount());
|
||||||
recipient.setUserId(transaction.getUserId());
|
recipient.setUserId(transaction.getUserId());
|
||||||
|
recipient.setWorkspaceId(transaction.getWorkspaceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
recipient.setLatestProviderLabel(transaction.getProviderLabel());
|
recipient.setLatestProviderLabel(transaction.getProviderLabel());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package zw.qantra.tm.seeders;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import zw.qantra.tm.domain.enums.CurrencyType;
|
||||||
import zw.qantra.tm.domain.models.PaymentProcessor;
|
import zw.qantra.tm.domain.models.PaymentProcessor;
|
||||||
import zw.qantra.tm.domain.repositories.PaymentProcessorRepository;
|
import zw.qantra.tm.domain.repositories.PaymentProcessorRepository;
|
||||||
|
|
||||||
@@ -33,6 +34,18 @@ public class PaymentProcessorSeeder implements Seeder {
|
|||||||
.accountFieldLabel("Wallet Phone Number")
|
.accountFieldLabel("Wallet Phone Number")
|
||||||
.description("Pay using your CityWallet")
|
.description("Pay using your CityWallet")
|
||||||
.authType("REMOTE")
|
.authType("REMOTE")
|
||||||
|
.currency(CurrencyType.USD)
|
||||||
|
.build(),
|
||||||
|
PaymentProcessor.builder()
|
||||||
|
.name("CityWallet")
|
||||||
|
.label("WALLET")
|
||||||
|
.type("GATEWAY")
|
||||||
|
.image("city.png")
|
||||||
|
.accountFieldName("phoneNumber")
|
||||||
|
.accountFieldLabel("Wallet Phone Number")
|
||||||
|
.description("Pay using your CityWallet")
|
||||||
|
.authType("REMOTE")
|
||||||
|
.currency(CurrencyType.ZWG)
|
||||||
.build(),
|
.build(),
|
||||||
PaymentProcessor.builder()
|
PaymentProcessor.builder()
|
||||||
.name("Ecocash")
|
.name("Ecocash")
|
||||||
@@ -43,6 +56,7 @@ public class PaymentProcessorSeeder implements Seeder {
|
|||||||
.accountFieldLabel("EcoCash Phone Number")
|
.accountFieldLabel("EcoCash Phone Number")
|
||||||
.description("Pay using your mobile wallet")
|
.description("Pay using your mobile wallet")
|
||||||
.authType("REMOTE")
|
.authType("REMOTE")
|
||||||
|
.currency(CurrencyType.USD)
|
||||||
.build(),
|
.build(),
|
||||||
PaymentProcessor.builder()
|
PaymentProcessor.builder()
|
||||||
.name("Visa/MasterCard")
|
.name("Visa/MasterCard")
|
||||||
@@ -53,11 +67,12 @@ public class PaymentProcessorSeeder implements Seeder {
|
|||||||
.accountFieldLabel("Card Number")
|
.accountFieldLabel("Card Number")
|
||||||
.description("Pay using your international card")
|
.description("Pay using your international card")
|
||||||
.authType("WEB")
|
.authType("WEB")
|
||||||
|
.currency(CurrencyType.USD)
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
|
|
||||||
for (PaymentProcessor processor : processors) {
|
for (PaymentProcessor processor : processors) {
|
||||||
if (paymentProcessorRepository.existsByLabel(processor.getLabel())) {
|
if (paymentProcessorRepository.existsByLabelAndCurrency(processor.getLabel(), processor.getCurrency())) {
|
||||||
log.info("PaymentProcessor '{}' already exists, skipping.", processor.getLabel());
|
log.info("PaymentProcessor '{}' already exists, skipping.", processor.getLabel());
|
||||||
} else {
|
} else {
|
||||||
paymentProcessorRepository.save(processor);
|
paymentProcessorRepository.save(processor);
|
||||||
|
|||||||
@@ -41,10 +41,31 @@ public class ProviderSeeder implements Seeder {
|
|||||||
.requiresAccount(false)
|
.requiresAccount(false)
|
||||||
.requiresAmount(true)
|
.requiresAmount(true)
|
||||||
.requiresPhone(true)
|
.requiresPhone(true)
|
||||||
|
.requiresProducts(false)
|
||||||
.priority(2)
|
.priority(2)
|
||||||
.meta("{\"hotProductId\": \"101\"}")
|
.meta("{\"hotProductId\": \"101\"}")
|
||||||
.uid(Utils.getUid())
|
.uid(Utils.getUid())
|
||||||
.build(),
|
.build(),
|
||||||
|
Provider.builder()
|
||||||
|
.name("Econet Airtime")
|
||||||
|
.description("Econet Airtime")
|
||||||
|
.clientId("econet_airtime_zwg")
|
||||||
|
.currency(CurrencyType.ZWG)
|
||||||
|
.label("ECONET")
|
||||||
|
.category("AIRTIME")
|
||||||
|
.image("econet.png")
|
||||||
|
.externalId("78f1f497-c9eb-401c-b22c-884756e68e41")
|
||||||
|
.integrationProcessorLabel("HOT")
|
||||||
|
.accountFieldName("Phone Number")
|
||||||
|
.percentageCommission(5)
|
||||||
|
.requiresAccount(false)
|
||||||
|
.requiresAmount(true)
|
||||||
|
.requiresPhone(true)
|
||||||
|
.requiresProducts(false)
|
||||||
|
.priority(2)
|
||||||
|
.meta("{\"hotProductId\": \"7\"}")
|
||||||
|
.uid(Utils.getUid())
|
||||||
|
.build(),
|
||||||
Provider.builder()
|
Provider.builder()
|
||||||
.name("NetOne Airtime")
|
.name("NetOne Airtime")
|
||||||
.description("NetOne Airtime")
|
.description("NetOne Airtime")
|
||||||
@@ -60,6 +81,7 @@ public class ProviderSeeder implements Seeder {
|
|||||||
.requiresAccount(false)
|
.requiresAccount(false)
|
||||||
.requiresAmount(true)
|
.requiresAmount(true)
|
||||||
.requiresPhone(true)
|
.requiresPhone(true)
|
||||||
|
.requiresProducts(true)
|
||||||
.uid(Utils.getUid())
|
.uid(Utils.getUid())
|
||||||
.priority(3)
|
.priority(3)
|
||||||
.meta("{\"hotProductId\": \"35\"}")
|
.meta("{\"hotProductId\": \"35\"}")
|
||||||
@@ -79,6 +101,7 @@ public class ProviderSeeder implements Seeder {
|
|||||||
.requiresAccount(true)
|
.requiresAccount(true)
|
||||||
.requiresAmount(true)
|
.requiresAmount(true)
|
||||||
.requiresPhone(true)
|
.requiresPhone(true)
|
||||||
|
.requiresProducts(false)
|
||||||
.priority(1)
|
.priority(1)
|
||||||
.meta("{\"hotProductId\": \"24\"}")
|
.meta("{\"hotProductId\": \"24\"}")
|
||||||
.uid(Utils.getUid())
|
.uid(Utils.getUid())
|
||||||
@@ -98,6 +121,7 @@ public class ProviderSeeder implements Seeder {
|
|||||||
.requiresAccount(true)
|
.requiresAccount(true)
|
||||||
.requiresAmount(true)
|
.requiresAmount(true)
|
||||||
.requiresPhone(true)
|
.requiresPhone(true)
|
||||||
|
.requiresProducts(false)
|
||||||
.priority(1)
|
.priority(1)
|
||||||
.meta("{\"hotProductId\": \"41\"}")
|
.meta("{\"hotProductId\": \"41\"}")
|
||||||
.uid(Utils.getUid())
|
.uid(Utils.getUid())
|
||||||
@@ -117,9 +141,31 @@ public class ProviderSeeder implements Seeder {
|
|||||||
.requiresAccount(false)
|
.requiresAccount(false)
|
||||||
.requiresAmount(false)
|
.requiresAmount(false)
|
||||||
.requiresPhone(true)
|
.requiresPhone(true)
|
||||||
|
.requiresProducts(true)
|
||||||
.priority(4)
|
.priority(4)
|
||||||
.meta("{\"hotProductId\": \"111\"}")
|
.meta("{\"hotProductId\": \"111\"}")
|
||||||
.uid(Utils.getUid())
|
.uid(Utils.getUid())
|
||||||
|
.build(),
|
||||||
|
Provider.builder()
|
||||||
|
.name("Econet Bundles")
|
||||||
|
.description("Econet Bundles")
|
||||||
|
.clientId("econet_bundles_zwg")
|
||||||
|
.currency(CurrencyType.ZWG)
|
||||||
|
.label("ECONET_BUNDLES")
|
||||||
|
.category("AIRTIME")
|
||||||
|
.image("econet.png")
|
||||||
|
.externalId("287863e2-a791-4106-b629-79dadc9a6780")
|
||||||
|
.integrationProcessorLabel("HOT")
|
||||||
|
.accountFieldName("Phone Number")
|
||||||
|
.percentageCommission(5)
|
||||||
|
.requiresAccount(false)
|
||||||
|
.requiresAmount(false)
|
||||||
|
.requiresPhone(true)
|
||||||
|
.requiresProducts(true)
|
||||||
|
.requiresProducts(true)
|
||||||
|
.priority(4)
|
||||||
|
.meta("{\"hotProductId\": \"16\"}")
|
||||||
|
.uid(Utils.getUid())
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user