From 61bea5fb6ec9d4d25ee6ddd259881ec8b9091643 Mon Sep 17 00:00:00 2001 From: Narayanan Madaswamy Date: Wed, 12 Aug 2026 09:38:18 +0530 Subject: [PATCH] Company options values, portfolio options values migration done --- .../cygnus/client/CloudIdentityClient.java | 18 +++ .../cygnus/client/model/CloudDataItem.java | 4 + .../cygnus/client/model/CloudDataRequest.java | 6 + .../platform/api/ScopedDataController.java | 49 ++++++ .../cloud/platform/api/ScopedDataItem.java | 7 + .../cloud/platform/api/ScopedDataRequest.java | 10 ++ .../repository/ScopedDataRepository.java | 79 ++++++++++ .../platform/service/ScopedDataException.java | 7 + .../platform/service/ScopedDataService.java | 74 +++++++++ .../security/CloudSecurityConfiguration.java | 2 + .../services/commons/CommonService.java | 26 +++- .../matrix/services/edp/PunchingService.java | 44 ++++-- .../services/edp/PunchingServiceTest.java | 29 ++-- .../db/017_company_branch_portfolios.sql | 54 +++++++ .../db/018_platform_options_tables.sql | 145 ++++++++++++++++++ .../db/020_platform_portfolio_references.sql | 76 +++++++++ .../db/021_portfolio_reference_query.sql | 8 + 17 files changed, 616 insertions(+), 22 deletions(-) create mode 100644 cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataItem.java create mode 100644 cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataRequest.java create mode 100644 cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataController.java create mode 100644 cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataItem.java create mode 100644 cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataRequest.java create mode 100644 cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/repository/ScopedDataRepository.java create mode 100644 cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataException.java create mode 100644 cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataService.java create mode 100644 cygnus-onprem-db/src/main/resources/db/017_company_branch_portfolios.sql create mode 100644 cygnus-onprem-db/src/main/resources/db/018_platform_options_tables.sql create mode 100644 cygnus-onprem-db/src/main/resources/db/020_platform_portfolio_references.sql create mode 100644 cygnus-onprem-db/src/main/resources/db/021_portfolio_reference_query.sql diff --git a/cygnus-cloud-client/src/main/java/com/cygnus/client/CloudIdentityClient.java b/cygnus-cloud-client/src/main/java/com/cygnus/client/CloudIdentityClient.java index f21dd8b..8f2076a 100644 --- a/cygnus-cloud-client/src/main/java/com/cygnus/client/CloudIdentityClient.java +++ b/cygnus-cloud-client/src/main/java/com/cygnus/client/CloudIdentityClient.java @@ -1,11 +1,15 @@ package com.cygnus.client; import com.cygnus.client.model.CloudIdentitySession; +import com.cygnus.client.model.CloudDataItem; +import com.cygnus.client.model.CloudDataRequest; import com.cygnus.client.model.CloudQueryResponse; import com.cygnus.client.model.LoginPayload; import com.cygnus.client.security.LoginEnvelopeEncryptor; import com.cygnus.client.security.MachineTokenProvider; import java.time.Clock; +import java.util.List; +import java.util.Map; import java.util.UUID; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -73,4 +77,18 @@ public class CloudIdentityClient { .bodyToMono(CloudQueryResponse.class)) .timeout(properties.requestTimeout()); } + + public Mono> fetchData(String scope, Map data) { + return tokenProvider.accessToken() + .flatMap(token -> webClient.post() + .uri(properties.baseUri().resolve("/api/v1/platform/data")) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(new CloudDataRequest(scope, Map.copyOf(data))) + .retrieve() + .bodyToFlux(CloudDataItem.class) + .collectList()) + .timeout(properties.requestTimeout()); + } } diff --git a/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataItem.java b/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataItem.java new file mode 100644 index 0000000..67821d1 --- /dev/null +++ b/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataItem.java @@ -0,0 +1,4 @@ +package com.cygnus.client.model; + +public record CloudDataItem(Object value, String label, String group) { +} diff --git a/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataRequest.java b/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataRequest.java new file mode 100644 index 0000000..4f189bc --- /dev/null +++ b/cygnus-cloud-client/src/main/java/com/cygnus/client/model/CloudDataRequest.java @@ -0,0 +1,6 @@ +package com.cygnus.client.model; + +import java.util.Map; + +public record CloudDataRequest(String scope, Map data) { +} diff --git a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataController.java b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataController.java new file mode 100644 index 0000000..0309581 --- /dev/null +++ b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataController.java @@ -0,0 +1,49 @@ +package com.cygnus.cloud.platform.api; + +import com.cygnus.cloud.platform.service.ScopedDataException; +import com.cygnus.cloud.platform.service.ScopedDataService; +import jakarta.validation.Valid; +import java.util.Map; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +@RestController +@RequestMapping("/api/v1/platform/data") +public class ScopedDataController { + private static final Logger LOGGER = LoggerFactory.getLogger(ScopedDataController.class); + private final ScopedDataService service; + + public ScopedDataController(ScopedDataService service) { + this.service = service; + } + + @PostMapping + public Flux data( + @AuthenticationPrincipal Jwt machineJwt, + @Valid @RequestBody ScopedDataRequest request) { + if (machineJwt == null) throw new ScopedDataException("Machine authentication required"); + try { + return service.fetch(UUID.fromString(machineJwt.getClaimAsString("tenant_id")), request); + } catch (IllegalArgumentException exception) { + throw new ScopedDataException("Machine tenant is invalid"); + } + } + + @ExceptionHandler(ScopedDataException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + Map invalidRequest(ScopedDataException exception) { + LOGGER.warn("Scoped platform-data request rejected: {}", exception.getMessage()); + return Map.of("code", "PLATFORM_DATA_INVALID", "message", exception.getMessage()); + } +} diff --git a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataItem.java b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataItem.java new file mode 100644 index 0000000..d6a0296 --- /dev/null +++ b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataItem.java @@ -0,0 +1,7 @@ +package com.cygnus.cloud.platform.api; + +public record ScopedDataItem( + Object value, + String label, + String group) { +} diff --git a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataRequest.java b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataRequest.java new file mode 100644 index 0000000..8905e90 --- /dev/null +++ b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/api/ScopedDataRequest.java @@ -0,0 +1,10 @@ +package com.cygnus.cloud.platform.api; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.Map; + +public record ScopedDataRequest( + @NotBlank String scope, + @NotNull Map data) { +} diff --git a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/repository/ScopedDataRepository.java b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/repository/ScopedDataRepository.java new file mode 100644 index 0000000..8b49942 --- /dev/null +++ b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/repository/ScopedDataRepository.java @@ -0,0 +1,79 @@ +package com.cygnus.cloud.platform.repository; + +import com.cygnus.cloud.database.ReactiveDatabaseClient; +import com.cygnus.cloud.platform.api.ScopedDataItem; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.Tuple; +import java.util.List; +import java.util.UUID; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +@Repository +public class ScopedDataRepository { + private static final String COMPANY_OPTIONS = """ + SELECT ov.opt_value AS value, + ov.val_description AS label, + lower(o.description) || '.' AS group_name + FROM platform.company_options_mapping m + JOIN platform.options o ON o.option_id = m.option_id + JOIN platform.options_values ov ON ov.opt_value_id = m.opt_value_id + JOIN identity.company c ON c.company_id = m.company_id + WHERE c.tenant_id = $1 + AND m.company_id = $2 + AND o.isactive = 1 + AND ov.isactive = 1 + AND o.description = ANY($3::text[]) + ORDER BY o.description, ov.val_description + """; + private static final String PORTFOLIO_OPTIONS = """ + SELECT ov.opt_value AS value, + ov.val_description AS label, + lower(o.description) || '.' AS group_name + FROM platform.options_mapping m + JOIN platform.options o ON o.option_id = m.option_id + JOIN platform.options_values ov ON ov.opt_value_id = m.opt_value_id + JOIN identity.company c ON c.company_id = m.company_id + JOIN platform.company_branch_portfolios cbp + ON cbp.company_id = m.company_id + AND cbp.portfolio_reference = m.portfolio_reference + WHERE c.tenant_id = $1 + AND m.company_id = $2 + AND cbp.company_branch_id = $3 + AND m.portfolio_reference = $4 + AND o.isactive = 1 + AND ov.isactive = 1 + AND o.description = ANY($5::text[]) + ORDER BY o.description, ov.val_description + """; + + private final ReactiveDatabaseClient database; + + public ScopedDataRepository(ReactiveDatabaseClient database) { + this.database = database; + } + + public Flux companyOptions( + UUID tenantId, short companyId, List descriptions) { + return rows(COMPANY_OPTIONS, Tuple.of(tenantId, companyId, descriptions.toArray(String[]::new))); + } + + public Flux portfolioOptions( + UUID tenantId, short companyId, short branchId, UUID portfolioReference, + List descriptions) { + return rows(PORTFOLIO_OPTIONS, + Tuple.of(tenantId, companyId, branchId, portfolioReference, + descriptions.toArray(String[]::new))); + } + + private Flux rows(String sql, Tuple parameters) { + return database.preparedQuery(sql, parameters) + .flatMapMany(result -> Flux.fromIterable(result)) + .map(this::item); + } + + private ScopedDataItem item(Row row) { + return new ScopedDataItem( + row.getValue("value"), row.getString("label"), row.getString("group_name")); + } +} diff --git a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataException.java b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataException.java new file mode 100644 index 0000000..132d71c --- /dev/null +++ b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataException.java @@ -0,0 +1,7 @@ +package com.cygnus.cloud.platform.service; + +public class ScopedDataException extends RuntimeException { + public ScopedDataException(String message) { + super(message); + } +} diff --git a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataService.java b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataService.java new file mode 100644 index 0000000..df542cc --- /dev/null +++ b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/platform/service/ScopedDataService.java @@ -0,0 +1,74 @@ +package com.cygnus.cloud.platform.service; + +import com.cygnus.cloud.platform.api.ScopedDataItem; +import com.cygnus.cloud.platform.api.ScopedDataRequest; +import com.cygnus.cloud.platform.repository.ScopedDataRepository; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +@Service +public class ScopedDataService { + public static final String COMPANY_OPTIONS = "company-options"; + public static final String PORTFOLIO_OPTIONS = "portfolio-options"; + + private final ScopedDataRepository repository; + + public ScopedDataService(ScopedDataRepository repository) { + this.repository = repository; + } + + public Flux fetch(UUID tenantId, ScopedDataRequest request) { + Map data = request.data(); + short companyId = shortValue(data, "companyId"); + List descriptions = descriptions(data); + return switch (request.scope().trim().toLowerCase(Locale.ROOT)) { + case COMPANY_OPTIONS -> repository.companyOptions(tenantId, companyId, descriptions); + case PORTFOLIO_OPTIONS -> repository.portfolioOptions( + tenantId, companyId, shortValue(data, "branchId"), + uuidValue(data, "portfolioReference"), descriptions); + default -> Flux.error(new ScopedDataException("Unsupported data scope")); + }; + } + + private UUID uuidValue(Map data, String name) { + try { + return UUID.fromString(String.valueOf(data.get(name))); + } catch (RuntimeException exception) { + throw new ScopedDataException(name + " must be a valid UUID"); + } + } + + private short shortValue(Map data, String name) { + Object value = data.get(name); + try { + short result = value instanceof Number number + ? number.shortValue() : Short.parseShort(String.valueOf(value)); + if (result <= 0) throw new NumberFormatException(); + return result; + } catch (RuntimeException exception) { + throw new ScopedDataException(name + " must be a positive number"); + } + } + + private List descriptions(Map data) { + Object value = data.get("descriptions"); + if (!(value instanceof List values)) { + throw new ScopedDataException("descriptions must be a non-empty list"); + } + List result = values.stream() + .map(String::valueOf) + .map(String::trim) + .filter(text -> !text.isEmpty()) + .map(text -> text.toUpperCase(Locale.ROOT)) + .distinct() + .toList(); + if (result.isEmpty() || result.size() > 20) { + throw new ScopedDataException("descriptions must contain between 1 and 20 values"); + } + return result; + } +} diff --git a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/CloudSecurityConfiguration.java b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/CloudSecurityConfiguration.java index db90c1e..3622ecf 100644 --- a/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/CloudSecurityConfiguration.java +++ b/cygnus-cloud-service/src/main/java/com/cygnus/cloud/security/CloudSecurityConfiguration.java @@ -38,6 +38,8 @@ public class CloudSecurityConfiguration { .hasAuthority("SCOPE_identity.login") .pathMatchers("/api/v1/queries/**") .hasAuthority("SCOPE_identity.login") + .pathMatchers("/api/v1/platform/data") + .hasAuthority("SCOPE_identity.login") .pathMatchers("/api/v1/admin/**") .hasAuthority("SCOPE_cygnus.admin") .anyExchange().authenticated()) diff --git a/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonService.java b/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonService.java index a0e2720..4dc5a64 100644 --- a/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonService.java +++ b/cygnus-onprem-app/src/main/java/matrix/services/commons/CommonService.java @@ -1,11 +1,14 @@ package matrix.services.commons; import com.cygnus.db.CygnusDbExecutor; +import com.cygnus.client.CloudIdentityClient; +import com.cygnus.client.model.CloudDataItem; import jakarta.servlet.http.HttpSession; import lib.models.Option; import lib.models.UserSession; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -23,16 +26,19 @@ public class CommonService { Pattern.CASE_INSENSITIVE); private final CygnusDbExecutor dbExecutor; + private final CloudIdentityClient cloudIdentityClient; @Autowired - public CommonService(CygnusDbExecutor dbExecutor) { + public CommonService(CygnusDbExecutor dbExecutor, CloudIdentityClient cloudIdentityClient) { this.dbExecutor = Objects.requireNonNull(dbExecutor, "dbExecutor"); + this.cloudIdentityClient = Objects.requireNonNull(cloudIdentityClient, "cloudIdentityClient"); } /* Used by the session-only unit tests. Production construction is handled by Spring through the CygnusDbExecutor constructor above. */ public CommonService() { this.dbExecutor = null; + this.cloudIdentityClient = null; } public Session getSession(HttpSession httpSession) { @@ -138,4 +144,22 @@ public class CommonService { Object[] parameters = queryParams == null ? new Object[0] : queryParams; return dbExecutor.query(queryId, parameters, Option.class); } + + public List