diff --git a/data/jobrunr.mv.db b/data/jobrunr.mv.db
new file mode 100644
index 0000000..dc305ea
Binary files /dev/null and b/data/jobrunr.mv.db differ
diff --git a/pom.xml b/pom.xml
index 1dc43bd..a5ac904 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,6 +107,11 @@
com.fasterxml.jackson.datatype
jackson-datatype-joda
+
+ com.h2database
+ h2
+ runtime
+
org.postgresql
postgresql
@@ -183,7 +188,20 @@
com.github.ben-manes.caffeine
caffeine
-
+
+ org.jobrunr
+ jobrunr-spring-boot-3-starter
+ 8.6.1
+
+
+
+
+ org.apache.poi
+ poi-ooxml
+ 5.2.5
+
+
+
diff --git a/reports/batch-report-b4b19592-6d9d-4892-a82e-be7c0810fa6a.xlsx b/reports/batch-report-b4b19592-6d9d-4892-a82e-be7c0810fa6a.xlsx
new file mode 100644
index 0000000..4c7a1af
Binary files /dev/null and b/reports/batch-report-b4b19592-6d9d-4892-a82e-be7c0810fa6a.xlsx differ
diff --git a/reports/batch-report-bffebe3e-1d84-48be-acd4-5213ef61fb4f.xlsx b/reports/batch-report-bffebe3e-1d84-48be-acd4-5213ef61fb4f.xlsx
new file mode 100644
index 0000000..493991e
Binary files /dev/null and b/reports/batch-report-bffebe3e-1d84-48be-acd4-5213ef61fb4f.xlsx differ
diff --git a/reports/batch-report-e72b4959-fd95-4227-a9b4-1754c89231e9.xlsx b/reports/batch-report-e72b4959-fd95-4227-a9b4-1754c89231e9.xlsx
new file mode 100644
index 0000000..49e4d27
Binary files /dev/null and b/reports/batch-report-e72b4959-fd95-4227-a9b4-1754c89231e9.xlsx differ
diff --git a/src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java b/src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java
index 525a98d..e31c34f 100644
--- a/src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java
+++ b/src/main/java/zw/qantra/tm/configs/DatabaseConfiguration.java
@@ -27,4 +27,19 @@ public class DatabaseConfiguration {
.initializeDataSourceBuilder()
.build();
}
+
+ // JobRunr DataSource Properties (H2)
+ @Bean
+ @ConfigurationProperties(prefix = "jobrunr.datasource")
+ public DataSourceProperties jobrunrDataSourceProperties() {
+ return new DataSourceProperties();
+ }
+
+ // JobRunr DataSource using H2
+ @Bean
+ public DataSource jobrunrDataSource() {
+ return jobrunrDataSourceProperties()
+ .initializeDataSourceBuilder()
+ .build();
+ }
}
\ No newline at end of file
diff --git a/src/main/java/zw/qantra/tm/configs/ReportsResourceConfig.java b/src/main/java/zw/qantra/tm/configs/ReportsResourceConfig.java
new file mode 100644
index 0000000..e8dccbd
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/configs/ReportsResourceConfig.java
@@ -0,0 +1,22 @@
+package zw.qantra.tm.configs;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.nio.file.Path;
+
+@Configuration
+public class ReportsResourceConfig implements WebMvcConfigurer {
+
+ @Value("${app.reports.storage-path:./reports}")
+ private String reportsStoragePath;
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ Path reportsDir = Path.of(reportsStoragePath).toAbsolutePath();
+ registry.addResourceHandler("/reports/**")
+ .addResourceLocations("file:" + reportsDir + "/");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
index fbc73e6..0425f29 100644
--- a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
+++ b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java
@@ -30,8 +30,9 @@ public class SecurityConfig {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
- .requestMatchers("/configs/seed/**").permitAll()
- .requestMatchers("/test/**").permitAll()
+ .requestMatchers("/reports/**").permitAll()
+ .requestMatchers("/configs/seed/**").permitAll()
+ .requestMatchers("/test/**").permitAll()
.requestMatchers("/nflow/**").permitAll()
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/public/**").permitAll()
diff --git a/src/main/java/zw/qantra/tm/domain/controllers/GroupBatchController.java b/src/main/java/zw/qantra/tm/domain/controllers/GroupBatchController.java
new file mode 100644
index 0000000..17c49e6
--- /dev/null
+++ b/src/main/java/zw/qantra/tm/domain/controllers/GroupBatchController.java
@@ -0,0 +1,113 @@
+package zw.qantra.tm.domain.controllers;
+
+import lombok.RequiredArgsConstructor;
+import net.kaczmarzyk.spring.data.jpa.domain.Equal;
+import net.kaczmarzyk.spring.data.jpa.web.annotation.And;
+import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import zw.qantra.tm.domain.dtos.GroupBatchCreateRequest;
+import zw.qantra.tm.domain.dtos.GroupBatchUpdateRequest;
+import zw.qantra.tm.domain.dtos.GroupBatchTriggerRequest;
+import zw.qantra.tm.domain.models.GroupBatch;
+import zw.qantra.tm.domain.models.GroupBatchItem;
+import zw.qantra.tm.domain.services.BatchReportService;
+import zw.qantra.tm.domain.services.GroupBatchService;
+
+import java.util.Map;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/public/group-batches")
+@RequiredArgsConstructor
+public class GroupBatchController {
+ private final GroupBatchService groupBatchService;
+ private final BatchReportService batchReportService;
+
+ @GetMapping
+ public ResponseEntity findAll(
+ @And({
+ @Spec(path = "recipientGroupId", defaultVal = "null", spec = Equal.class),
+ @Spec(path = "userId", spec = Equal.class),
+ @Spec(path = "status", spec = Equal.class),
+ @Spec(path = "currency", spec = Equal.class),
+ })Specification spec, Pageable pageable){
+ return ResponseEntity.ok(groupBatchService.findAll(spec, pageable));
+ }
+
+ @PostMapping
+ public ResponseEntity create(@RequestBody GroupBatchCreateRequest request) {
+ return ResponseEntity.ok(groupBatchService.createBatch(request));
+ }
+
+ @PutMapping("/{batchId}")
+ public ResponseEntity update(@PathVariable UUID batchId,
+ @RequestBody GroupBatchUpdateRequest request) {
+ request.setId(batchId);
+ return ResponseEntity.ok(groupBatchService.updateBatch(request));
+ }
+
+ @PostMapping("/{batchId}/confirm")
+ public ResponseEntity confirm(@PathVariable UUID batchId,
+ @RequestBody GroupBatchTriggerRequest request) {
+ return ResponseEntity.ok(groupBatchService.triggerConfirm(
+ batchId,
+ request.getUserId(),
+ request.getIdempotencyKey()));
+ }
+
+ @PostMapping("/{batchId}/request")
+ public ResponseEntity request(@PathVariable UUID batchId,
+ @RequestBody GroupBatchTriggerRequest request) {
+ Object response = groupBatchService.triggerRequest(
+ batchId,
+ request.getUserId(),
+ request.getAuthType(),
+ request.getIdempotencyKey());
+ return ResponseEntity.ok(response);
+ }
+
+ @PostMapping("/{batchId}/poll")
+ public ResponseEntity poll(@PathVariable UUID batchId,
+ @RequestBody GroupBatchTriggerRequest request) {
+ return ResponseEntity.ok(groupBatchService.triggerPoll(
+ batchId,
+ request.getUserId(),
+ request.getIdempotencyKey()));
+ }
+
+ @GetMapping("/{batchId}")
+ public ResponseEntity getBatch(@PathVariable UUID batchId,
+ @RequestParam String userId) {
+ return ResponseEntity.ok(groupBatchService.getBatch(batchId, userId));
+ }
+
+ @GetMapping("/items")
+ public ResponseEntity getBatchItems(
+ @And({
+ @Spec(path = "groupBatchId", defaultVal = "null", spec = Equal.class),
+ @Spec(path = "recipientId", spec = Equal.class),
+ }) Specification spec, Pageable pageable) {
+ return ResponseEntity.ok(groupBatchService.getBatchItems(spec, pageable));
+ }
+
+ @DeleteMapping("/{batchId}")
+ public ResponseEntity deleteBatch(@PathVariable UUID batchId,
+ @RequestParam String userId) {
+ groupBatchService.deleteBatch(batchId, userId);
+ return ResponseEntity.noContent().build();
+ }
+
+ @GetMapping("/{batchId}/report")
+ public ResponseEntity