From 598287b0cc1a847c0d2e2182b96b266dc92d6393 Mon Sep 17 00:00:00 2001 From: Vusa Date: Mon, 22 Jun 2026 13:40:58 +0200 Subject: [PATCH] polishing the workspace migration --- .../zw/qantra/tm/configs/SecurityConfig.java | 3 +- .../domain/controllers/AccountController.java | 28 ++- .../controllers/GroupBatchController.java | 24 +-- .../controllers/RecipientController.java | 20 +- .../controllers/RecipientGroupController.java | 21 +- .../tm/domain/controllers/TestController.java | 8 + .../controllers/TransactonController.java | 15 +- .../controllers/WorkspaceController.java | 93 ++++++++ .../domain/dtos/GroupBatchCreateRequest.java | 2 +- .../dtos/GroupBatchItemUpdateRequest.java | 2 +- .../domain/dtos/GroupBatchTriggerRequest.java | 4 +- .../domain/dtos/GroupBatchUpdateRequest.java | 2 +- .../tm/domain/dtos/GroupMemberRequest.java | 2 +- .../dtos/RecipientGroupCreateRequest.java | 2 + .../dtos/RecipientGroupUpdateRequest.java | 4 +- .../qantra/tm/domain/models/GroupBatch.java | 4 +- .../zw/qantra/tm/domain/models/Recipient.java | 8 +- .../tm/domain/models/RecipientGroup.java | 6 +- .../domain/models/RecipientGroupMember.java | 2 + .../qantra/tm/domain/models/Transaction.java | 4 +- .../java/zw/qantra/tm/domain/models/User.java | 7 + .../zw/qantra/tm/domain/models/Workspace.java | 39 ++++ .../repositories/GroupBatchRepository.java | 4 +- .../RecipientGroupMemberRepository.java | 1 + .../RecipientGroupRepository.java | 5 +- .../repositories/RecipientRepository.java | 5 +- .../repositories/TransactionRepository.java | 5 +- .../repositories/WorkspaceRepository.java | 14 ++ .../domain/services/BatchReportService.java | 4 +- .../tm/domain/services/GroupBatchService.java | 36 ++-- .../services/GroupBatchWorkflowService.java | 8 +- .../services/RecipientGroupService.java | 73 +++---- .../tm/domain/services/RecipientService.java | 11 +- .../domain/services/TransactionService.java | 22 +- .../services/WorkspaceMigrationService.java | 203 ++++++++++++++++++ .../tm/domain/services/WorkspaceService.java | 95 ++++++++ .../workflows/RegistrationWorkflow.java | 33 ++- .../handlers/RecipientsUpdateHandler.java | 3 +- .../zw/qantra/tm/seeders/ProviderSeeder.java | 7 + src/main/resources/application.properties | 2 +- .../TransactionRepositoryTest.java | 33 +-- .../services/TransactionServiceTest.java | 14 +- .../handlers/RecipientsUpdateHandlerTest.java | 19 +- 43 files changed, 730 insertions(+), 167 deletions(-) create mode 100644 src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java create mode 100644 src/main/java/zw/qantra/tm/domain/models/Workspace.java create mode 100644 src/main/java/zw/qantra/tm/domain/repositories/WorkspaceRepository.java create mode 100644 src/main/java/zw/qantra/tm/domain/services/WorkspaceMigrationService.java create mode 100644 src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java diff --git a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java index 0425f29..1630a94 100644 --- a/src/main/java/zw/qantra/tm/configs/SecurityConfig.java +++ b/src/main/java/zw/qantra/tm/configs/SecurityConfig.java @@ -39,8 +39,7 @@ public class SecurityConfig { .anyRequest().authenticated() ) .authenticationProvider(authenticationProvider(userService)) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) - .formLogin(withDefaults()); + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/zw/qantra/tm/domain/controllers/AccountController.java b/src/main/java/zw/qantra/tm/domain/controllers/AccountController.java index 10ddc1e..c5aef0d 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/AccountController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/AccountController.java @@ -6,7 +6,11 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import zw.qantra.tm.domain.dtos.erp.VelocityAccountDto; import zw.qantra.tm.domain.dtos.erp.VelocityStatementDto; +import zw.qantra.tm.domain.models.Workspace; import zw.qantra.tm.domain.services.VelocityAccountService; +import zw.qantra.tm.domain.services.WorkspaceService; + +import java.util.UUID; @RestController @RequestMapping("/accounts") @@ -14,12 +18,17 @@ import zw.qantra.tm.domain.services.VelocityAccountService; public class AccountController { private final VelocityAccountService velocityAccountService; + private final WorkspaceService workspaceService; - @GetMapping("/phone/{currencyPhoneConcat}") - @PreAuthorize("hasRole('ACCOUNT_READ')") - public ResponseEntity getAccountByPhone( - @PathVariable String currencyPhoneConcat) { + @GetMapping("/{workspaceId}/balance/{currency}") + public ResponseEntity getAccount( + @PathVariable UUID workspaceId, + @PathVariable String currency + ) { try { + Workspace workspace = workspaceService.getWorkspace(workspaceId) + .orElseThrow(() -> new IllegalArgumentException("Workspace not found")); + String currencyPhoneConcat = currency + workspace.getPhone(); VelocityAccountDto account = velocityAccountService.getAccountByPhone(currencyPhoneConcat); return ResponseEntity.ok(account); } catch (Exception e) { @@ -27,13 +36,16 @@ public class AccountController { } } - @GetMapping("/phone/{currencyPhoneConcat}/statement") - @PreAuthorize("hasRole('ACCOUNT_READ')") - public ResponseEntity getStatementByPhone( - @PathVariable String currencyPhoneConcat, + @GetMapping("/{workspaceId}/statement/{currency}") + public ResponseEntity getStatement( + @PathVariable UUID workspaceId, + @PathVariable String currency, @RequestParam String startDate, @RequestParam String endDate) { try { + Workspace workspace = workspaceService.getWorkspace(workspaceId) + .orElseThrow(() -> new IllegalArgumentException("Workspace not found")); + String currencyPhoneConcat = currency + workspace.getPhone(); VelocityStatementDto statement = velocityAccountService.getStatementByPhone( currencyPhoneConcat, startDate, endDate); return ResponseEntity.ok(statement); diff --git a/src/main/java/zw/qantra/tm/domain/controllers/GroupBatchController.java b/src/main/java/zw/qantra/tm/domain/controllers/GroupBatchController.java index dc8b8f4..9fdd1ee 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/GroupBatchController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/GroupBatchController.java @@ -22,7 +22,7 @@ import java.util.Map; import java.util.UUID; @RestController -@RequestMapping("/public/group-batches") +@RequestMapping("/group-batches") @RequiredArgsConstructor public class GroupBatchController { private final GroupBatchService groupBatchService; @@ -32,7 +32,7 @@ public class GroupBatchController { public ResponseEntity findAll( @And({ @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 = "currency", spec = Equal.class), })Specification spec, Pageable pageable){ @@ -56,7 +56,7 @@ public class GroupBatchController { @RequestBody GroupBatchTriggerRequest request) { return ResponseEntity.ok(groupBatchService.triggerConfirm( batchId, - request.getUserId(), + request.getWorkspaceId(), request.getIdempotencyKey())); } @@ -65,7 +65,7 @@ public class GroupBatchController { @RequestBody GroupBatchTriggerRequest request) { Object response = groupBatchService.triggerRequest( batchId, - request.getUserId(), + request.getWorkspaceId(), request.getAuthType(), request.getIdempotencyKey()); return ResponseEntity.ok(response); @@ -76,14 +76,14 @@ public class GroupBatchController { @RequestBody GroupBatchTriggerRequest request) { return ResponseEntity.ok(groupBatchService.triggerPoll( batchId, - request.getUserId(), + request.getWorkspaceId(), request.getIdempotencyKey())); } @GetMapping("/{batchId}") public ResponseEntity getBatch(@PathVariable UUID batchId, - @RequestParam String userId) { - return ResponseEntity.ok(groupBatchService.getBatch(batchId, userId)); + @RequestParam UUID workspaceId) { + return ResponseEntity.ok(groupBatchService.getBatch(batchId, workspaceId)); } @GetMapping("/items") @@ -101,21 +101,21 @@ public class GroupBatchController { @RequestBody GroupBatchItemUpdateRequest.ListWrapper wrapper) { return ResponseEntity.ok(groupBatchService.updateBatchItems( batchId, - wrapper.getUserId(), + wrapper.getWorkspaceId(), wrapper.getItems())); } @DeleteMapping("/{batchId}") public ResponseEntity deleteBatch(@PathVariable UUID batchId, - @RequestParam String userId) { - groupBatchService.deleteBatch(batchId, userId); + @RequestParam UUID workspaceId) { + groupBatchService.deleteBatch(batchId, workspaceId); return ResponseEntity.noContent().build(); } @GetMapping("/{batchId}/report") public ResponseEntity> downloadBatchReport(@PathVariable UUID batchId, - @RequestParam String userId) { - BatchReportService.BatchReportResult result = batchReportService.generateBatchReport(batchId, userId); + @RequestParam UUID workspaceId) { + BatchReportService.BatchReportResult result = batchReportService.generateBatchReport(batchId, workspaceId); return ResponseEntity.ok(Map.of( "fileUrl", result.fileUrl(), "alreadyExisted", result.alreadyExisted() diff --git a/src/main/java/zw/qantra/tm/domain/controllers/RecipientController.java b/src/main/java/zw/qantra/tm/domain/controllers/RecipientController.java index 01e3b87..f8d1213 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/RecipientController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/RecipientController.java @@ -2,9 +2,8 @@ package zw.qantra.tm.domain.controllers; import lombok.RequiredArgsConstructor; 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.Or; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -22,22 +21,23 @@ public class RecipientController { private final RecipientService recipientService; - @GetMapping("/search") - public ResponseEntity> searchRecipients( + @GetMapping + public ResponseEntity searchRecipients( @And({ @Spec(path = "name", spec = Equal.class), @Spec(path = "email", spec = Equal.class), @Spec(path = "phoneNumber", spec = Equal.class), @Spec(path = "account", spec = Equal.class), @Spec(path = "latestProviderLabel", spec = Equal.class), - @Spec(path = "userId", defaultVal = "null", spec = Equal.class) - }) Specification specification) { - return ResponseEntity.ok(recipientService.searchRecipients(specification)); + @Spec(path = "userId", spec = Equal.class), + @Spec(path = "workspaceId", defaultVal = "null", spec = Equal.class) + }) Specification specification, Pageable pageable) { + return ResponseEntity.ok(recipientService.findAllRecipients(specification, pageable)); } - @GetMapping("/{account}/user/{userId}") - public ResponseEntity getRecipient(@PathVariable String account, @PathVariable String userId) { - List recipients = recipientService.getRecipient(account, userId); + @GetMapping("/{account}/workspace/{workspaceId}") + public ResponseEntity getRecipient(@PathVariable String account, @PathVariable UUID workspaceId) { + List recipients = recipientService.getRecipient(account, workspaceId); if(recipients.size() > 0){ return ResponseEntity.ok(recipients.get(0)); } diff --git a/src/main/java/zw/qantra/tm/domain/controllers/RecipientGroupController.java b/src/main/java/zw/qantra/tm/domain/controllers/RecipientGroupController.java index fe4882a..4800e2b 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/RecipientGroupController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/RecipientGroupController.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.UUID; @RestController -@RequestMapping("/public/recipient-groups") +@RequestMapping("/recipient-groups") @RequiredArgsConstructor public class RecipientGroupController { private final RecipientGroupService recipientGroupService; @@ -34,7 +34,7 @@ public class RecipientGroupController { public ResponseEntity findAll( @And({ @Spec(path = "name", spec = EqualIgnoreCase.class), - @Spec(path = "userId", defaultVal = "null", spec = Equal.class) + @Spec(path = "workspaceId", defaultVal = "null", spec = Equal.class) })Specification spec, Pageable pageable) { return ResponseEntity.ok(recipientGroupService.findAll(spec, pageable)); } @@ -44,6 +44,7 @@ public class RecipientGroupController { RecipientGroup group = RecipientGroup.builder() .name(request.getName()) .description(request.getDescription()) + .workspaceId(request.getWorkspaceId()) .userId(request.getUserId()) .build(); return ResponseEntity.ok(recipientGroupService.create(group, request.getRecipients())); @@ -56,7 +57,7 @@ public class RecipientGroupController { @RequestParam(value = "description", required = false) String description, @RequestParam(value = "requestedBy", required = false) String requestedBy, @RequestParam(value = "currency", required = false) String currency, - @RequestParam("userId") String userId) { + @RequestParam("workspaceId") UUID workspaceId) { GroupCreateFromFileRequest request = new GroupCreateFromFileRequest(); request.setGroupName(groupName.trim()); @@ -64,7 +65,7 @@ public class RecipientGroupController { request.setRequestedBy(requestedBy); request.setCurrency(currency); - return ResponseEntity.ok(recipientGroupService.createFromFile(file, request, userId)); + return ResponseEntity.ok(recipientGroupService.createFromFile(file, request, workspaceId)); } @PutMapping("/update/{groupId}") @@ -72,15 +73,15 @@ public class RecipientGroupController { @RequestBody RecipientGroupUpdateRequest request) { return ResponseEntity.ok(recipientGroupService.update( groupId, - request.getUserId(), + request.getWorkspaceId(), request.getName(), request.getDescription())); } @DeleteMapping("/delete/{groupId}") public ResponseEntity delete(@PathVariable UUID groupId, - @RequestParam String userId) { - recipientGroupService.delete(groupId, userId); + @RequestParam UUID workspaceId) { + recipientGroupService.delete(groupId, workspaceId); return ResponseEntity.noContent().build(); } @@ -89,15 +90,15 @@ public class RecipientGroupController { @RequestBody GroupMemberRequest request) { return ResponseEntity.ok(recipientGroupService.addMembersByIds( groupId, - request.getUserId(), + request.getWorkspaceId(), request.getRecipientIds())); } @DeleteMapping("/{groupId}/members/{recipientId}") public ResponseEntity removeMember(@PathVariable UUID groupId, @PathVariable UUID recipientId, - @RequestParam String userId) { - recipientGroupService.removeMember(groupId, recipientId, userId); + @RequestParam UUID workspaceId) { + recipientGroupService.removeMember(groupId, recipientId, workspaceId); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/zw/qantra/tm/domain/controllers/TestController.java b/src/main/java/zw/qantra/tm/domain/controllers/TestController.java index 8ae9160..0db0028 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/TestController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/TestController.java @@ -12,6 +12,7 @@ import zw.qantra.tm.domain.enums.RequestType; import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.services.NotificationService; import zw.qantra.tm.domain.services.TransactionService; +import zw.qantra.tm.domain.services.WorkspaceMigrationService; @RestController @RequestMapping("/test") @@ -20,6 +21,7 @@ import zw.qantra.tm.domain.services.TransactionService; public class TestController { private final NotificationService notificationService; private final TransactionService transactionService; + private final WorkspaceMigrationService workspaceMigrationService; private static final Logger logger = LoggerFactory.getLogger(TestController.class); @@ -59,4 +61,10 @@ public class TestController { public ResponseEntity publicEndpoint() { return ResponseEntity.ok("This is a public endpoint - no authentication required!"); } + + @PostMapping("/migrate-workspaces") + public ResponseEntity migrateWorkspaces() { + WorkspaceMigrationService.MigrationResult result = workspaceMigrationService.migrate(); + return ResponseEntity.ok(result); + } } diff --git a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java index f585c9c..20efc4b 100644 --- a/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java +++ b/src/main/java/zw/qantra/tm/domain/controllers/TransactonController.java @@ -10,6 +10,7 @@ import io.nflow.engine.workflow.instance.WorkflowInstanceAction; import io.nflow.engine.workflow.instance.WorkflowInstanceFactory; 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.Or; import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec; import net.kaczmarzyk.spring.data.jpa.web.annotation.Conjunction; @@ -72,20 +73,22 @@ public class TransactonController { @Spec(path = "amount", spec = Equal.class), @Spec(path = "reference", 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 = "paymentProcessorLabel", spec = Equal.class), }) }, and = { @Spec(path = "type", spec = Equal.class), - @Spec(path = "userId", defaultVal = "null", spec = Equal.class), + @Spec(path = "workspaceId", defaultVal = "null", spec = Equal.class), }) Specification specification, Pageable pageable) { return ResponseEntity.ok(transactionService.findAll(specification, pageable)); } @GetMapping("/{id}") - public ResponseEntity getById(@PathVariable UUID id) { - return ResponseEntity.ok(transactionService.findById(id)); + public ResponseEntity getById( + @PathVariable UUID id, + @RequestParam UUID workspaceId) { + return ResponseEntity.ok(transactionService.getTransaction(id, workspaceId)); } @GetMapping("/integration/{id}") @@ -192,8 +195,8 @@ public class TransactonController { var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() .setType(TransactionWorkflow.TYPE) - .setExternalId(transaction.getUserId() + "-" + new Instant().getMillis()) - .setBusinessKey(transaction.getUserId()) + .setExternalId(transaction.getWorkspaceId() + "-" + new Instant().getMillis()) + .setBusinessKey(transaction.getWorkspaceId().toString()) .putStateVariable("confirmation", Utils.toJson(transaction)) .setNextActivation(DateTime.now()) .build(); diff --git a/src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java b/src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java new file mode 100644 index 0000000..4103e3a --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/controllers/WorkspaceController.java @@ -0,0 +1,93 @@ +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 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(); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteWorkspace(@PathVariable UUID id) { + if (workspaceService.deleteWorkspace(id)) { + return ResponseEntity.ok(Map.of("message", "Workspace deleted successfully")); + } + 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)); + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchCreateRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchCreateRequest.java index 56e04d8..dbfc1c5 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchCreateRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchCreateRequest.java @@ -11,7 +11,7 @@ import java.util.UUID; @JsonInclude(JsonInclude.Include.NON_NULL) public class GroupBatchCreateRequest { private UUID recipientGroupId; - private String userId; + private UUID workspaceId; private String description; private String requestedBy; private String currency; diff --git a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchItemUpdateRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchItemUpdateRequest.java index 5724a39..b61b9e5 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchItemUpdateRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchItemUpdateRequest.java @@ -25,6 +25,6 @@ public class GroupBatchItemUpdateRequest { @JsonInclude(JsonInclude.Include.NON_NULL) public static class ListWrapper { private List items; - private String userId; + private UUID workspaceId; } } \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchTriggerRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchTriggerRequest.java index 2a73de4..b8288fb 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchTriggerRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchTriggerRequest.java @@ -3,9 +3,11 @@ package zw.qantra.tm.domain.dtos; import lombok.Data; import zw.qantra.tm.domain.enums.AuthType; +import java.util.UUID; + @Data public class GroupBatchTriggerRequest { - private String userId; + private UUID workspaceId; private String idempotencyKey; private AuthType authType; } diff --git a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchUpdateRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchUpdateRequest.java index 23932e8..3d01cfb 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchUpdateRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/GroupBatchUpdateRequest.java @@ -11,7 +11,7 @@ import java.util.UUID; @JsonInclude(JsonInclude.Include.NON_NULL) public class GroupBatchUpdateRequest { private UUID id; - private String userId; + private UUID workspaceId; private String description; private String requestedBy; private String currency; diff --git a/src/main/java/zw/qantra/tm/domain/dtos/GroupMemberRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/GroupMemberRequest.java index bf1ce07..d3a7e64 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/GroupMemberRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/GroupMemberRequest.java @@ -9,7 +9,7 @@ import java.util.UUID; @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class GroupMemberRequest { - private String userId; + private UUID workspaceId; private List recipientIds; } diff --git a/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupCreateRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupCreateRequest.java index 1757118..f86261f 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupCreateRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupCreateRequest.java @@ -5,6 +5,7 @@ import lombok.Data; import zw.qantra.tm.domain.models.Recipient; import java.util.List; +import java.util.UUID; @Data @JsonInclude(JsonInclude.Include.NON_NULL) @@ -12,6 +13,7 @@ public class RecipientGroupCreateRequest { private String name; private String description; private String userId; + private UUID workspaceId; private List recipients; } diff --git a/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupUpdateRequest.java b/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupUpdateRequest.java index 59907ef..997b606 100644 --- a/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupUpdateRequest.java +++ b/src/main/java/zw/qantra/tm/domain/dtos/RecipientGroupUpdateRequest.java @@ -3,11 +3,13 @@ package zw.qantra.tm.domain.dtos; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; +import java.util.UUID; + @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class RecipientGroupUpdateRequest { private String name; private String description; - private String userId; + private UUID workspaceId; } diff --git a/src/main/java/zw/qantra/tm/domain/models/GroupBatch.java b/src/main/java/zw/qantra/tm/domain/models/GroupBatch.java index d82c07a..adf0376 100644 --- a/src/main/java/zw/qantra/tm/domain/models/GroupBatch.java +++ b/src/main/java/zw/qantra/tm/domain/models/GroupBatch.java @@ -16,7 +16,7 @@ import java.util.UUID; @Entity @Table(name = "group_batch", indexes = { - @Index(name = "idx_group_batch_user_id", columnList = "user_id"), + @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"), @@ -32,6 +32,8 @@ import java.util.UUID; public class GroupBatch extends BaseEntity { @Column(name = "recipient_group_id") private UUID recipientGroupId; + @Column(name = "workspace_id") + private UUID workspaceId; @Column(name = "user_id") private String userId; private String description; diff --git a/src/main/java/zw/qantra/tm/domain/models/Recipient.java b/src/main/java/zw/qantra/tm/domain/models/Recipient.java index 841a480..8ff94ea 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Recipient.java +++ b/src/main/java/zw/qantra/tm/domain/models/Recipient.java @@ -7,10 +7,12 @@ import jakarta.validation.constraints.NotNull; import lombok.*; import org.hibernate.annotations.SQLRestriction; +import java.util.UUID; + @Entity @Table(name = "recipient", indexes = { - @Index(name = "idx_recipient_account_user", columnList = "account, user_id"), - @Index(name = "idx_recipient_user_id", columnList = "user_id"), + @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"), }) @@ -31,6 +33,8 @@ public class Recipient extends BaseEntity { private String initials; @Column(name = "latest_provider_label") private String latestProviderLabel; + @Column(name = "workspace_id") + private UUID workspaceId; @Column(name = "user_id") private String userId; } diff --git a/src/main/java/zw/qantra/tm/domain/models/RecipientGroup.java b/src/main/java/zw/qantra/tm/domain/models/RecipientGroup.java index 6ea7f4e..5bee5c5 100644 --- a/src/main/java/zw/qantra/tm/domain/models/RecipientGroup.java +++ b/src/main/java/zw/qantra/tm/domain/models/RecipientGroup.java @@ -9,9 +9,11 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLRestriction; +import java.util.UUID; + @Entity @Table(name = "recipient_group", indexes = { - @Index(name = "idx_recipient_group_user_id", columnList = "user_id"), + @Index(name = "idx_recipient_group_workspace_id", columnList = "workspace_id"), }) @Getter @Setter @@ -23,6 +25,8 @@ import org.hibernate.annotations.SQLRestriction; public class RecipientGroup extends BaseEntity { private String name; private String description; + @Column(name = "workspace_id") + private UUID workspaceId; @Column(name = "user_id") private String userId; } diff --git a/src/main/java/zw/qantra/tm/domain/models/RecipientGroupMember.java b/src/main/java/zw/qantra/tm/domain/models/RecipientGroupMember.java index 35cc8bc..38ab856 100644 --- a/src/main/java/zw/qantra/tm/domain/models/RecipientGroupMember.java +++ b/src/main/java/zw/qantra/tm/domain/models/RecipientGroupMember.java @@ -32,6 +32,8 @@ public class RecipientGroupMember extends BaseEntity { @Column(name = "recipient_uid") private UUID recipientUid; private String account; + @Column(name = "workspace_id") + private UUID workspaceId; @Column(name = "user_id") private String userId; } diff --git a/src/main/java/zw/qantra/tm/domain/models/Transaction.java b/src/main/java/zw/qantra/tm/domain/models/Transaction.java index e90b7ef..8fa3091 100644 --- a/src/main/java/zw/qantra/tm/domain/models/Transaction.java +++ b/src/main/java/zw/qantra/tm/domain/models/Transaction.java @@ -15,7 +15,7 @@ import java.util.UUID; @Table(name = "transaction", indexes = { @Index(name = "idx_transaction_trace_type", columnList = "trace, type"), @Index(name = "idx_transaction_reference", columnList = "reference"), - @Index(name = "idx_transaction_user_id", columnList = "user_id"), + @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"), @@ -33,6 +33,8 @@ import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) @SQLRestriction("deleted = false") public class Transaction extends BaseEntity { + @Column(name = "workspace_id") + private UUID workspaceId; @Column(name = "user_id") private String userId; private String trace; diff --git a/src/main/java/zw/qantra/tm/domain/models/User.java b/src/main/java/zw/qantra/tm/domain/models/User.java index d9a673f..2ff46b9 100644 --- a/src/main/java/zw/qantra/tm/domain/models/User.java +++ b/src/main/java/zw/qantra/tm/domain/models/User.java @@ -1,5 +1,6 @@ package zw.qantra.tm.domain.models; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; @@ -9,7 +10,9 @@ 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 @Table(name = "users", indexes = { @@ -50,6 +53,10 @@ public class User extends BaseEntity implements UserDetails { @Column(name = "is_credentials_non_expired") private boolean credentialsNonExpired = true; + + @JsonIgnore + @ManyToMany(mappedBy = "users", fetch = FetchType.LAZY) + private Set workspaces = new HashSet<>(); @Override public Collection getAuthorities() { diff --git a/src/main/java/zw/qantra/tm/domain/models/Workspace.java b/src/main/java/zw/qantra/tm/domain/models/Workspace.java new file mode 100644 index 0000000..dfa52ef --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/models/Workspace.java @@ -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 users = new HashSet<>(); +} diff --git a/src/main/java/zw/qantra/tm/domain/repositories/GroupBatchRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/GroupBatchRepository.java index cc50350..6cb9981 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/GroupBatchRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/GroupBatchRepository.java @@ -5,11 +5,13 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import zw.qantra.tm.domain.models.GroupBatch; +import java.util.List; import java.util.Optional; import java.util.UUID; @Repository public interface GroupBatchRepository extends JpaRepository, JpaSpecificationExecutor { - Optional findByIdAndUserId(UUID id, String userId); + Optional findByIdAndWorkspaceId(UUID id, UUID workspaceId); + List findAllByWorkspaceIdIsNullAndUserIdIsNotNull(); } diff --git a/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupMemberRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupMemberRepository.java index 18baf0b..ab114e4 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupMemberRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupMemberRepository.java @@ -13,5 +13,6 @@ public interface RecipientGroupMemberRepository extends JpaRepository findAllByRecipientGroupId(UUID recipientGroupId); boolean existsByRecipientGroupIdAndRecipientId(UUID recipientGroupId, UUID recipientId); void deleteAllByRecipientGroupId(UUID recipientGroupId); + List findAllByWorkspaceIdIsNullAndUserIdIsNotNull(); } diff --git a/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupRepository.java index da86d8c..bdb6f41 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/RecipientGroupRepository.java @@ -11,7 +11,8 @@ import java.util.UUID; @Repository public interface RecipientGroupRepository extends JpaRepository, JpaSpecificationExecutor { - List findAllByUserId(String userId); - Optional findByIdAndUserId(UUID id, String userId); + List findAllByWorkspaceId(UUID workspaceId); + Optional findByIdAndWorkspaceId(UUID id, UUID workspaceId); + List findAllByWorkspaceIdIsNullAndUserIdIsNotNull(); } diff --git a/src/main/java/zw/qantra/tm/domain/repositories/RecipientRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/RecipientRepository.java index 6ffb9cc..5e7ea7f 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/RecipientRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/RecipientRepository.java @@ -13,5 +13,6 @@ public interface RecipientRepository extends JpaRepository, Jpa List findByNameContainingIgnoreCase(String name); List findByEmailContainingIgnoreCase(String email); List findByPhoneNumberContaining(String phoneNumber); - List findByAccountAndUserId(String account, String userId); -} \ No newline at end of file + List findByAccountAndWorkspaceId(String account, UUID workspaceId); + List findAllByWorkspaceIdIsNullAndUserIdIsNotNull(); +} diff --git a/src/main/java/zw/qantra/tm/domain/repositories/TransactionRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/TransactionRepository.java index 8264318..6f97b66 100644 --- a/src/main/java/zw/qantra/tm/domain/repositories/TransactionRepository.java +++ b/src/main/java/zw/qantra/tm/domain/repositories/TransactionRepository.java @@ -16,7 +16,10 @@ import java.util.UUID; public interface TransactionRepository extends JpaRepository, JpaSpecificationExecutor { Transaction findByTraceAndType(String trace, RequestType type); Optional findByReference(String reference); - List findAllByUserId(String uid); + List findAllByUserId(String userId); + Optional findByIdAndWorkspaceId(UUID id, UUID workspaceId); + List findAllByWorkspaceId(UUID workspaceId); + List findAllByWorkspaceIdIsNullAndUserIdIsNotNull(); List findAllByConfirmationStatusAndPaymentStatusAndIntegrationStatus( Status s1, Status s2, Status s3); List findAllByPaymentStatusAndPollingStatus(Status s1, Status s2); diff --git a/src/main/java/zw/qantra/tm/domain/repositories/WorkspaceRepository.java b/src/main/java/zw/qantra/tm/domain/repositories/WorkspaceRepository.java new file mode 100644 index 0000000..7235569 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/repositories/WorkspaceRepository.java @@ -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, JpaSpecificationExecutor { + List findByNameContainingIgnoreCase(String name); +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/BatchReportService.java b/src/main/java/zw/qantra/tm/domain/services/BatchReportService.java index 83fb4cb..05ef007 100644 --- a/src/main/java/zw/qantra/tm/domain/services/BatchReportService.java +++ b/src/main/java/zw/qantra/tm/domain/services/BatchReportService.java @@ -52,8 +52,8 @@ public class BatchReportService { private final GroupBatchItemRepository groupBatchItemRepository; @Transactional - public BatchReportResult generateBatchReport(UUID batchId, String userId) { - GroupBatch batch = groupBatchRepository.findByIdAndUserId(batchId, userId) + public BatchReportResult generateBatchReport(UUID batchId, UUID workspaceId) { + GroupBatch batch = groupBatchRepository.findByIdAndWorkspaceId(batchId, workspaceId) .orElseThrow(() -> new ApiException("Group batch not found")); if (!REPORTABLE_STATUSES.contains(batch.getStatus())) { diff --git a/src/main/java/zw/qantra/tm/domain/services/GroupBatchService.java b/src/main/java/zw/qantra/tm/domain/services/GroupBatchService.java index 1fb0bcd..b2a12aa 100644 --- a/src/main/java/zw/qantra/tm/domain/services/GroupBatchService.java +++ b/src/main/java/zw/qantra/tm/domain/services/GroupBatchService.java @@ -67,10 +67,10 @@ public class GroupBatchService { @Transactional public GroupBatch createBatch(GroupBatchCreateRequest request) { RecipientGroup group = recipientGroupService.get( - request.getRecipientGroupId(), request.getUserId()); + request.getRecipientGroupId(), request.getWorkspaceId()); List members = recipientGroupService.members( - group.getId(), request.getUserId()); + group.getId(), request.getWorkspaceId()); if (members.isEmpty()) { throw new ApiException("Recipient group has no members"); @@ -78,7 +78,7 @@ public class GroupBatchService { GroupBatch batch = GroupBatch.builder() .recipientGroupId(group.getId()) - .userId(request.getUserId()) + .workspaceId(request.getWorkspaceId()) .description(request.getDescription()) .requestedBy(request.getRequestedBy()) .currency(request.getCurrency()) @@ -126,8 +126,8 @@ public class GroupBatchService { return groupBatchRepository.save(savedBatch); } - public GroupBatch triggerConfirm(UUID batchId, String userId, String idempotencyKey) { - GroupBatch batch = getBatch(batchId, userId); + public GroupBatch triggerConfirm(UUID batchId, UUID workspaceId, String idempotencyKey) { + GroupBatch batch = getBatch(batchId, workspaceId); if (batch.getConfirmIdempotencyKey() != null && batch.getConfirmIdempotencyKey().equals(idempotencyKey)) { return batch; @@ -140,7 +140,7 @@ public class GroupBatchService { if (batch.getWorkflowId() == null) { var instance = workflowInstanceFactory.newWorkflowInstanceBuilder() .setType(BatchWorkflow.TYPE) - .setExternalId(batch.getUserId() + "-batch-" + new Instant().getMillis()) + .setExternalId(batch.getWorkspaceId() + "-batch-" + new Instant().getMillis()) .setBusinessKey(batch.getId().toString()) .putStateVariable("batchId", Utils.toJson(batch.getId().toString())) .setNextActivation(DateTime.now()) @@ -170,9 +170,9 @@ public class GroupBatchService { return batch; } - public Object triggerRequest(UUID batchId, String userId, + public Object triggerRequest(UUID batchId, UUID workspaceId, AuthType authType, String idempotencyKey) { - GroupBatch batch = getBatch(batchId, userId); + GroupBatch batch = getBatch(batchId, workspaceId); if (batch.getRequestIdempotencyKey() != null && batch.getRequestIdempotencyKey().equals(idempotencyKey)) { return batch; @@ -217,8 +217,8 @@ public class GroupBatchService { } - public Object triggerPoll(UUID batchId, String userId, String idempotencyKey) { - GroupBatch batch = getBatch(batchId, userId); + public Object triggerPoll(UUID batchId, UUID workspaceId, String idempotencyKey) { + GroupBatch batch = getBatch(batchId, workspaceId); if (batch.getPollIdempotencyKey() != null && batch.getPollIdempotencyKey().equals(idempotencyKey)) { return batch; @@ -284,8 +284,8 @@ public class GroupBatchService { } } - public GroupBatch getBatch(UUID batchId, String userId) { - return groupBatchRepository.findByIdAndUserId(batchId, userId) + public GroupBatch getBatch(UUID batchId, UUID workspaceId) { + return groupBatchRepository.findByIdAndWorkspaceId(batchId, workspaceId) .orElseThrow(() -> new ApiException("Group batch not found")); } @@ -299,7 +299,7 @@ public class GroupBatchService { @Transactional 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())) { throw new ApiException( @@ -350,9 +350,9 @@ public class GroupBatchService { } @Transactional - public List updateBatchItems(UUID batchId, String userId, - List items) { - GroupBatch batch = getBatch(batchId, userId); + public List updateBatchItems(UUID batchId, UUID workspaceId, + List items) { + GroupBatch batch = getBatch(batchId, workspaceId); if (!UPDATABLE_STATUSES.contains(batch.getStatus())) { throw new ApiException( @@ -424,8 +424,8 @@ public class GroupBatchService { } @Transactional - public void deleteBatch(UUID batchId, String userId) { - GroupBatch batch = getBatch(batchId, userId); + public void deleteBatch(UUID batchId, UUID workspaceId) { + GroupBatch batch = getBatch(batchId, workspaceId); if (!DELETABLE_STATUSES.contains(batch.getStatus())) { throw new ApiException( diff --git a/src/main/java/zw/qantra/tm/domain/services/GroupBatchWorkflowService.java b/src/main/java/zw/qantra/tm/domain/services/GroupBatchWorkflowService.java index 97bfa50..2892ae6 100644 --- a/src/main/java/zw/qantra/tm/domain/services/GroupBatchWorkflowService.java +++ b/src/main/java/zw/qantra/tm/domain/services/GroupBatchWorkflowService.java @@ -98,7 +98,7 @@ public class GroupBatchWorkflowService { BigDecimal total = confirmedItems.stream() .map(item -> item.getAmount() == null ? BigDecimal.ZERO : item.getAmount()) .reduce(BigDecimal.ZERO, BigDecimal::add); - User user = userService.getUserRepository().findById(UUID.fromString(batch.getUserId())).orElseThrow(); + User user = userService.getUserRepository().findByUsername(SecurityUtils.getCurrentUsername()).orElseThrow(); Transaction transaction = new Transaction(); transaction.setId(null); @@ -113,7 +113,7 @@ public class GroupBatchWorkflowService { transaction.setAmount(total); transaction.setTotalAmount(total); transaction.setReference("batch-" + batch.getId()); - transaction.setUserId(batch.getUserId()); + transaction.setWorkspaceId(batch.getWorkspaceId()); transaction.setPaymentProcessorLabel(batch.getPaymentProcessorLabel()); transaction.setPaymentProcessorName(batch.getPaymentProcessorName()); transaction.setPaymentProcessorImage(batch.getPaymentProcessorImage()); @@ -205,7 +205,7 @@ public class GroupBatchWorkflowService { private void confirmItem(GroupBatch batch, GroupBatchItem item) { GroupBatchItem mutableItem = groupBatchItemRepository.findById(item.getId()).orElse(item); try { - User user = userService.getUserRepository().findById(UUID.fromString(batch.getUserId())).orElseThrow(); + User user = userService.getUserRepository().findByUsername(SecurityUtils.getCurrentUsername()).orElseThrow(); Transaction transaction = new Transaction(); transaction.setId(null); transaction.setDebitPhone(user.getPhone()); @@ -219,7 +219,7 @@ public class GroupBatchWorkflowService { transaction.setProviderImage(item.getProviderImage()); transaction.setProviderLabel(item.getProviderLabel()); - transaction.setUserId(batch.getUserId()); + transaction.setWorkspaceId(batch.getWorkspaceId()); transaction.setType(RequestType.CONFIRM); transaction.setPartyType(PartyType.BATCH); transaction.setAuthType(AuthType.REMOTE); // default to remote for now diff --git a/src/main/java/zw/qantra/tm/domain/services/RecipientGroupService.java b/src/main/java/zw/qantra/tm/domain/services/RecipientGroupService.java index 9f9abf4..53a04cb 100644 --- a/src/main/java/zw/qantra/tm/domain/services/RecipientGroupService.java +++ b/src/main/java/zw/qantra/tm/domain/services/RecipientGroupService.java @@ -55,7 +55,7 @@ public class RecipientGroupService { * 5. Return the batch + any row-level errors */ @Transactional - public GroupCreateFromFileResult createFromFile(MultipartFile file, GroupCreateFromFileRequest request, String userId) { + public GroupCreateFromFileResult createFromFile(MultipartFile file, GroupCreateFromFileRequest request, UUID workspaceId) { ParseResult parseResult = parseCsv(file); if(!parseResult.parseErrors.isEmpty()) { @@ -77,7 +77,7 @@ public class RecipientGroupService { .name(row.name) .account(row.account) .phoneNumber(row.phone) - .userId(userId) + .workspaceId(workspaceId) .build(); recipients.add(recipient); } @@ -86,15 +86,15 @@ public class RecipientGroupService { RecipientGroup group = RecipientGroup.builder() .name(request.getGroupName()) .description(request.getDescription()) - .userId(userId) + .workspaceId(workspaceId) .build(); RecipientGroup savedGroup = recipientGroupRepository.save(group); - List members = addMembers(savedGroup.getId(), userId, recipients); + List members = addMembers(savedGroup.getId(), workspaceId, recipients); // Step 3: create group batch (inline to avoid circular dependency with GroupBatchService) - GroupBatch batch = createBatchInline(savedGroup.getId(), userId, request, members); + GroupBatch batch = createBatchInline(savedGroup.getId(), workspaceId, request, members); // Step 4: update batch items with bill/provider fields from CSV List batchItems = groupBatchItemRepository.findAllByGroupBatchId(batch.getId()); @@ -136,7 +136,7 @@ public class RecipientGroupService { } // Return the updated batch + errors - GroupBatch resultBatch = groupBatchRepository.findByIdAndUserId(batch.getId(), userId) + GroupBatch resultBatch = groupBatchRepository.findByIdAndWorkspaceId(batch.getId(), workspaceId) .orElseThrow(() -> new ApiException("Batch not found after creation")); return GroupCreateFromFileResult.builder() @@ -152,12 +152,12 @@ public class RecipientGroupService { * Create a GroupBatch and its items directly (mirrors GroupBatchService.createBatch logic * but without circular dependency). */ - private GroupBatch createBatchInline(UUID recipientGroupId, String userId, + private GroupBatch createBatchInline(UUID recipientGroupId, UUID workspaceId, GroupCreateFromFileRequest request, List members) { GroupBatch batch = GroupBatch.builder() .recipientGroupId(recipientGroupId) - .userId(userId) + .workspaceId(workspaceId) .description(request.getDescription()) .requestedBy(request.getRequestedBy()) .currency(request.getCurrency()) @@ -384,25 +384,26 @@ public class RecipientGroupService { RecipientGroup savedGroup = recipientGroupRepository.save(group); if (recipients != null && !recipients.isEmpty()) { - addMembers(savedGroup.getId(), savedGroup.getUserId(), recipients); + addMembers(savedGroup.getId(), savedGroup.getWorkspaceId(), recipients); } return savedGroup; } @Transactional - public List addMembers(UUID groupId, String userId, List recipients) { - get(groupId, userId); + public List addMembers(UUID groupId, UUID workspaceId, List recipients) { + get(groupId, workspaceId); List added = new ArrayList<>(); for (Recipient incomingRecipient : recipients) { - Recipient recipient = resolveOrCreateRecipient(incomingRecipient, userId); + Recipient recipient = resolveOrCreateRecipient(incomingRecipient, workspaceId); RecipientGroupMember member = RecipientGroupMember.builder() .recipientGroupId(groupId) .recipientUid(recipient.getId()) .recipient(recipient) .account(recipient.getAccount()) - .userId(userId) + .workspaceId(workspaceId) + .userId(incomingRecipient.getUserId()) .build(); added.add(recipientGroupMemberRepository.save(member)); } @@ -413,25 +414,25 @@ public class RecipientGroupService { return recipientGroupRepository.findAll(spec, pageable); } - public List list(String userId) { - return recipientGroupRepository.findAllByUserId(userId); + public List list(UUID workspaceId) { + return recipientGroupRepository.findAllByWorkspaceId(workspaceId); } - public RecipientGroup get(UUID groupId, String userId) { - return recipientGroupRepository.findByIdAndUserId(groupId, userId) + public RecipientGroup get(UUID groupId, UUID workspaceId) { + return recipientGroupRepository.findByIdAndWorkspaceId(groupId, workspaceId) .orElseThrow(() -> new ApiException("Recipient group not found")); } - public RecipientGroup update(UUID groupId, String userId, String name, String description) { - RecipientGroup group = get(groupId, userId); + public RecipientGroup update(UUID groupId, UUID workspaceId, String name, String description) { + RecipientGroup group = get(groupId, workspaceId); group.setName(name); group.setDescription(description); return recipientGroupRepository.save(group); } @Transactional - public void delete(UUID groupId, String userId) { - RecipientGroup group = get(groupId, userId); + public void delete(UUID groupId, UUID workspaceId) { + RecipientGroup group = get(groupId, workspaceId); group.setDeleted(true); recipientGroupRepository.save(group); @@ -441,14 +442,14 @@ public class RecipientGroupService { } @Transactional - public List addMembersByIds(UUID groupId, String userId, List recipientIds) { - get(groupId, userId); + public List addMembersByIds(UUID groupId, UUID workspaceId, List recipientIds) { + get(groupId, workspaceId); List added = new ArrayList<>(); for (UUID recipientId : recipientIds) { Recipient recipient = recipientRepository.findById(recipientId) .orElseThrow(() -> new ApiException("Recipient not found: " + recipientId)); - if (!userId.equals(recipient.getUserId())) { - throw new ApiException("Recipient does not belong to user"); + if (!workspaceId.equals(recipient.getWorkspaceId())) { + throw new ApiException("Recipient does not belong to workspace"); } if (recipientGroupMemberRepository.existsByRecipientGroupIdAndRecipientId(groupId, recipientId)) { continue; @@ -458,23 +459,23 @@ public class RecipientGroupService { .recipientUid(recipientId) .recipient(recipient) .account(recipient.getAccount()) - .userId(userId) + .workspaceId(workspaceId) .build(); added.add(recipientGroupMemberRepository.save(member)); } 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()) { throw new ApiException("Recipient account is required"); } - String recipientUserId = incomingRecipient.getUserId() == null || incomingRecipient.getUserId().isBlank() - ? fallbackUserId - : incomingRecipient.getUserId(); + UUID recipientWorkspaceId = incomingRecipient.getWorkspaceId() == null + ? fallbackWorkspaceId + : incomingRecipient.getWorkspaceId(); - List existing = recipientRepository.findByAccountAndUserId(incomingRecipient.getAccount(), recipientUserId); + List existing = recipientRepository.findByAccountAndWorkspaceId(incomingRecipient.getAccount(), recipientWorkspaceId); if (!existing.isEmpty()) { Recipient recipient = existing.get(0); recipient.setName(incomingRecipient.getName()); @@ -483,13 +484,13 @@ public class RecipientGroupService { } incomingRecipient.setId(null); - incomingRecipient.setUserId(recipientUserId); + incomingRecipient.setWorkspaceId(recipientWorkspaceId); return recipientRepository.save(incomingRecipient); } @Transactional - public void removeMember(UUID groupId, UUID recipientId, String userId) { - get(groupId, userId); + public void removeMember(UUID groupId, UUID recipientId, UUID workspaceId) { + get(groupId, workspaceId); List members = recipientGroupMemberRepository.findAllByRecipientGroupId(groupId); RecipientGroupMember target = members.stream() .filter(item -> recipientId.equals(item.getRecipientUid())) @@ -499,8 +500,8 @@ public class RecipientGroupService { recipientGroupMemberRepository.save(target); } - public List members(UUID groupId, String userId) { - get(groupId, userId); + public List members(UUID groupId, UUID workspaceId) { + get(groupId, workspaceId); return recipientGroupMemberRepository.findAllByRecipientGroupId(groupId); } diff --git a/src/main/java/zw/qantra/tm/domain/services/RecipientService.java b/src/main/java/zw/qantra/tm/domain/services/RecipientService.java index 1a86bc9..27b72dd 100644 --- a/src/main/java/zw/qantra/tm/domain/services/RecipientService.java +++ b/src/main/java/zw/qantra/tm/domain/services/RecipientService.java @@ -2,13 +2,14 @@ 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.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import zw.qantra.tm.domain.models.Recipient; import zw.qantra.tm.domain.repositories.RecipientRepository; import java.util.List; -import java.util.Optional; import java.util.UUID; @Service @@ -21,8 +22,8 @@ public class RecipientService { return recipientRepository.findAll(); } - public List getRecipient(String account, String userId) { - return recipientRepository.findByAccountAndUserId(account, userId); + public List getRecipient(String account, UUID workspaceId) { + return recipientRepository.findByAccountAndWorkspaceId(account, workspaceId); } public Recipient save(Recipient recipient) { @@ -45,7 +46,7 @@ public class RecipientService { return false; } - public List searchRecipients(Specification specification) { - return recipientRepository.findAll(specification); + public Page findAllRecipients(Specification specification, Pageable pageable) { + return recipientRepository.findAll(specification, pageable); } } \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java index 6c409a8..6ab83a8 100644 --- a/src/main/java/zw/qantra/tm/domain/services/TransactionService.java +++ b/src/main/java/zw/qantra/tm/domain/services/TransactionService.java @@ -47,16 +47,30 @@ public class TransactionService { return creditAmount; } - public void reassignTransactions(String oldUid, String newUid){ - List transactions = transactionRepository.findAllByUserId(oldUid); + public void reassignTransactions(UUID oldWorkspaceId, UUID newWorkspaceId){ + List transactions = transactionRepository.findAllByWorkspaceId(oldWorkspaceId); transactions.forEach(transaction -> { - transaction.setUserId(newUid); + transaction.setWorkspaceId(newWorkspaceId); transactionRepository.save(transaction); }); } + public void reassignTransactions(String oldUserId, String newUserId){ + List 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) { - 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 { AdditionalData additionalData = additionalDataService.getAdditionalDataRepository() diff --git a/src/main/java/zw/qantra/tm/domain/services/WorkspaceMigrationService.java b/src/main/java/zw/qantra/tm/domain/services/WorkspaceMigrationService.java new file mode 100644 index 0000000..f716d68 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/WorkspaceMigrationService.java @@ -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 recordsUpdated = new LinkedHashMap<>(); + + // Step 1: Create workspaces for users without one + int workspacesCreated = createWorkspacesForUsers(); + + // Build a userId → workspaceId lookup map + Map 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 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 buildUserToWorkspaceMap() { + Map map = new HashMap<>(); + List 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 userToWorkspace) { + List 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 userToWorkspace) { + List 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 userToWorkspace) { + List 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 userToWorkspace) { + List 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 userToWorkspace) { + List 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 recordsUpdated; + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java b/src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java new file mode 100644 index 0000000..a82c6e0 --- /dev/null +++ b/src/main/java/zw/qantra/tm/domain/services/WorkspaceService.java @@ -0,0 +1,95 @@ +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 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 getAllWorkspaces(Specification specification, Pageable pageable) { + return workspaceRepository.findAll(specification, pageable); + } + + public Optional getWorkspace(UUID id) { + return workspaceRepository.findById(id); + } + + public List 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 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 disassociateUser(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().remove(user); + workspaceRepository.save(workspace); + return Optional.of(workspace); + } + + @Transactional(readOnly = true) + public Set getWorkspaceUsers(UUID workspaceId) { + return workspaceRepository.findById(workspaceId) + .map(Workspace::getUsers) + .orElse(Set.of()); + } + + @Transactional(readOnly = true) + public Set getUserWorkspaces(UUID userId) { + return userRepository.findById(userId) + .map(User::getWorkspaces) + .orElse(Set.of()); + } +} \ No newline at end of file diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java index d38c065..c056aff 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/RegistrationWorkflow.java @@ -18,14 +18,12 @@ import zw.qantra.tm.domain.dtos.*; import zw.qantra.tm.domain.dtos.erp.VelocityCustomerResponseDto; import zw.qantra.tm.domain.models.Otp; import zw.qantra.tm.domain.models.User; -import zw.qantra.tm.domain.services.NotificationService; -import zw.qantra.tm.domain.services.OtpService; -import zw.qantra.tm.domain.services.UserService; +import zw.qantra.tm.domain.models.Workspace; +import zw.qantra.tm.domain.services.*; import io.nflow.engine.workflow.curated.State; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validator; -import zw.qantra.tm.domain.services.VelocityPaymentService; import zw.qantra.tm.exceptions.AlreadyExistsException; import zw.qantra.tm.rest.RestService; @@ -51,6 +49,8 @@ public class RegistrationWorkflow extends WorkflowDefinition { private NotificationService notificationService; @Autowired private VelocityPaymentService velocityPaymentService; + @Autowired + private WorkspaceService workspaceService; 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 RESEND_OTP = new State("resendOtp", WorkflowStateType.manual); 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 FAILED = new State("failed", WorkflowStateType.manual); 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(RESEND_OTP, VERIFY_OTP); 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); } @@ -167,9 +169,28 @@ public class RegistrationWorkflow extends WorkflowDefinition { 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) { RegisterRequest registerRequest = execution.getVariable("registerRequest", RegisterRequest.class); diff --git a/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandler.java b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandler.java index 2f2fdba..cadbdb3 100644 --- a/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandler.java +++ b/src/main/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandler.java @@ -21,7 +21,7 @@ public class RecipientsUpdateHandler implements HandlerInterface { try { if(transaction.getStatus() == Status.SUCCESS){ - List recipients = recipientService.getRecipient(transaction.getCreditAccount(), transaction.getUserId()); + List recipients = recipientService.getRecipient(transaction.getCreditAccount(), transaction.getWorkspaceId()); Recipient recipient = null; if(!recipients.isEmpty()){ @@ -30,6 +30,7 @@ public class RecipientsUpdateHandler implements HandlerInterface { recipient = new Recipient(); recipient.setAccount(transaction.getCreditAccount()); recipient.setUserId(transaction.getUserId()); + recipient.setWorkspaceId(transaction.getWorkspaceId()); } recipient.setLatestProviderLabel(transaction.getProviderLabel()); diff --git a/src/main/java/zw/qantra/tm/seeders/ProviderSeeder.java b/src/main/java/zw/qantra/tm/seeders/ProviderSeeder.java index 1836510..d1c1b5a 100644 --- a/src/main/java/zw/qantra/tm/seeders/ProviderSeeder.java +++ b/src/main/java/zw/qantra/tm/seeders/ProviderSeeder.java @@ -41,6 +41,7 @@ public class ProviderSeeder implements Seeder { .requiresAccount(false) .requiresAmount(true) .requiresPhone(true) + .requiresProducts(false) .priority(2) .meta("{\"hotProductId\": \"101\"}") .uid(Utils.getUid()) @@ -60,6 +61,7 @@ public class ProviderSeeder implements Seeder { .requiresAccount(false) .requiresAmount(true) .requiresPhone(true) + .requiresProducts(false) .priority(2) .meta("{\"hotProductId\": \"7\"}") .uid(Utils.getUid()) @@ -79,6 +81,7 @@ public class ProviderSeeder implements Seeder { .requiresAccount(false) .requiresAmount(true) .requiresPhone(true) + .requiresProducts(true) .uid(Utils.getUid()) .priority(3) .meta("{\"hotProductId\": \"35\"}") @@ -98,6 +101,7 @@ public class ProviderSeeder implements Seeder { .requiresAccount(true) .requiresAmount(true) .requiresPhone(true) + .requiresProducts(false) .priority(1) .meta("{\"hotProductId\": \"24\"}") .uid(Utils.getUid()) @@ -117,6 +121,7 @@ public class ProviderSeeder implements Seeder { .requiresAccount(true) .requiresAmount(true) .requiresPhone(true) + .requiresProducts(false) .priority(1) .meta("{\"hotProductId\": \"41\"}") .uid(Utils.getUid()) @@ -136,6 +141,7 @@ public class ProviderSeeder implements Seeder { .requiresAccount(false) .requiresAmount(false) .requiresPhone(true) + .requiresProducts(true) .priority(4) .meta("{\"hotProductId\": \"111\"}") .uid(Utils.getUid()) @@ -156,6 +162,7 @@ public class ProviderSeeder implements Seeder { .requiresAmount(false) .requiresPhone(true) .requiresProducts(true) + .requiresProducts(false) .priority(4) .meta("{\"hotProductId\": \"16\"}") .uid(Utils.getUid()) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 53cc7a1..c6c1262 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -32,7 +32,7 @@ mpgs.return-url=https://pay.velocityafrica.net/return # JWT Configuration jwt.secret=your-super-secret-jwt-key-here-make-it-very-long-and-secure-in-production -jwt.expiration=86400000 +jwt.expiration=7776000000 logging.level.root=WARN spring.datasource.nflow.url=jdbc:postgresql://62.171.179.108:5432/qpay-nflow diff --git a/src/test/java/zw/qantra/tm/domain/repositories/TransactionRepositoryTest.java b/src/test/java/zw/qantra/tm/domain/repositories/TransactionRepositoryTest.java index 98cfa5d..b1d85ea 100644 --- a/src/test/java/zw/qantra/tm/domain/repositories/TransactionRepositoryTest.java +++ b/src/test/java/zw/qantra/tm/domain/repositories/TransactionRepositoryTest.java @@ -17,6 +17,8 @@ import java.math.BigDecimal; import java.util.Optional; import java.util.UUID; +import static java.util.UUID.randomUUID; + import static org.junit.jupiter.api.Assertions.*; @DataJpaTest @@ -29,13 +31,14 @@ class TransactionRepositoryTest { @Test @DisplayName("Should save and find transaction by ID") void shouldSaveAndFindById() { - Transaction transaction = createTestTransaction(); + UUID wsId = randomUUID(); + Transaction transaction = createTestTransaction(wsId); Transaction saved = transactionRepository.save(transaction); Optional found = transactionRepository.findById(saved.getId()); assertTrue(found.isPresent()); - assertEquals("user-1", found.get().getUserId()); + assertEquals(wsId, found.get().getWorkspaceId()); assertEquals(new BigDecimal("100.00"), found.get().getAmount()); } @@ -48,19 +51,21 @@ class TransactionRepositoryTest { } @Test - @DisplayName("Should find transactions by userId") - void shouldFindByUserId() { + @DisplayName("Should find transactions by workspaceId") + void shouldFindByWorkspaceId() { + UUID ws1 = randomUUID(); + UUID ws2 = randomUUID(); Transaction tx1 = createTestTransaction(); - tx1.setUserId("user-1"); + tx1.setWorkspaceId(ws1); Transaction tx2 = createTestTransaction(); - tx2.setUserId("user-2"); + tx2.setWorkspaceId(ws2); transactionRepository.save(tx1); transactionRepository.save(tx2); - var results = transactionRepository.findAllByUserId("user-1"); + var results = transactionRepository.findAllByWorkspaceId(ws1); assertEquals(1, results.size()); - assertEquals("user-1", results.get(0).getUserId()); + assertEquals(ws1, results.get(0).getWorkspaceId()); } @Test @@ -68,7 +73,7 @@ class TransactionRepositoryTest { void shouldReturnPaginatedResults() { for (int i = 0; i < 5; i++) { Transaction tx = createTestTransaction(); - tx.setUserId("user-" + i); + tx.setWorkspaceId(randomUUID()); transactionRepository.save(tx); } @@ -97,7 +102,7 @@ class TransactionRepositoryTest { @DisplayName("Should persist all transaction fields correctly") void shouldPersistAllFields() { Transaction transaction = Transaction.builder() - .userId("user-1") + .workspaceId(randomUUID()) .trace("TRACE-123") .region("ZW") .amount(new BigDecimal("150.00")) @@ -134,7 +139,7 @@ class TransactionRepositoryTest { Optional found = transactionRepository.findById(saved.getId()); assertTrue(found.isPresent()); - assertEquals("user-1", found.get().getUserId()); + assertNotNull(found.get().getWorkspaceId()); assertEquals(new BigDecimal("150.00"), found.get().getAmount()); assertEquals(new BigDecimal("5.00"), found.get().getCharge()); assertEquals(new BigDecimal("158.50"), found.get().getTotalAmount()); @@ -146,8 +151,12 @@ class TransactionRepositoryTest { } private Transaction createTestTransaction() { + return createTestTransaction(randomUUID()); + } + + private Transaction createTestTransaction(UUID workspaceId) { return Transaction.builder() - .userId("user-1") + .workspaceId(workspaceId) .amount(new BigDecimal("100.00")) .debitCurrency(CurrencyType.USD) .build(); diff --git a/src/test/java/zw/qantra/tm/domain/services/TransactionServiceTest.java b/src/test/java/zw/qantra/tm/domain/services/TransactionServiceTest.java index c1f36b6..3ef997a 100644 --- a/src/test/java/zw/qantra/tm/domain/services/TransactionServiceTest.java +++ b/src/test/java/zw/qantra/tm/domain/services/TransactionServiceTest.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import static java.util.UUID.randomUUID; + import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -55,7 +57,7 @@ class TransactionServiceTest { void setUp() { transaction = new Transaction(); transaction.setId(txId); - transaction.setUserId("user-1"); + transaction.setWorkspaceId(randomUUID()); transaction.setAmount(new BigDecimal("100.00")); transaction.setDebitCurrency(CurrencyType.USD); transaction.setCreditCurrency(CurrencyType.ZWL); @@ -142,15 +144,17 @@ class TransactionServiceTest { } @Test - @DisplayName("Should reassign transactions from old user to new user") + @DisplayName("Should reassign transactions from old workspace to new workspace") void shouldReassignTransactions() { + UUID oldWs = randomUUID(); + UUID newWs = randomUUID(); List transactions = List.of(transaction); - when(transactionRepository.findAllByUserId("old-user")).thenReturn(transactions); + when(transactionRepository.findAllByWorkspaceId(oldWs)).thenReturn(transactions); when(transactionRepository.save(any(Transaction.class))).thenReturn(transaction); - transactionService.reassignTransactions("old-user", "new-user"); + transactionService.reassignTransactions(oldWs, newWs); - assertEquals("new-user", transaction.getUserId()); + assertEquals(newWs, transaction.getWorkspaceId()); verify(transactionRepository).save(transaction); } diff --git a/src/test/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandlerTest.java b/src/test/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandlerTest.java index 66b5434..93b734d 100644 --- a/src/test/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandlerTest.java +++ b/src/test/java/zw/qantra/tm/domain/services/processors/workflows/handlers/RecipientsUpdateHandlerTest.java @@ -13,6 +13,9 @@ import zw.qantra.tm.domain.models.Transaction; import zw.qantra.tm.domain.services.RecipientService; import java.util.List; +import java.util.UUID; + +import static java.util.UUID.randomUUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -34,7 +37,7 @@ class RecipientsUpdateHandlerTest { transaction = new Transaction(); transaction.setStatus(Status.SUCCESS); transaction.setCreditAccount("ACC-123"); - transaction.setUserId("user-1"); + transaction.setWorkspaceId(randomUUID()); transaction.setProviderLabel("econet"); transaction.setCreditName("John Doe"); transaction.setCreditEmail("john@example.com"); @@ -47,10 +50,10 @@ class RecipientsUpdateHandlerTest { void shouldUpdateExistingRecipient() { Recipient existingRecipient = new Recipient(); existingRecipient.setAccount("ACC-123"); - existingRecipient.setUserId("user-1"); + existingRecipient.setWorkspaceId(randomUUID()); existingRecipient.setName("Old Name"); - when(recipientService.getRecipient("ACC-123", "user-1")) + when(recipientService.getRecipient("ACC-123", transaction.getWorkspaceId())) .thenReturn(List.of(existingRecipient)); when(recipientService.save(any(Recipient.class))).thenReturn(existingRecipient); @@ -68,7 +71,7 @@ class RecipientsUpdateHandlerTest { @Test @DisplayName("Should create new recipient when not found") void shouldCreateNewRecipient() { - when(recipientService.getRecipient("ACC-123", "user-1")) + when(recipientService.getRecipient("ACC-123", transaction.getWorkspaceId())) .thenReturn(List.of()); when(recipientService.save(any(Recipient.class))).thenAnswer(i -> i.getArgument(0)); @@ -94,7 +97,7 @@ class RecipientsUpdateHandlerTest { void shouldUseBillNameForInitials() { transaction.setCreditName(null); - when(recipientService.getRecipient("ACC-123", "user-1")) + when(recipientService.getRecipient("ACC-123", transaction.getWorkspaceId())) .thenReturn(List.of()); when(recipientService.save(any(Recipient.class))).thenAnswer(i -> i.getArgument(0)); @@ -106,7 +109,7 @@ class RecipientsUpdateHandlerTest { @Test @DisplayName("Should handle exception gracefully without throwing") void shouldHandleExceptionGracefully() { - when(recipientService.getRecipient("ACC-123", "user-1")) + when(recipientService.getRecipient("ACC-123", transaction.getWorkspaceId())) .thenThrow(new RuntimeException("DB error")); assertDoesNotThrow(() -> handler.process(transaction)); @@ -121,9 +124,9 @@ class RecipientsUpdateHandlerTest { Recipient existingRecipient = new Recipient(); existingRecipient.setAccount("ACC-123"); - existingRecipient.setUserId("user-1"); + existingRecipient.setWorkspaceId(randomUUID()); - when(recipientService.getRecipient("ACC-123", "user-1")) + when(recipientService.getRecipient("ACC-123", transaction.getWorkspaceId())) .thenReturn(List.of(existingRecipient)); when(recipientService.save(any(Recipient.class))).thenReturn(existingRecipient);