Company options values, portfolio options values migration done

This commit is contained in:
2026-08-12 09:38:18 +05:30
parent c835a9deb0
commit 61bea5fb6e
17 changed files with 616 additions and 22 deletions

View File

@@ -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());
}
} }

View File

@@ -0,0 +1,4 @@
package com.cygnus.client.model;
public record CloudDataItem(Object value, String label, String group) {
}

View File

@@ -0,0 +1,6 @@
package com.cygnus.client.model;
import java.util.Map;
public record CloudDataRequest(String scope, Map<String, Object> data) {
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,7 @@
package com.cygnus.cloud.platform.api;
public record ScopedDataItem(
Object value,
String label,
String group) {
}

View File

@@ -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) {
}

View File

@@ -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"));
}
}

View File

@@ -0,0 +1,7 @@
package com.cygnus.cloud.platform.service;
public class ScopedDataException extends RuntimeException {
public ScopedDataException(String message) {
super(message);
}
}

View File

@@ -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;
}
}

View File

@@ -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())

View File

@@ -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;
}
} }

View File

@@ -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,

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;