Company options values, portfolio options values migration done
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
package com.cygnus.client;
|
package com.cygnus.client;
|
||||||
|
|
||||||
import com.cygnus.client.model.CloudIdentitySession;
|
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.CloudQueryResponse;
|
||||||
import com.cygnus.client.model.LoginPayload;
|
import com.cygnus.client.model.LoginPayload;
|
||||||
import com.cygnus.client.security.LoginEnvelopeEncryptor;
|
import com.cygnus.client.security.LoginEnvelopeEncryptor;
|
||||||
import com.cygnus.client.security.MachineTokenProvider;
|
import com.cygnus.client.security.MachineTokenProvider;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -73,4 +77,18 @@ public class CloudIdentityClient {
|
|||||||
.bodyToMono(CloudQueryResponse.class))
|
.bodyToMono(CloudQueryResponse.class))
|
||||||
.timeout(properties.requestTimeout());
|
.timeout(properties.requestTimeout());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Mono<List<CloudDataItem>> fetchData(String scope, Map<String, Object> 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.cygnus.client.model;
|
||||||
|
|
||||||
|
public record CloudDataItem(Object value, String label, String group) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.cygnus.client.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record CloudDataRequest(String scope, Map<String, Object> data) {
|
||||||
|
}
|
||||||
@@ -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<ScopedDataItem> 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<String, String> invalidRequest(ScopedDataException exception) {
|
||||||
|
LOGGER.warn("Scoped platform-data request rejected: {}", exception.getMessage());
|
||||||
|
return Map.of("code", "PLATFORM_DATA_INVALID", "message", exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.cygnus.cloud.platform.api;
|
||||||
|
|
||||||
|
public record ScopedDataItem(
|
||||||
|
Object value,
|
||||||
|
String label,
|
||||||
|
String group) {
|
||||||
|
}
|
||||||
@@ -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<String, Object> data) {
|
||||||
|
}
|
||||||
@@ -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<ScopedDataItem> companyOptions(
|
||||||
|
UUID tenantId, short companyId, List<String> descriptions) {
|
||||||
|
return rows(COMPANY_OPTIONS, Tuple.of(tenantId, companyId, descriptions.toArray(String[]::new)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<ScopedDataItem> portfolioOptions(
|
||||||
|
UUID tenantId, short companyId, short branchId, UUID portfolioReference,
|
||||||
|
List<String> descriptions) {
|
||||||
|
return rows(PORTFOLIO_OPTIONS,
|
||||||
|
Tuple.of(tenantId, companyId, branchId, portfolioReference,
|
||||||
|
descriptions.toArray(String[]::new)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Flux<ScopedDataItem> 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.cygnus.cloud.platform.service;
|
||||||
|
|
||||||
|
public class ScopedDataException extends RuntimeException {
|
||||||
|
public ScopedDataException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ScopedDataItem> fetch(UUID tenantId, ScopedDataRequest request) {
|
||||||
|
Map<String, Object> data = request.data();
|
||||||
|
short companyId = shortValue(data, "companyId");
|
||||||
|
List<String> 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<String, Object> 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<String, Object> 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<String> descriptions(Map<String, Object> data) {
|
||||||
|
Object value = data.get("descriptions");
|
||||||
|
if (!(value instanceof List<?> values)) {
|
||||||
|
throw new ScopedDataException("descriptions must be a non-empty list");
|
||||||
|
}
|
||||||
|
List<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ public class CloudSecurityConfiguration {
|
|||||||
.hasAuthority("SCOPE_identity.login")
|
.hasAuthority("SCOPE_identity.login")
|
||||||
.pathMatchers("/api/v1/queries/**")
|
.pathMatchers("/api/v1/queries/**")
|
||||||
.hasAuthority("SCOPE_identity.login")
|
.hasAuthority("SCOPE_identity.login")
|
||||||
|
.pathMatchers("/api/v1/platform/data")
|
||||||
|
.hasAuthority("SCOPE_identity.login")
|
||||||
.pathMatchers("/api/v1/admin/**")
|
.pathMatchers("/api/v1/admin/**")
|
||||||
.hasAuthority("SCOPE_cygnus.admin")
|
.hasAuthority("SCOPE_cygnus.admin")
|
||||||
.anyExchange().authenticated())
|
.anyExchange().authenticated())
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package matrix.services.commons;
|
package matrix.services.commons;
|
||||||
|
|
||||||
import com.cygnus.db.CygnusDbExecutor;
|
import com.cygnus.db.CygnusDbExecutor;
|
||||||
|
import com.cygnus.client.CloudIdentityClient;
|
||||||
|
import com.cygnus.client.model.CloudDataItem;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import lib.models.Option;
|
import lib.models.Option;
|
||||||
import lib.models.UserSession;
|
import lib.models.UserSession;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -23,16 +26,19 @@ public class CommonService {
|
|||||||
Pattern.CASE_INSENSITIVE);
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
private final CygnusDbExecutor dbExecutor;
|
private final CygnusDbExecutor dbExecutor;
|
||||||
|
private final CloudIdentityClient cloudIdentityClient;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public CommonService(CygnusDbExecutor dbExecutor) {
|
public CommonService(CygnusDbExecutor dbExecutor, CloudIdentityClient cloudIdentityClient) {
|
||||||
this.dbExecutor = Objects.requireNonNull(dbExecutor, "dbExecutor");
|
this.dbExecutor = Objects.requireNonNull(dbExecutor, "dbExecutor");
|
||||||
|
this.cloudIdentityClient = Objects.requireNonNull(cloudIdentityClient, "cloudIdentityClient");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Used by the session-only unit tests. Production construction is handled
|
/* Used by the session-only unit tests. Production construction is handled
|
||||||
by Spring through the CygnusDbExecutor constructor above. */
|
by Spring through the CygnusDbExecutor constructor above. */
|
||||||
public CommonService() {
|
public CommonService() {
|
||||||
this.dbExecutor = null;
|
this.dbExecutor = null;
|
||||||
|
this.cloudIdentityClient = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Session getSession(HttpSession httpSession) {
|
public Session getSession(HttpSession httpSession) {
|
||||||
@@ -138,4 +144,22 @@ public class CommonService {
|
|||||||
Object[] parameters = queryParams == null ? new Object[0] : queryParams;
|
Object[] parameters = queryParams == null ? new Object[0] : queryParams;
|
||||||
return dbExecutor.query(queryId, parameters, Option.class);
|
return dbExecutor.query(queryId, parameters, Option.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Option> getCloudOptions(String scope, Map<String, Object> data) {
|
||||||
|
if (cloudIdentityClient == null) {
|
||||||
|
throw new IllegalStateException("CloudIdentityClient is not configured");
|
||||||
|
}
|
||||||
|
return cloudIdentityClient.fetchData(scope, data)
|
||||||
|
.map(items -> items.stream().map(this::option).toList())
|
||||||
|
.blockOptional()
|
||||||
|
.orElseGet(List::of);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Option option(CloudDataItem item) {
|
||||||
|
Option option = new Option();
|
||||||
|
option.setValue(item.value());
|
||||||
|
option.setLabel(item.label());
|
||||||
|
option.setGroup(item.group());
|
||||||
|
return option;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package matrix.services.edp;
|
|||||||
|
|
||||||
import com.cygnus.db.CygnusDbExecutor;
|
import com.cygnus.db.CygnusDbExecutor;
|
||||||
import com.cygnus.db.RowView;
|
import com.cygnus.db.RowView;
|
||||||
import com.cygnus.db.SqlArrayParameter;
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@@ -10,6 +9,7 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Date;
|
import java.sql.Date;
|
||||||
@@ -60,8 +60,8 @@ public class PunchingService {
|
|||||||
private static final String ADD_CASE_TEMPLATE = "ADDCASE";
|
private static final String ADD_CASE_TEMPLATE = "ADDCASE";
|
||||||
private static final Pattern DYNAMIC_FIELD = Pattern.compile(
|
private static final Pattern DYNAMIC_FIELD = Pattern.compile(
|
||||||
"(?:field(?:[1-9]|1[0-2]|150(?:_2)?)|dtfield1)", Pattern.CASE_INSENSITIVE);
|
"(?:field(?:[1-9]|1[0-2]|150(?:_2)?)|dtfield1)", Pattern.CASE_INSENSITIVE);
|
||||||
private static final SqlArrayParameter GENERAL_OPTION_TYPES = SqlArrayParameter.text("CITY", "APPTYPE");
|
private static final List<String> GENERAL_OPTION_TYPES = List.of("CITY", "APPTYPE");
|
||||||
private static final SqlArrayParameter PORTFOLIO_OPTION_TYPES = SqlArrayParameter.text("CATEGORY", "PRODUCT");
|
private static final List<String> PORTFOLIO_OPTION_TYPES = List.of("CATEGORY", "PRODUCT");
|
||||||
|
|
||||||
private final CommonService commonService;
|
private final CommonService commonService;
|
||||||
private final CygnusDbExecutor dbExecutor;
|
private final CygnusDbExecutor dbExecutor;
|
||||||
@@ -467,17 +467,43 @@ public class PunchingService {
|
|||||||
Short branchId, Short companyId, Short portfolioId) {
|
Short branchId, Short companyId, Short portfolioId) {
|
||||||
|
|
||||||
Map<String, List<Option>> options = new LinkedHashMap<>();
|
Map<String, List<Option>> options = new LinkedHashMap<>();
|
||||||
addOptions(options, PORTFOLIO, commonService.getOptions(
|
List<Option> portfolios = commonService.getOptions(
|
||||||
3, new Object[] { branchId, ACTIVE }));
|
3, new Object[] { branchId, ACTIVE });
|
||||||
|
addOptions(options, PORTFOLIO, portfolios);
|
||||||
addOptions(options, BRANCH, commonService.getOptions(
|
addOptions(options, BRANCH, commonService.getOptions(
|
||||||
4, new Object[] { portfolioId, ACTIVE }));
|
4, new Object[] { portfolioId, ACTIVE }));
|
||||||
addGroupedOptions(options, commonService.getOptions(
|
addGroupedOptions(options, commonService.getCloudOptions(
|
||||||
5, new Object[] { companyId, GENERAL_OPTION_TYPES }));
|
"company-options", Map.of(
|
||||||
addGroupedOptions(options, commonService.getOptions(
|
"companyId", companyId,
|
||||||
70, new Object[] { portfolioId, PORTFOLIO_OPTION_TYPES }));
|
"descriptions", GENERAL_OPTION_TYPES)));
|
||||||
|
addGroupedOptions(options, commonService.getCloudOptions(
|
||||||
|
"portfolio-options", Map.of(
|
||||||
|
"companyId", companyId,
|
||||||
|
"branchId", branchId,
|
||||||
|
"portfolioReference", portfolioReference(portfolios, portfolioId),
|
||||||
|
"descriptions", PORTFOLIO_OPTION_TYPES)));
|
||||||
return freezeOptions(options);
|
return freezeOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UUID portfolioReference(List<Option> portfolios, Short portfolioId) {
|
||||||
|
String reference = portfolios.stream()
|
||||||
|
.filter(option -> option != null
|
||||||
|
&& String.valueOf(option.getValue()).equals(String.valueOf(portfolioId)))
|
||||||
|
.map(Option::getDescription)
|
||||||
|
.filter(candidate -> candidate != null && !candidate.isBlank())
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new ApplicationException(
|
||||||
|
ApplicationMessage.APPLICATION_ACCESS_DENIED,
|
||||||
|
"Portfolio reference is unavailable"));
|
||||||
|
try {
|
||||||
|
return UUID.fromString(reference);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
throw new ApplicationException(
|
||||||
|
ApplicationMessage.APPLICATION_ACCESS_DENIED,
|
||||||
|
"Portfolio reference is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public CasePunching prepareApplicationEdit(
|
public CasePunching prepareApplicationEdit(
|
||||||
Short portfolioId,
|
Short portfolioId,
|
||||||
String documentCaseId,
|
String documentCaseId,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
|
|
||||||
import com.cygnus.db.CygnusDbExecutor;
|
import com.cygnus.db.CygnusDbExecutor;
|
||||||
import com.cygnus.db.RowView;
|
import com.cygnus.db.RowView;
|
||||||
import com.cygnus.db.SqlArrayParameter;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.lang.reflect.Proxy;
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -131,19 +130,12 @@ class PunchingServiceTest {
|
|||||||
|
|
||||||
Map<String, List<Option>> options = service.loadOptions((short) 5, (short) 1, (short) 38);
|
Map<String, List<Option>> options = service.loadOptions((short) 5, (short) 1, (short) 38);
|
||||||
|
|
||||||
assertEquals(List.of(3, 4, 5, 70),
|
assertEquals(List.of(3, 4),
|
||||||
commonService.calls.stream().map(Call::queryId).toList());
|
commonService.calls.stream().map(Call::queryId).toList());
|
||||||
assertArrayEquals(new Object[] { (short) 5, (short) 1 },
|
assertArrayEquals(new Object[] { (short) 5, (short) 1 },
|
||||||
commonService.calls.get(0).parameters());
|
commonService.calls.get(0).parameters());
|
||||||
assertArrayEquals(new Object[] { (short) 38, (short) 1 },
|
assertArrayEquals(new Object[] { (short) 38, (short) 1 },
|
||||||
commonService.calls.get(1).parameters());
|
commonService.calls.get(1).parameters());
|
||||||
assertEquals((short) 1, commonService.calls.get(2).parameters()[0]);
|
|
||||||
assertArrayEquals(new Object[] { "CITY", "APPTYPE" },
|
|
||||||
((SqlArrayParameter) commonService.calls.get(2).parameters()[1]).values());
|
|
||||||
assertEquals((short) 38, commonService.calls.get(3).parameters()[0]);
|
|
||||||
assertArrayEquals(new Object[] { "CATEGORY", "PRODUCT" },
|
|
||||||
((SqlArrayParameter) commonService.calls.get(3).parameters()[1]).values());
|
|
||||||
|
|
||||||
assertEquals("Portfolio", options.get("portfolio.").getFirst().getLabel());
|
assertEquals("Portfolio", options.get("portfolio.").getFirst().getLabel());
|
||||||
assertEquals("Branch", options.get("branch.").getFirst().getLabel());
|
assertEquals("Branch", options.get("branch.").getFirst().getLabel());
|
||||||
assertEquals("City", options.get("city.").getFirst().getLabel());
|
assertEquals("City", options.get("city.").getFirst().getLabel());
|
||||||
@@ -196,12 +188,20 @@ class PunchingServiceTest {
|
|||||||
public List<Option> getOptions(Integer queryId, Object[] queryParams) {
|
public List<Option> getOptions(Integer queryId, Object[] queryParams) {
|
||||||
calls.add(new Call(queryId, queryParams.clone()));
|
calls.add(new Call(queryId, queryParams.clone()));
|
||||||
return switch (queryId) {
|
return switch (queryId) {
|
||||||
case 3 -> List.of(option(38, "Portfolio", "portfolio."));
|
case 3 -> List.of(option(38, "Portfolio", "portfolio.",
|
||||||
|
"88e0cb4a-3b4d-401c-b11b-fe4117842fef"));
|
||||||
case 4 -> List.of(option(7, "Branch", "branch."));
|
case 4 -> List.of(option(7, "Branch", "branch."));
|
||||||
case 5 -> List.of(
|
default -> List.of();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Option> getCloudOptions(String scope, Map<String, Object> data) {
|
||||||
|
return switch (scope) {
|
||||||
|
case "company-options" -> List.of(
|
||||||
option("DEL", "City", "city."),
|
option("DEL", "City", "city."),
|
||||||
option("APPLICANT", "Applicant", "apptype."));
|
option("APPLICANT", "Applicant", "apptype."));
|
||||||
case 70 -> List.of(
|
case "portfolio-options" -> List.of(
|
||||||
option("DIRECTOR", "Category", "category."),
|
option("DIRECTOR", "Category", "category."),
|
||||||
option("AUTO", "Product", "product."));
|
option("AUTO", "Product", "product."));
|
||||||
default -> List.of();
|
default -> List.of();
|
||||||
@@ -210,10 +210,15 @@ class PunchingServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Option option(Object value, String label, String group) {
|
private static Option option(Object value, String label, String group) {
|
||||||
|
return option(value, label, group, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Option option(Object value, String label, String group, String description) {
|
||||||
Option option = new Option();
|
Option option = new Option();
|
||||||
option.setValue(value);
|
option.setValue(value);
|
||||||
option.setLabel(label);
|
option.setLabel(label);
|
||||||
option.setGroup(group);
|
option.setGroup(group);
|
||||||
|
option.setDescription(description);
|
||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS platform;
|
||||||
|
|
||||||
|
ALTER TABLE public.portfolio
|
||||||
|
ADD COLUMN IF NOT EXISTS portfolio_reference uuid;
|
||||||
|
|
||||||
|
UPDATE public.portfolio
|
||||||
|
SET portfolio_reference = gen_random_uuid()
|
||||||
|
WHERE portfolio_reference IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE public.portfolio
|
||||||
|
ALTER COLUMN portfolio_reference SET DEFAULT gen_random_uuid(),
|
||||||
|
ALTER COLUMN portfolio_reference SET NOT NULL;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_portfolio_reference
|
||||||
|
ON public.portfolio (portfolio_reference);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_company_branch_company_branch
|
||||||
|
ON public.company_branch (company_id, branch_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS platform.company_branch_portfolios (
|
||||||
|
company_id smallint NOT NULL,
|
||||||
|
company_branch_id smallint NOT NULL,
|
||||||
|
portfolio_reference uuid NOT NULL,
|
||||||
|
createdon timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT pk_company_branch_portfolios
|
||||||
|
PRIMARY KEY (company_id, company_branch_id, portfolio_reference),
|
||||||
|
CONSTRAINT fk_company_branch_portfolios_branch
|
||||||
|
FOREIGN KEY (company_id, company_branch_id)
|
||||||
|
REFERENCES public.company_branch (company_id, branch_id),
|
||||||
|
CONSTRAINT fk_company_branch_portfolios_portfolio
|
||||||
|
FOREIGN KEY (portfolio_reference)
|
||||||
|
REFERENCES public.portfolio (portfolio_reference)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO platform.company_branch_portfolios (
|
||||||
|
company_id,
|
||||||
|
company_branch_id,
|
||||||
|
portfolio_reference,
|
||||||
|
createdon
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
p.company_id,
|
||||||
|
p.branch_id,
|
||||||
|
p.portfolio_reference,
|
||||||
|
COALESCE(p.createdon, CURRENT_TIMESTAMP)
|
||||||
|
FROM public.portfolio p
|
||||||
|
JOIN public.company_branch cb
|
||||||
|
ON cb.company_id = p.company_id
|
||||||
|
AND cb.branch_id = p.branch_id
|
||||||
|
ON CONFLICT (company_id, company_branch_id, portfolio_reference) DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS platform;
|
||||||
|
|
||||||
|
CREATE TABLE platform.options
|
||||||
|
(LIKE public.options INCLUDING ALL);
|
||||||
|
|
||||||
|
CREATE TABLE platform.options_values
|
||||||
|
(LIKE public.options_values INCLUDING ALL);
|
||||||
|
|
||||||
|
CREATE TABLE platform.options_mapping
|
||||||
|
(LIKE public.options_mapping INCLUDING ALL);
|
||||||
|
|
||||||
|
CREATE TABLE platform.company_options_mapping
|
||||||
|
(LIKE public.comp_options_mapping INCLUDING ALL);
|
||||||
|
|
||||||
|
CREATE TABLE platform.options_history
|
||||||
|
(LIKE public.options_history INCLUDING ALL);
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ADD COLUMN company_id smallint,
|
||||||
|
ADD COLUMN portfolio_reference uuid;
|
||||||
|
|
||||||
|
INSERT INTO platform.options
|
||||||
|
SELECT * FROM public.options;
|
||||||
|
|
||||||
|
INSERT INTO platform.options_values
|
||||||
|
SELECT * FROM public.options_values;
|
||||||
|
|
||||||
|
INSERT INTO platform.options_mapping (
|
||||||
|
portfolio_id, option_id, createdon, createdby, lasteditedon,
|
||||||
|
lasteditedby, opt_value_id, opt_map_id, company_id, portfolio_reference
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
om.portfolio_id, om.option_id, om.createdon, om.createdby, om.lasteditedon,
|
||||||
|
om.lasteditedby, om.opt_value_id, om.opt_map_id, 1,
|
||||||
|
COALESCE(p.portfolio_reference, archived.portfolio_reference)
|
||||||
|
FROM public.options_mapping om
|
||||||
|
LEFT JOIN public.portfolio p ON p.portfolio_id = om.portfolio_id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT portfolio_id, gen_random_uuid() portfolio_reference
|
||||||
|
FROM public.options_mapping
|
||||||
|
GROUP BY portfolio_id
|
||||||
|
) archived ON archived.portfolio_id = om.portfolio_id;
|
||||||
|
|
||||||
|
INSERT INTO platform.company_options_mapping (
|
||||||
|
company_id, option_id, createdon, createdby, lasteditedon,
|
||||||
|
lasteditedby, opt_value_id, opt_map_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
1, option_id, createdon, createdby, lasteditedon,
|
||||||
|
lasteditedby, opt_value_id, opt_map_id
|
||||||
|
FROM public.comp_options_mapping;
|
||||||
|
|
||||||
|
INSERT INTO platform.options_history
|
||||||
|
SELECT * FROM public.options_history;
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ALTER COLUMN company_id SET NOT NULL,
|
||||||
|
ALTER COLUMN portfolio_reference SET NOT NULL;
|
||||||
|
|
||||||
|
DO $duplicates$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM platform.options_mapping
|
||||||
|
GROUP BY option_id, opt_value_id, portfolio_reference, company_id
|
||||||
|
HAVING count(*) > 1
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Duplicate platform.options_mapping rows found for (option_id, opt_value_id, portfolio_reference, company_id)';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM platform.company_options_mapping
|
||||||
|
GROUP BY option_id, opt_value_id, company_id
|
||||||
|
HAVING count(*) > 1
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'Duplicate platform.company_options_mapping rows found for (option_id, opt_value_id, company_id)';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$duplicates$;
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ADD CONSTRAINT uq_platform_options_mapping_scope
|
||||||
|
UNIQUE (option_id, opt_value_id, portfolio_reference, company_id);
|
||||||
|
|
||||||
|
ALTER TABLE platform.company_options_mapping
|
||||||
|
ADD CONSTRAINT uq_platform_company_options_mapping_scope
|
||||||
|
UNIQUE (option_id, opt_value_id, company_id);
|
||||||
|
|
||||||
|
-- CREATE TABLE ... LIKE does not copy foreign keys. Recreate the source FK
|
||||||
|
-- against the copied platform table rather than retaining a public dependency.
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
DROP CONSTRAINT IF EXISTS options_mapping_opt_value_id;
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ADD CONSTRAINT options_mapping_opt_value_id
|
||||||
|
FOREIGN KEY (opt_value_id)
|
||||||
|
REFERENCES platform.options_values (opt_value_id)
|
||||||
|
ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
|
||||||
|
|
||||||
|
-- Replace copied defaults that still point to public sequences.
|
||||||
|
CREATE SEQUENCE platform.options_mapping_opt_map_id_seq;
|
||||||
|
ALTER SEQUENCE platform.options_mapping_opt_map_id_seq
|
||||||
|
OWNED BY platform.options_mapping.opt_map_id;
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ALTER COLUMN opt_map_id
|
||||||
|
SET DEFAULT nextval('platform.options_mapping_opt_map_id_seq'::regclass);
|
||||||
|
SELECT setval(
|
||||||
|
'platform.options_mapping_opt_map_id_seq',
|
||||||
|
COALESCE((SELECT max(opt_map_id) FROM platform.options_mapping), 1),
|
||||||
|
EXISTS (SELECT 1 FROM platform.options_mapping)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
DROP COLUMN portfolio_id;
|
||||||
|
|
||||||
|
CREATE SEQUENCE platform.company_options_mapping_opt_map_id_seq;
|
||||||
|
ALTER SEQUENCE platform.company_options_mapping_opt_map_id_seq
|
||||||
|
OWNED BY platform.company_options_mapping.opt_map_id;
|
||||||
|
ALTER TABLE platform.company_options_mapping
|
||||||
|
ALTER COLUMN opt_map_id
|
||||||
|
SET DEFAULT nextval('platform.company_options_mapping_opt_map_id_seq'::regclass);
|
||||||
|
SELECT setval(
|
||||||
|
'platform.company_options_mapping_opt_map_id_seq',
|
||||||
|
COALESCE((SELECT max(opt_map_id) FROM platform.company_options_mapping), 1),
|
||||||
|
EXISTS (SELECT 1 FROM platform.company_options_mapping)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE SEQUENCE platform.options_history_uid_seq;
|
||||||
|
ALTER SEQUENCE platform.options_history_uid_seq
|
||||||
|
OWNED BY platform.options_history.uid;
|
||||||
|
ALTER TABLE platform.options_history
|
||||||
|
ALTER COLUMN uid
|
||||||
|
SET DEFAULT nextval('platform.options_history_uid_seq'::regclass);
|
||||||
|
SELECT setval(
|
||||||
|
'platform.options_history_uid_seq',
|
||||||
|
COALESCE((SELECT max(uid) FROM platform.options_history), 1),
|
||||||
|
EXISTS (SELECT 1 FROM platform.options_history)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ADD COLUMN IF NOT EXISTS portfolio_reference uuid;
|
||||||
|
|
||||||
|
CREATE TEMPORARY TABLE portfolio_reference_migration (
|
||||||
|
company_id smallint NOT NULL,
|
||||||
|
portfolio_id smallint NOT NULL,
|
||||||
|
portfolio_reference uuid NOT NULL,
|
||||||
|
PRIMARY KEY (company_id, portfolio_id)
|
||||||
|
) ON COMMIT DROP;
|
||||||
|
|
||||||
|
DO $migration$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema='platform' AND table_name='options_mapping'
|
||||||
|
AND column_name='portfolio_id'
|
||||||
|
) THEN
|
||||||
|
EXECUTE $sql$
|
||||||
|
INSERT INTO portfolio_reference_migration
|
||||||
|
(company_id, portfolio_id, portfolio_reference)
|
||||||
|
SELECT om.company_id, om.portfolio_id,
|
||||||
|
COALESCE(
|
||||||
|
(array_agg(p.portfolio_reference)
|
||||||
|
FILTER (WHERE p.portfolio_reference IS NOT NULL))[1],
|
||||||
|
gen_random_uuid())
|
||||||
|
FROM platform.options_mapping om
|
||||||
|
LEFT JOIN public.portfolio p ON p.portfolio_id=om.portfolio_id
|
||||||
|
GROUP BY om.company_id, om.portfolio_id
|
||||||
|
$sql$;
|
||||||
|
EXECUTE $sql$
|
||||||
|
UPDATE platform.options_mapping om
|
||||||
|
SET portfolio_reference=prm.portfolio_reference
|
||||||
|
FROM portfolio_reference_migration prm
|
||||||
|
WHERE prm.company_id=om.company_id
|
||||||
|
AND prm.portfolio_id=om.portfolio_id
|
||||||
|
AND om.portfolio_reference IS NULL
|
||||||
|
$sql$;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
DO $validation$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM platform.options_mapping WHERE portfolio_reference IS NULL) THEN
|
||||||
|
RAISE EXCEPTION 'Some option mappings have no portfolio reference';
|
||||||
|
END IF;
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM platform.options_mapping
|
||||||
|
GROUP BY option_id, opt_value_id, portfolio_reference, company_id
|
||||||
|
HAVING count(*) > 1
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'Duplicate option mappings found for portfolio references';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$validation$;
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ALTER COLUMN portfolio_reference SET NOT NULL,
|
||||||
|
DROP CONSTRAINT IF EXISTS uq_platform_options_mapping_scope;
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
ADD CONSTRAINT uq_platform_options_mapping_scope
|
||||||
|
UNIQUE (option_id, opt_value_id, portfolio_reference, company_id);
|
||||||
|
|
||||||
|
ALTER TABLE platform.options_mapping
|
||||||
|
DROP COLUMN IF EXISTS portfolio_id;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS platform.uq_company_branch_portfolios_local_scope;
|
||||||
|
|
||||||
|
ALTER TABLE platform.company_branch_portfolios
|
||||||
|
DROP COLUMN IF EXISTS portfolio_id;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE platform.application_query
|
||||||
|
SET query_text = 'select!C0L!SELECT portfolio_id AS "value", portname AS "label", portfolio_reference::text AS "description", ''portfolio.'' AS "group" FROM portfolio WHERE branch_id=? AND isactive=? ORDER BY portname',
|
||||||
|
ismigrated = true
|
||||||
|
WHERE query_id = 3;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user