Case Punching Feature Done
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -68,6 +68,8 @@
|
||||
"CYGNUS_CLIENT_ASSERTION": "file:${workspaceFolder}/config/clients/matrix/matrix-matrix-delhi-cygnus-01-assertion.jwt",
|
||||
"CYGNUS_LOGIN_KEY_ID": "cygnus-login-2026-01",
|
||||
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/login-public.pem",
|
||||
"CYGNUS_PAYLOAD_PRIVATE_KEY": "file:${workspaceFolder}/config/keys/case-save-private.pem",
|
||||
"CYGNUS_PAYLOAD_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/case-save-public.pem",
|
||||
"CYGNUS_CLOUD_REQUEST_TIMEOUT": "PT10S",
|
||||
"CYGNUS_TOKEN_REFRESH_SKEW": "PT30S"
|
||||
},
|
||||
|
||||
@@ -62,4 +62,15 @@ public class CloudIdentityClient {
|
||||
.bodyToMono(CloudQueryResponse.class))
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
|
||||
public Mono<CloudQueryResponse> fetchQuery(String queryKey) {
|
||||
return tokenProvider.accessToken()
|
||||
.flatMap(token -> webClient.get()
|
||||
.uri(properties.baseUri().resolve("/api/v1/queries/key/" + queryKey))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(CloudQueryResponse.class))
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,10 @@ public class CloudQueryController {
|
||||
return repository.findEnabled(queryId)
|
||||
.switchIfEmpty(Mono.error(new QueryNotFoundException(queryId)));
|
||||
}
|
||||
|
||||
@GetMapping("/key/{queryKey}")
|
||||
public Mono<CloudQuery> queryByKey(@PathVariable String queryKey) {
|
||||
return repository.findEnabled(queryKey)
|
||||
.switchIfEmpty(Mono.error(new QueryNotFoundException(queryKey)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ public class QueryCatalogRepository {
|
||||
|
||||
private static final String INITIALIZE = """
|
||||
CREATE SCHEMA IF NOT EXISTS platform;
|
||||
CREATE SEQUENCE IF NOT EXISTS platform.application_query_id_seq;
|
||||
CREATE TABLE IF NOT EXISTS platform.application_query (
|
||||
query_id integer PRIMARY KEY,
|
||||
query_id integer PRIMARY KEY DEFAULT nextval('platform.application_query_id_seq'),
|
||||
query_key varchar(100),
|
||||
query_text text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
@@ -20,7 +22,15 @@ public class QueryCatalogRepository {
|
||||
CHECK (query_id > 0),
|
||||
CONSTRAINT ck_platform_application_query_text
|
||||
CHECK (length(btrim(query_text)) > 0)
|
||||
)
|
||||
);
|
||||
ALTER TABLE platform.application_query
|
||||
ADD COLUMN IF NOT EXISTS query_key varchar(100);
|
||||
ALTER TABLE platform.application_query ALTER COLUMN query_id
|
||||
SET DEFAULT nextval('platform.application_query_id_seq');
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_platform_application_query_key
|
||||
ON platform.application_query (query_key) WHERE query_key IS NOT NULL;
|
||||
SELECT setval('platform.application_query_id_seq',
|
||||
greatest(coalesce((SELECT max(query_id) FROM platform.application_query), 0) + 1, 1), false)
|
||||
""";
|
||||
private static final String FIND = """
|
||||
SELECT query_id, query_text
|
||||
@@ -28,6 +38,12 @@ public class QueryCatalogRepository {
|
||||
WHERE query_id = $1
|
||||
AND enabled = true
|
||||
""";
|
||||
private static final String FIND_BY_KEY = """
|
||||
SELECT query_id, query_text
|
||||
FROM platform.application_query
|
||||
WHERE query_key = $1
|
||||
AND enabled = true
|
||||
""";
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public QueryCatalogRepository(ReactiveDatabaseClient database) {
|
||||
@@ -45,4 +61,11 @@ public class QueryCatalogRepository {
|
||||
.map(row -> new CloudQuery(
|
||||
row.getInteger("query_id"), row.getString("query_text")));
|
||||
}
|
||||
|
||||
public Mono<CloudQuery> findEnabled(String queryKey) {
|
||||
return database.preparedQuery(FIND_BY_KEY, Tuple.of(queryKey))
|
||||
.flatMapMany(rows -> reactor.core.publisher.Flux.fromIterable(rows))
|
||||
.next()
|
||||
.map(row -> new CloudQuery(row.getInteger("query_id"), row.getString("query_text")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,8 @@ public class QueryNotFoundException extends RuntimeException {
|
||||
public QueryNotFoundException(int queryId) {
|
||||
super("Query was not found: " + queryId);
|
||||
}
|
||||
|
||||
public QueryNotFoundException(String queryKey) {
|
||||
super("Query was not found: " + queryKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
CREATE SCHEMA IF NOT EXISTS platform;
|
||||
CREATE SEQUENCE IF NOT EXISTS platform.application_query_id_seq;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform.application_query (
|
||||
query_id integer PRIMARY KEY,
|
||||
query_id integer PRIMARY KEY DEFAULT nextval('platform.application_query_id_seq'),
|
||||
query_key varchar(100),
|
||||
query_text text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
@@ -11,3 +13,11 @@ CREATE TABLE IF NOT EXISTS platform.application_query (
|
||||
CONSTRAINT ck_platform_application_query_text
|
||||
CHECK (length(btrim(query_text)) > 0)
|
||||
);
|
||||
ALTER TABLE platform.application_query
|
||||
ADD COLUMN IF NOT EXISTS query_key varchar(100);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_platform_application_query_key
|
||||
ON platform.application_query (query_key) WHERE query_key IS NOT NULL;
|
||||
ALTER TABLE platform.application_query ALTER COLUMN query_id
|
||||
SET DEFAULT nextval('platform.application_query_id_seq');
|
||||
SELECT setval('platform.application_query_id_seq',
|
||||
greatest(coalesce((SELECT max(query_id) FROM platform.application_query), 0) + 1, 1), false);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Stable query key; query_id is generated by PostgreSQL.
|
||||
BEGIN;
|
||||
|
||||
INSERT INTO platform.application_query(query_key, query_text, enabled, created_at, updated_at, ismigrated)
|
||||
VALUES ('APPLICATION_SAVE',
|
||||
'procedure!C0L!select * from public.save_application_details(?::jsonb,?::smallint,?::smallint,?::smallint)',
|
||||
true, clock_timestamp(), clock_timestamp(), true)
|
||||
ON CONFLICT (query_key) WHERE query_key IS NOT NULL DO UPDATE SET
|
||||
query_text=excluded.query_text, enabled=true, updated_at=clock_timestamp(), ismigrated=true;
|
||||
|
||||
COMMIT;
|
||||
@@ -1,9 +1,7 @@
|
||||
package lib.constants;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Canonical user-facing errors shared by Cygnus applications. */
|
||||
public enum ApplicationError {
|
||||
/** Canonical user-facing messages shared by Cygnus applications. */
|
||||
public enum ApplicationMessage {
|
||||
BAD_REQUEST(400, "HTTP-400", "Invalid request",
|
||||
"Cygnus could not process the submitted request.",
|
||||
"Review the supplied information and try again."),
|
||||
@@ -28,6 +26,23 @@ public enum ApplicationError {
|
||||
TOO_MANY_REQUESTS(429, "HTTP-429", "Too many requests",
|
||||
"Cygnus has received too many requests in a short period.",
|
||||
"Wait briefly before trying again."),
|
||||
PAYLOAD_INVALID(400, "REQ-4001", "Invalid request details",
|
||||
"The encrypted request could not be verified.",
|
||||
"Refresh the page and submit the form again."),
|
||||
APPLICATION_VALIDATION_FAILED(422, "APP-4221", "Application validation failed",
|
||||
"One or more application fields contain invalid information.",
|
||||
"Correct the highlighted fields and submit the application again."),
|
||||
APPLICATION_ACCESS_DENIED(403, "APP-4031", "Application access denied",
|
||||
"The selected portfolio or application is not available to your account.",
|
||||
"Select an authorized portfolio or contact your administrator."),
|
||||
APPLICATION_SAVE_CONFLICT(409, "APP-4091", "Application save conflict",
|
||||
"This request was already processed or the application changed.",
|
||||
"Refresh the application before submitting it again."),
|
||||
APPLICATION_SAVED(200, "APP-2001", "Application saved",
|
||||
"The application details were saved successfully.", ""),
|
||||
APPLICATION_SAVE_FAILED(500, "APP-5001", "Application could not be saved",
|
||||
"Cygnus could not save the application details.",
|
||||
"Try again. If the issue continues, share the reference ID with support."),
|
||||
INTERNAL_SERVER_ERROR(500, "HTTP-500", "Something went wrong",
|
||||
"Cygnus could not complete your request.",
|
||||
"Try again. If the issue continues, share the reference ID with support."),
|
||||
@@ -47,7 +62,7 @@ public enum ApplicationError {
|
||||
private final String message;
|
||||
private final String description;
|
||||
|
||||
ApplicationError(
|
||||
ApplicationMessage(
|
||||
int httpStatus, String code, String title, String message, String description) {
|
||||
this.httpStatus = httpStatus;
|
||||
this.code = code;
|
||||
@@ -56,11 +71,21 @@ public enum ApplicationError {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public static ApplicationError fromHttpStatus(int status) {
|
||||
return Arrays.stream(values())
|
||||
.filter(error -> error.httpStatus == status)
|
||||
.findFirst()
|
||||
.orElse(INTERNAL_SERVER_ERROR);
|
||||
public static ApplicationMessage fromHttpStatus(int status) {
|
||||
return switch (status) {
|
||||
case 400 -> BAD_REQUEST;
|
||||
case 401 -> SESSION_REQUIRED;
|
||||
case 403 -> ACCESS_DENIED;
|
||||
case 404 -> PAGE_NOT_FOUND;
|
||||
case 405 -> METHOD_NOT_ALLOWED;
|
||||
case 409 -> CONFLICT;
|
||||
case 413 -> PAYLOAD_TOO_LARGE;
|
||||
case 429 -> TOO_MANY_REQUESTS;
|
||||
case 502 -> BAD_GATEWAY;
|
||||
case 503 -> SERVICE_UNAVAILABLE;
|
||||
case 504 -> GATEWAY_TIMEOUT;
|
||||
default -> INTERNAL_SERVER_ERROR;
|
||||
};
|
||||
}
|
||||
|
||||
public int getHttpStatus() { return httpStatus; }
|
||||
@@ -0,0 +1,20 @@
|
||||
package lib.exceptions;
|
||||
|
||||
import lib.constants.ApplicationMessage;
|
||||
|
||||
/** Single typed application exception carrying a safe user-facing message. */
|
||||
public class ApplicationException extends RuntimeException {
|
||||
private final ApplicationMessage applicationMessage;
|
||||
|
||||
public ApplicationException(ApplicationMessage message, String technicalMessage) {
|
||||
super(technicalMessage);
|
||||
this.applicationMessage = message;
|
||||
}
|
||||
|
||||
public ApplicationException(ApplicationMessage message, String technicalMessage, Throwable cause) {
|
||||
super(technicalMessage, cause);
|
||||
this.applicationMessage = message;
|
||||
}
|
||||
|
||||
public ApplicationMessage getApplicationMessage() { return applicationMessage; }
|
||||
}
|
||||
12
cygnus-lib/src/main/java/lib/models/ApiResponse.java
Normal file
12
cygnus-lib/src/main/java/lib/models/ApiResponse.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package lib.models;
|
||||
|
||||
/** Consistent API response contract for every migrated workflow. */
|
||||
public record ApiResponse<T>(boolean success, T data, MessageDetails message) {
|
||||
public static <T> ApiResponse<T> success(T data, MessageDetails message) {
|
||||
return new ApiResponse<>(true, data, message);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> failure(MessageDetails message) {
|
||||
return new ApiResponse<>(false, null, message);
|
||||
}
|
||||
}
|
||||
87
cygnus-lib/src/main/java/lib/models/ApplicationDetails.java
Normal file
87
cygnus-lib/src/main/java/lib/models/ApplicationDetails.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package lib.models;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/** User-editable case initiation data. Identity and audit fields come from UserSession. */
|
||||
@Getter
|
||||
@Setter
|
||||
public class ApplicationDetails implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer applicationId;
|
||||
private Short portfolioId;
|
||||
private Short bankBranchId;
|
||||
private String applicationNumber;
|
||||
private String bankCode;
|
||||
private String product;
|
||||
private String loanAmount;
|
||||
private String customerName;
|
||||
private String fatherName;
|
||||
private String applicationType;
|
||||
private String category;
|
||||
private String dateOfBirth;
|
||||
private String contactPerson;
|
||||
private String mobileNumber;
|
||||
private String specialInstruction;
|
||||
|
||||
private Boolean residenceVerification;
|
||||
private Boolean residenceTelephoneVerification;
|
||||
private Boolean officeVerification;
|
||||
private Boolean officeTelephoneVerification;
|
||||
private Boolean propertyVerification;
|
||||
private Boolean referenceVerification;
|
||||
private Boolean documentVerification;
|
||||
private Boolean residenceCoApplicant;
|
||||
private Boolean residenceCoProprietor;
|
||||
private Boolean officeCoProprietor;
|
||||
private Boolean sameResidenceAddress;
|
||||
private Boolean sameOfficeAddress;
|
||||
private Boolean samePropertyAddress;
|
||||
|
||||
private String residenceAddress1;
|
||||
private String residenceAddress2;
|
||||
private String residenceAddress3;
|
||||
private String residenceLandmark;
|
||||
private Integer residenceColonyId;
|
||||
private String residenceCity;
|
||||
private String residencePincode;
|
||||
private String residencePhone;
|
||||
|
||||
private String companyName;
|
||||
private String officeAddress1;
|
||||
private String officeAddress2;
|
||||
private String officeAddress3;
|
||||
private String officeLandmark;
|
||||
private Integer officeColonyId;
|
||||
private String officeCity;
|
||||
private String officePincode;
|
||||
private String department;
|
||||
private String designation;
|
||||
private String officePhone;
|
||||
private String extension;
|
||||
|
||||
private String propertyAddress1;
|
||||
private String propertyAddress2;
|
||||
private String propertyAddress3;
|
||||
private String propertyLandmark;
|
||||
private Integer propertyColonyId;
|
||||
private String propertyCity;
|
||||
private String propertyPincode;
|
||||
|
||||
private String referenceName1;
|
||||
private String referenceAddress1;
|
||||
private String referenceContactNumber1;
|
||||
private String referenceName2;
|
||||
private String referenceAddress2;
|
||||
private String referenceContactNumber2;
|
||||
|
||||
private Map<String, String> dynamicFields = new LinkedHashMap<>();
|
||||
private Boolean autoCutOff;
|
||||
private Integer formMode;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package lib.models;
|
||||
|
||||
/** Data returned after saving a loan-verification application. */
|
||||
public record ApplicationSaveData(String operation, Integer applicationId, String mvCode) {}
|
||||
@@ -0,0 +1,16 @@
|
||||
package lib.models;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Result row returned by the application-save database operation. */
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class ApplicationSaveResult {
|
||||
private String operation;
|
||||
private Integer applicationId;
|
||||
private String mvCode;
|
||||
private String internalUuid;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import lombok.Setter;
|
||||
@Getter
|
||||
@Setter
|
||||
public class CasePunching {
|
||||
private ErrorDetails errorDetails;
|
||||
private MessageDetails messageDetails;
|
||||
private Map<String, String> optionsl;
|
||||
private Map<String, List<Option>> options;
|
||||
private List<String> visibleSections;
|
||||
|
||||
11
cygnus-lib/src/main/java/lib/models/CaseSaveRequest.java
Normal file
11
cygnus-lib/src/main/java/lib/models/CaseSaveRequest.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package lib.models;
|
||||
|
||||
/** Hybrid RSA-OAEP/AES-GCM envelope submitted by the initiation page. */
|
||||
public record CaseSaveRequest(
|
||||
String keyId,
|
||||
String encryptedKey,
|
||||
String initializationVector,
|
||||
String encryptedPayload,
|
||||
String requestId,
|
||||
String timestamp) {
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package lib.models;
|
||||
|
||||
import lib.constants.ApplicationError;
|
||||
import lib.constants.ApplicationMessage;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
@@ -10,37 +10,37 @@ import lombok.Getter;
|
||||
|
||||
/** Dependency-free error information shared across Cygnus modules. */
|
||||
@Getter
|
||||
public final class ErrorDetails implements Serializable {
|
||||
public final class MessageDetails implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final int httpStatus;
|
||||
private final String errorCode;
|
||||
private final String code;
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final String description;
|
||||
private final String referenceId;
|
||||
private final Instant timestamp;
|
||||
|
||||
public ErrorDetails(ApplicationError error) {
|
||||
this(error.getHttpStatus(), error.getCode(), error.getTitle(),
|
||||
error.getMessage(), error.getDescription(),
|
||||
public MessageDetails(ApplicationMessage message) {
|
||||
this(message.getHttpStatus(), message.getCode(), message.getTitle(),
|
||||
message.getMessage(), message.getDescription(),
|
||||
UUID.randomUUID().toString(), Instant.now());
|
||||
}
|
||||
|
||||
private ErrorDetails(
|
||||
private MessageDetails(
|
||||
int httpStatus,
|
||||
String errorCode,
|
||||
String code,
|
||||
String title,
|
||||
String message,
|
||||
String description,
|
||||
String referenceId,
|
||||
Instant timestamp) {
|
||||
if (httpStatus < 400 || httpStatus > 599) {
|
||||
throw new IllegalArgumentException("HTTP error status must be between 400 and 599");
|
||||
if (httpStatus < 100 || httpStatus > 599) {
|
||||
throw new IllegalArgumentException("HTTP status must be between 100 and 599");
|
||||
}
|
||||
this.httpStatus = httpStatus;
|
||||
this.errorCode = required(errorCode, "errorCode");
|
||||
this.code = required(code, "code");
|
||||
this.title = required(title, "title");
|
||||
this.message = required(message, "message");
|
||||
this.description = description == null ? "" : description.trim();
|
||||
@@ -1,22 +0,0 @@
|
||||
package lib.models;
|
||||
|
||||
import lib.constants.ApplicationError;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ErrorDetailsTest {
|
||||
@Test
|
||||
void createsAReusableErrorContract() {
|
||||
ErrorDetails details = new ErrorDetails(ApplicationError.ACCESS_DENIED);
|
||||
assertEquals(403, details.getHttpStatus());
|
||||
assertEquals("AUTH-403", details.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsHttpStatusToCanonicalError() {
|
||||
assertEquals(ApplicationError.PAGE_NOT_FOUND, ApplicationError.fromHttpStatus(404));
|
||||
assertEquals(ApplicationError.INTERNAL_SERVER_ERROR,
|
||||
ApplicationError.fromHttpStatus(599));
|
||||
}
|
||||
}
|
||||
22
cygnus-lib/src/test/java/lib/models/MessageDetailsTest.java
Normal file
22
cygnus-lib/src/test/java/lib/models/MessageDetailsTest.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package lib.models;
|
||||
|
||||
import lib.constants.ApplicationMessage;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class MessageDetailsTest {
|
||||
@Test
|
||||
void createsAReusableErrorContract() {
|
||||
MessageDetails details = new MessageDetails(ApplicationMessage.ACCESS_DENIED);
|
||||
assertEquals(403, details.getHttpStatus());
|
||||
assertEquals("AUTH-403", details.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsHttpStatusToCanonicalError() {
|
||||
assertEquals(ApplicationMessage.PAGE_NOT_FOUND, ApplicationMessage.fromHttpStatus(404));
|
||||
assertEquals(ApplicationMessage.INTERNAL_SERVER_ERROR,
|
||||
ApplicationMessage.fromHttpStatus(599));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
BIN
cygnus-lib/target/classes/lib/constants/ApplicationMessage.class
Normal file
BIN
cygnus-lib/target/classes/lib/constants/ApplicationMessage.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
cygnus-lib/target/classes/lib/models/ApiResponse.class
Normal file
BIN
cygnus-lib/target/classes/lib/models/ApiResponse.class
Normal file
Binary file not shown.
BIN
cygnus-lib/target/classes/lib/models/ApplicationDetails.class
Normal file
BIN
cygnus-lib/target/classes/lib/models/ApplicationDetails.class
Normal file
Binary file not shown.
BIN
cygnus-lib/target/classes/lib/models/ApplicationSaveData.class
Normal file
BIN
cygnus-lib/target/classes/lib/models/ApplicationSaveData.class
Normal file
Binary file not shown.
BIN
cygnus-lib/target/classes/lib/models/ApplicationSaveResult.class
Normal file
BIN
cygnus-lib/target/classes/lib/models/ApplicationSaveResult.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
cygnus-lib/target/classes/lib/models/CaseSaveRequest.class
Normal file
BIN
cygnus-lib/target/classes/lib/models/CaseSaveRequest.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
cygnus-lib/target/classes/lib/models/MessageDetails.class
Normal file
BIN
cygnus-lib/target/classes/lib/models/MessageDetails.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,12 @@
|
||||
lib/models/ErrorDetails.class
|
||||
lib/models/MessageDetails.class
|
||||
lib/models/CasePunching.class
|
||||
lib/constants/ApplicationError.class
|
||||
lib/exceptions/ApplicationException.class
|
||||
lib/models/Option.class
|
||||
lib/models/UserSession.class
|
||||
lib/models/ApiResponse.class
|
||||
lib/models/ApplicationSaveResult.class
|
||||
lib/constants/ApplicationMessage.class
|
||||
lib/models/ApplicationSaveData.class
|
||||
lib/models/UserSession$UserSessionBuilder.class
|
||||
lib/models/CaseSaveRequest.class
|
||||
lib/models/ApplicationDetails.class
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/constants/ApplicationError.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/constants/ApplicationMessage.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/exceptions/ApplicationException.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/ApiResponse.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/ApplicationDetails.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/ApplicationSaveData.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/ApplicationSaveResult.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/CasePunching.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/ErrorDetails.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/CaseSaveRequest.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/MessageDetails.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/Option.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/main/java/lib/models/UserSession.java
|
||||
|
||||
@@ -1 +1 @@
|
||||
lib/models/ErrorDetailsTest.class
|
||||
lib/models/MessageDetailsTest.class
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/test/java/lib/models/ErrorDetailsTest.java
|
||||
/Users/maddy/Projects/cygnus/matrix/cygnus-lib/src/test/java/lib/models/MessageDetailsTest.java
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testsuite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd" version="3.0.2" name="lib.models.ErrorDetailsTest" time="0.028" tests="2" errors="0" skipped="0" failures="0">
|
||||
<properties>
|
||||
<property name="java.specification.version" value="21"/>
|
||||
<property name="sun.jnu.encoding" value="UTF-8"/>
|
||||
<property name="java.class.path" value="/Users/maddy/Projects/cygnus/matrix/cygnus-lib/target/test-classes:/Users/maddy/Projects/cygnus/matrix/cygnus-lib/target/classes:/Users/maddy/.m2/repository/org/projectlombok/lombok/1.18.46/lombok-1.18.46.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter/5.12.2/junit-jupiter-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.12.2/junit-jupiter-api-5.12.2.jar:/Users/maddy/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-commons/1.12.2/junit-platform-commons-1.12.2.jar:/Users/maddy/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.12.2/junit-jupiter-params-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.12.2/junit-jupiter-engine-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-engine/1.12.2/junit-platform-engine-1.12.2.jar:"/>
|
||||
<property name="java.vm.vendor" value="Microsoft"/>
|
||||
<property name="sun.arch.data.model" value="64"/>
|
||||
<property name="java.vendor.url" value="https://www.microsoft.com"/>
|
||||
<property name="os.name" value="Mac OS X"/>
|
||||
<property name="java.vm.specification.version" value="21"/>
|
||||
<property name="sun.java.launcher" value="SUN_STANDARD"/>
|
||||
<property name="user.country" value="US"/>
|
||||
<property name="sun.boot.library.path" value="/Users/maddy/Library/Java/JavaVirtualMachines/ms-21.0.8/Contents/Home/lib"/>
|
||||
<property name="sun.java.command" value="/Users/maddy/Projects/cygnus/matrix/cygnus-lib/target/surefire/surefirebooter-20260801225315947_3.jar /Users/maddy/Projects/cygnus/matrix/cygnus-lib/target/surefire 2026-08-01T22-53-15_920-jvmRun1 surefire-20260801225315947_1tmp surefire_0-20260801225315947_2tmp"/>
|
||||
<property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
|
||||
<property name="jdk.debug" value="release"/>
|
||||
<property name="surefire.test.class.path" value="/Users/maddy/Projects/cygnus/matrix/cygnus-lib/target/test-classes:/Users/maddy/Projects/cygnus/matrix/cygnus-lib/target/classes:/Users/maddy/.m2/repository/org/projectlombok/lombok/1.18.46/lombok-1.18.46.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter/5.12.2/junit-jupiter-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.12.2/junit-jupiter-api-5.12.2.jar:/Users/maddy/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-commons/1.12.2/junit-platform-commons-1.12.2.jar:/Users/maddy/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.12.2/junit-jupiter-params-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.12.2/junit-jupiter-engine-5.12.2.jar:/Users/maddy/.m2/repository/org/junit/platform/junit-platform-engine/1.12.2/junit-platform-engine-1.12.2.jar:"/>
|
||||
<property name="sun.cpu.endian" value="little"/>
|
||||
<property name="user.home" value="/Users/maddy"/>
|
||||
<property name="user.language" value="en"/>
|
||||
<property name="java.specification.vendor" value="Oracle Corporation"/>
|
||||
<property name="java.version.date" value="2025-07-15"/>
|
||||
<property name="java.home" value="/Users/maddy/Library/Java/JavaVirtualMachines/ms-21.0.8/Contents/Home"/>
|
||||
<property name="file.separator" value="/"/>
|
||||
<property name="basedir" value="/Users/maddy/Projects/cygnus/matrix/cygnus-lib"/>
|
||||
<property name="java.vm.compressedOopsMode" value="Zero based"/>
|
||||
<property name="line.separator" value=" "/>
|
||||
<property name="java.vm.specification.vendor" value="Oracle Corporation"/>
|
||||
<property name="java.specification.name" value="Java Platform API Specification"/>
|
||||
<property name="apple.awt.application.name" value="ForkedBooter"/>
|
||||
<property name="surefire.real.class.path" value="/Users/maddy/Projects/cygnus/matrix/cygnus-lib/target/surefire/surefirebooter-20260801225315947_3.jar"/>
|
||||
<property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
|
||||
<property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
|
||||
<property name="java.runtime.version" value="21.0.8+9-LTS"/>
|
||||
<property name="user.name" value="maddy"/>
|
||||
<property name="stdout.encoding" value="UTF-8"/>
|
||||
<property name="path.separator" value=":"/>
|
||||
<property name="os.version" value="26.5.2"/>
|
||||
<property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
|
||||
<property name="file.encoding" value="UTF-8"/>
|
||||
<property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
|
||||
<property name="java.vendor.version" value="Microsoft-11933201"/>
|
||||
<property name="localRepository" value="/Users/maddy/.m2/repository"/>
|
||||
<property name="java.vendor.url.bug" value="https://github.com/microsoft/openjdk/issues"/>
|
||||
<property name="java.io.tmpdir" value="/var/folders/1l/36214rdn79755j30lcnmgsqh0000gn/T/"/>
|
||||
<property name="java.version" value="21.0.8"/>
|
||||
<property name="user.dir" value="/Users/maddy/Projects/cygnus/matrix/cygnus-lib"/>
|
||||
<property name="os.arch" value="aarch64"/>
|
||||
<property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
|
||||
<property name="native.encoding" value="UTF-8"/>
|
||||
<property name="java.library.path" value="/Users/maddy/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
|
||||
<property name="java.vm.info" value="mixed mode, sharing"/>
|
||||
<property name="stderr.encoding" value="UTF-8"/>
|
||||
<property name="java.vendor" value="Microsoft"/>
|
||||
<property name="java.vm.version" value="21.0.8+9-LTS"/>
|
||||
<property name="sun.io.unicode.encoding" value="UnicodeBig"/>
|
||||
<property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
|
||||
<property name="java.class.version" value="65.0"/>
|
||||
</properties>
|
||||
<testcase name="mapsHttpStatusToCanonicalError" classname="lib.models.ErrorDetailsTest" time="0.008"/>
|
||||
<testcase name="createsAReusableErrorContract" classname="lib.models.ErrorDetailsTest" time="0.012"/>
|
||||
</testsuite>
|
||||
@@ -1,4 +0,0 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: lib.models.ErrorDetailsTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.028 s -- in lib.models.ErrorDetailsTest
|
||||
Binary file not shown.
Binary file not shown.
@@ -17,10 +17,12 @@
|
||||
<script language="javascript" src="/matrix/js/lib/constants.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/jquery1.7.2.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/ui/uifuncs.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/lib/jsfuncs.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/ui/msgdialog.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/lib/jsfuncs.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/lib/payload-crypto.js?ver=1" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/ui/msgdialog.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/ui/select.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/appjs/punching.js?ver=6" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/appjs/punching.js?ver=6" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/edp/initiation.js?ver=1" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/validator.js?ver=2" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/ajax.js" type="text/javascript"></script>
|
||||
<script language="javascript" src="/matrix/js/ajax-dynamic-list.js?ver=1" type="text/javascript"></script>
|
||||
@@ -75,7 +77,7 @@
|
||||
</c:if>
|
||||
<c:if test="${pageFormMode!=2}">
|
||||
<div class="divselect matrix-form-title__end" id="divportfolio" style="width:297px;">
|
||||
<select id="portfolio_id" name="portfolioId" style="width:315px" onchange="SubmitForm('caseadd','_parent','casedetails')">
|
||||
<select id="portfolio_id" name="portfolioId" style="width:315px" onchange="SubmitForm('caseadd','_parent','casedetails')">
|
||||
<option value="-1" selected >SELECT PORTFOLIO</option>
|
||||
<c:choose>
|
||||
<c:when test="${not empty model}">
|
||||
@@ -701,7 +703,7 @@
|
||||
<input type="button" class="button" name="btnprev" style="margin-top:-2px;float:left;width:80px" value="" id="btnprev" accesskey="P" onclick="findRecord(2)" />
|
||||
<input type="button" class="button" name="btnnext" style="margin-top:-2px;float:left;width:80px" value="" id="btnnext" accesskey="N" onclick="findRecord(3)" />
|
||||
<input type="button" class="button" name="btnlast" style="margin-top:-2px;float:left;width:80px" value="" id="btnlast" accesskey="L" onclick="findRecord(4)" />
|
||||
<input type="button" class="button" name="btnsave" style="float:right" value="Save" id="btnsave" accesskey="S" onclick="return ValidateSubmitForm('casedetails');" />
|
||||
<input type="button" class="button" name="btnsave" style="float:right" value="Save" id="btnsave" accesskey="S" onclick="return CygnusInitiation.save(event);" />
|
||||
<c:if test="${pageFormMode!=2}">
|
||||
<input type="button" class="button" name="btnadd" style="float:right" value="Add" id="btnadd" accesskey="A" onclick="return AddNewRecord();" />
|
||||
</c:if>
|
||||
@@ -710,9 +712,9 @@
|
||||
</form>
|
||||
<input type="hidden" id="formmode" name="formmode" value="${pageFormMode}" />
|
||||
<input type="hidden" id="usid" name="usid" value="${pageUserId}" />
|
||||
<c:if test="${not empty model.errorDetails}">
|
||||
<span id="page-error-code" hidden><c:out value="${model.errorDetails.errorCode}" /></span>
|
||||
<span id="page-error-message" hidden><c:out value="${model.errorDetails.message}" /></span>
|
||||
<c:if test="${not empty model.messageDetails}">
|
||||
<span id="page-error-code" hidden><c:out value="${model.messageDetails.code}" /></span>
|
||||
<span id="page-error-message" hidden><c:out value="${model.messageDetails.message}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
</div>
|
||||
@@ -728,10 +730,10 @@
|
||||
}
|
||||
InitPage();
|
||||
|
||||
var errorCode = document.getElementById("page-error-code");
|
||||
var code = document.getElementById("page-error-code");
|
||||
var errorMessage = document.getElementById("page-error-message");
|
||||
if (errorCode && errorMessage) {
|
||||
CallMessage(errorCode.textContent + ":error:" + errorMessage.textContent, 3000, 200, 300);
|
||||
if (code && errorMessage) {
|
||||
CallMessage(code.textContent + ":error:" + errorMessage.textContent, 3000, 200, 300);
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<link href="<c:url value='/css/matrix-shell-v2.css' />" rel="stylesheet" />
|
||||
<link href="<c:url value='/css/matrix-theme-v2.css' />" rel="stylesheet" />
|
||||
<script src="<c:url value='/js/matrix-shell-v2.js' />" defer></script>
|
||||
<title>Cygnus 1.0 | <c:out value="${empty errorDetails.title ? 'Page unavailable' : errorDetails.title}" /></title>
|
||||
<title>Cygnus 1.0 | <c:out value="${empty messageDetails.title ? 'Page unavailable' : messageDetails.title}" /></title>
|
||||
</head>
|
||||
<body class="matrix-v2 matrix-shell matrix-error-page matrix-error-page--${empty errorDetails.httpStatus ? 404 : errorDetails.httpStatus}">
|
||||
<body class="matrix-v2 matrix-shell matrix-error-page matrix-error-page--${empty messageDetails.httpStatus ? 404 : messageDetails.httpStatus}">
|
||||
<c:choose>
|
||||
<c:when test="${not empty Sessvals and not empty Sessvals.menuHtml}">
|
||||
<%@ include file="/WEB-INF/app/fragments/app-shell-header.jspf" %>
|
||||
@@ -25,20 +25,20 @@
|
||||
<main class="matrix-error-layout" id="mainContent">
|
||||
<section class="matrix-error-card" role="alert" aria-labelledby="errorTitle">
|
||||
<div class="matrix-error-card__status" aria-hidden="true">
|
||||
<span><c:out value="${empty errorDetails.httpStatus ? 404 : errorDetails.httpStatus}" /></span>
|
||||
<span><c:out value="${empty messageDetails.httpStatus ? 404 : messageDetails.httpStatus}" /></span>
|
||||
</div>
|
||||
<div class="matrix-error-card__content">
|
||||
<span class="matrix-error-card__eyebrow">
|
||||
Error <c:out value="${empty errorDetails.errorCode ? 'HTTP-404' : errorDetails.errorCode}" />
|
||||
Error <c:out value="${empty messageDetails.code ? 'HTTP-404' : messageDetails.code}" />
|
||||
</span>
|
||||
<h1 id="errorTitle">
|
||||
<c:out value="${empty errorDetails.title ? 'Page unavailable' : errorDetails.title}" />
|
||||
<c:out value="${empty messageDetails.title ? 'Page unavailable' : messageDetails.title}" />
|
||||
</h1>
|
||||
<p class="matrix-error-card__message">
|
||||
<c:out value="${empty errorDetails.message ? 'The requested page is unavailable.' : errorDetails.message}" />
|
||||
<c:out value="${empty messageDetails.message ? 'The requested page is unavailable.' : messageDetails.message}" />
|
||||
</p>
|
||||
<c:if test="${not empty errorDetails.description}">
|
||||
<p class="matrix-error-card__description"><c:out value="${errorDetails.description}" /></p>
|
||||
<c:if test="${not empty messageDetails.description}">
|
||||
<p class="matrix-error-card__description"><c:out value="${messageDetails.description}" /></p>
|
||||
</c:if>
|
||||
|
||||
<div class="matrix-error-card__actions">
|
||||
@@ -55,9 +55,9 @@
|
||||
</c:choose>
|
||||
</div>
|
||||
|
||||
<c:if test="${not empty errorDetails.referenceId}">
|
||||
<c:if test="${not empty messageDetails.referenceId}">
|
||||
<p class="matrix-error-card__reference">
|
||||
Reference ID: <code><c:out value="${errorDetails.referenceId}" /></code>
|
||||
Reference ID: <code><c:out value="${messageDetails.referenceId}" /></code>
|
||||
</p>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
.matrix-add-cases .matrix-form-controls textarea {
|
||||
width: 100% !important;
|
||||
min-height: 30px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.matrix-add-cases .matrix-form-controls .widget:has(#splist),
|
||||
|
||||
@@ -27,7 +27,7 @@ function GetRowData(SelectedRow)
|
||||
$("#"+SelectedRow.id+"col").html("<img src='/matrix/images/edited.png' title='Details has been edited successfully. Please click refresh button to see the changes' />");
|
||||
}*/
|
||||
$("#uuid").val($("#"+SelectedRow.id+"uuid").val());
|
||||
SubmitForm("caseedit", "_blank", "caseGrid");
|
||||
SubmitForm("caseedit", "_blank", "caseGrid");
|
||||
}
|
||||
function keyListen(param1,param2,e) {
|
||||
var keycode = e.keyCode;
|
||||
|
||||
@@ -19,7 +19,7 @@ function GetMoreDetails(SelectedRow)
|
||||
$("#"+SelectedRow.id+"col").html("<img src='/matrix/images/edited.png' title='Details has been edited successfully. Please click refresh button to see the changes' />");
|
||||
}*/
|
||||
$("#uuid").val($("#"+SelectedRow.id+"uuid").val());
|
||||
SubmitForm("caseedit", "_blank", "caseGrid");
|
||||
SubmitForm("caseedit", "_blank", "caseGrid");
|
||||
}
|
||||
function ValidateEmailSearch()
|
||||
{
|
||||
|
||||
170
cygnus-onprem-app/build/WebContent/js/edp/initiation.js
Normal file
170
cygnus-onprem-app/build/WebContent/js/edp/initiation.js
Normal file
@@ -0,0 +1,170 @@
|
||||
(function (window, document) {
|
||||
"use strict";
|
||||
|
||||
const API = Object.freeze({ key: "payload-key", save: "application-save" });
|
||||
const BOOLEAN_FIELDS = Object.freeze({
|
||||
residenceVerification: "rv",
|
||||
residenceTelephoneVerification: "rtv",
|
||||
officeVerification: "ov",
|
||||
officeTelephoneVerification: "otv",
|
||||
propertyVerification: "pv",
|
||||
referenceVerification: "refv",
|
||||
documentVerification: "docv",
|
||||
residenceCoApplicant: "rco",
|
||||
residenceCoProprietor: "rcp",
|
||||
officeCoProprietor: "ocp",
|
||||
sameResidenceAddress: "sradd",
|
||||
sameOfficeAddress: "soadd",
|
||||
samePropertyAddress: "spadd",
|
||||
autoCutOff: "autocut"
|
||||
});
|
||||
const VALUE_FIELDS = Object.freeze({
|
||||
applicationId: "case_id", portfolioId: "portfolio_id", bankBranchId: "bank_branch_id",
|
||||
applicationNumber: "applno", bankCode: "bankcode", product: "product",
|
||||
loanAmount: "loanamount", customerName: "customername", fatherName: "fathername",
|
||||
applicationType: "apptype", category: "category", dateOfBirth: "dob",
|
||||
contactPerson: "contactperson", mobileNumber: "mobileno", specialInstruction: "specialinst",
|
||||
residenceAddress1: "raddr1", residenceAddress2: "raddr2", residenceAddress3: "raddr3",
|
||||
residenceLandmark: "rlandmark", residenceColonyId: "Rcolony", residenceCity: "rcity",
|
||||
residencePincode: "rpincode", residencePhone: "rphone", companyName: "companyname",
|
||||
officeAddress1: "oaddr1", officeAddress2: "oaddr2", officeAddress3: "oaddr3",
|
||||
officeLandmark: "olandmark", officeColonyId: "Ocolony", officeCity: "ocity",
|
||||
officePincode: "opincode", department: "department", designation: "designation",
|
||||
officePhone: "ophone", extension: "extension", propertyAddress1: "paddr1",
|
||||
propertyAddress2: "paddr2", propertyAddress3: "paddr3", propertyLandmark: "plandmark",
|
||||
propertyColonyId: "Pcolony", propertyCity: "pcity", propertyPincode: "ppincode",
|
||||
referenceName1: "refname1", referenceAddress1: "refaddress1",
|
||||
referenceContactNumber1: "refcontactno1", referenceName2: "refname2",
|
||||
referenceAddress2: "refaddress2", referenceContactNumber2: "refcontactno2",
|
||||
formMode: "formmode"
|
||||
});
|
||||
const INTEGER_FIELDS = new Set(["applicationId", "portfolioId", "bankBranchId", "residenceColonyId",
|
||||
"officeColonyId", "propertyColonyId", "formMode"]);
|
||||
const element = (id) => document.getElementById(id);
|
||||
const value = (id) => element(id)?.value?.trim() ?? "";
|
||||
const integer = (id) => {
|
||||
const parsed = Number.parseInt(value(id), 10);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
};
|
||||
const checked = (id) => Boolean(element(id)?.checked);
|
||||
|
||||
function collectDynamicFields() {
|
||||
const names = value("dynamicfields").split(window.ColDelim || "!C0L!").filter(Boolean);
|
||||
return names.reduce((result, name) => {
|
||||
if (/^(?:field(?:[1-9]|1[0-2])|dtfield1)$/.test(name) && element(name)) {
|
||||
result[name] = value(name);
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function collectCase() {
|
||||
const details = {};
|
||||
Object.entries(VALUE_FIELDS).forEach(([property, id]) => {
|
||||
details[property] = INTEGER_FIELDS.has(property) ? integer(id) : value(id);
|
||||
});
|
||||
Object.entries(BOOLEAN_FIELDS).forEach(([property, id]) => { details[property] = checked(id); });
|
||||
details.dynamicFields = collectDynamicFields();
|
||||
return details;
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
const verificationIds = ["rv", "rtv", "ov", "otv", "pv", "refv", "docv"];
|
||||
if (!verificationIds.some(checked)) {
|
||||
showAlert("danger", "APP-4221", "At least one verification type is required.");
|
||||
return false;
|
||||
}
|
||||
window.invalidFields = 0;
|
||||
window.validate(element("applno"), "t", "AlphaNumeric");
|
||||
window.validate(element("bank_branch_id"), "t", "");
|
||||
window.validate(element("product"), "t", "");
|
||||
window.validate(element("customername"), "t", "AlphaSpace");
|
||||
window.validate(element("apptype"), "t", "");
|
||||
if (checked("rv")) window.CheckForm(element("rv"), "raddr1,raddr2,raddr3,rlandmark,colr,rcity,rpincode", "t,f,t,f,t,t,f", "SplInstruction,SplInstruction,SplInstruction,SplInstruction,SplInstruction,,Pincode");
|
||||
if (checked("ov")) window.CheckForm(element("ov"), "companyname,oaddr1,oaddr2,oaddr3,olandmark,colo,ocity,opincode,department,designation", "f,t,f,t,f,t,t,f,f,f", "SplInstruction,SplInstruction,SplInstruction,SplInstruction,SplInstruction,SplInstruction,,Pincode,SplInstruction,SplInstruction");
|
||||
if (checked("pv")) window.CheckForm(element("pv"), "paddr1,paddr2,paddr3,plandmark,colp,pcity,ppincode", "t,f,t,f,t,t,f", "SplInstruction,SplInstruction,SplInstruction,SplInstruction,SplInstruction,,Pincode");
|
||||
if (checked("rtv")) window.CheckForm(element("rtv"), "rphone", "t", "Phone");
|
||||
if (checked("otv")) window.CheckForm(element("otv"), "ophone", "t", "Phone");
|
||||
if (checked("refv")) window.CheckForm(element("refv"), "refname1,refaddress1,refcontactno1", "t,f,t", "AlphaSpace,SplInstruction,Phone");
|
||||
[["rv", "Rcolony", "colr"], ["ov", "Ocolony", "colo"], ["pv", "Pcolony", "colp"]]
|
||||
.forEach(([flag, hidden, visible]) => {
|
||||
if (checked(flag) && integer(hidden) === 0) window.highlightField(element(visible));
|
||||
});
|
||||
if (document.querySelector(".matrix-validation-error-icon")) {
|
||||
showAlert("danger", "APP-4221", "Correct the highlighted fields and submit the application again.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function showAlert(level, code, message, referenceId) {
|
||||
let host = element("case-save-alerts");
|
||||
if (!host) {
|
||||
host = document.createElement("div");
|
||||
host.id = "case-save-alerts";
|
||||
host.className = "position-fixed top-0 start-50 translate-middle-x p-3";
|
||||
host.style.cssText = "z-index:1085;max-width:720px;width:calc(100% - 2rem)";
|
||||
document.body.append(host);
|
||||
}
|
||||
const alert = document.createElement("div");
|
||||
alert.className = `alert alert-${level} alert-dismissible fade show shadow-sm`;
|
||||
alert.setAttribute("role", "alert");
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = `${code}: `;
|
||||
alert.append(strong, document.createTextNode(message));
|
||||
if (referenceId) {
|
||||
const reference = document.createElement("small");
|
||||
reference.className = "d-block mt-1";
|
||||
reference.textContent = `Reference: ${referenceId}`;
|
||||
alert.append(reference);
|
||||
}
|
||||
const close = document.createElement("button");
|
||||
close.type = "button";
|
||||
close.className = "btn-close";
|
||||
close.setAttribute("data-bs-dismiss", "alert");
|
||||
close.setAttribute("aria-label", "Close");
|
||||
alert.append(close);
|
||||
host.replaceChildren(alert);
|
||||
window.setTimeout(() => window.bootstrap?.Alert.getOrCreateInstance(alert).close(), 7000);
|
||||
}
|
||||
|
||||
function applyResult(result, originalApplicationId) {
|
||||
element("case_id").value = result.applicationId;
|
||||
if (element("mvcode")) element("mvcode").value = result.mvCode || "";
|
||||
if (Array.isArray(window.fields?.[0]) && Array.isArray(window.records?.[window.recpos])) {
|
||||
[["case_id", result.applicationId], ["mvcode", result.mvCode]].forEach(([name, fieldValue]) => {
|
||||
const index = window.fields[0].indexOf(name);
|
||||
if (index >= 0) window.records[window.recpos][index] = fieldValue;
|
||||
});
|
||||
}
|
||||
showAlert("success", "APP-2001", `Application ${result.mvCode || result.applicationId} saved successfully.`);
|
||||
if (originalApplicationId === 0 && typeof window.AddNewRecord === "function") window.AddNewRecord();
|
||||
else if (originalApplicationId > 0 && typeof window.findRecord === "function") window.findRecord(3);
|
||||
}
|
||||
|
||||
async function save(event) {
|
||||
event?.preventDefault();
|
||||
if (!validateForm()) return false;
|
||||
const button = element("btnsave");
|
||||
if (button?.disabled) return false;
|
||||
const details = collectCase();
|
||||
if (button) button.disabled = true;
|
||||
try {
|
||||
const envelope = await window.CygnusPayloadCrypto.encrypt(details, { keyUrl: API.key });
|
||||
const response = await fetch(API.save, { method: "POST", credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(envelope) });
|
||||
const body = await response.json().catch(() => null);
|
||||
if (!response.ok || !body?.success) throw body?.message || body || {};
|
||||
applyResult(body.data, details.applicationId);
|
||||
} catch (error) {
|
||||
showAlert("danger", error.code || "APP-5001",
|
||||
error.message || "Cygnus could not save the application details.", error.referenceId);
|
||||
} finally {
|
||||
if (button) button.disabled = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
window.CygnusInitiation = Object.freeze({ save });
|
||||
}(window, document));
|
||||
91
cygnus-onprem-app/build/WebContent/js/lib/payload-crypto.js
Normal file
91
cygnus-onprem-app/build/WebContent/js/lib/payload-crypto.js
Normal file
@@ -0,0 +1,91 @@
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
const keyRequests = new Map();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
function toBase64Url(bytes) {
|
||||
let binary = "";
|
||||
const view = new Uint8Array(bytes);
|
||||
for (let offset = 0; offset < view.length; offset += 0x8000) {
|
||||
binary += String.fromCharCode(...view.subarray(offset, offset + 0x8000));
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
function fromBase64Url(text) {
|
||||
const normalized = text.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const binary = atob(normalized + "=".repeat((4 - normalized.length % 4) % 4));
|
||||
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
}
|
||||
|
||||
function fetchKey(keyUrl) {
|
||||
if (!keyUrl) {
|
||||
return Promise.reject(new Error("A payload encryption key URL is required."));
|
||||
}
|
||||
if (!keyRequests.has(keyUrl)) {
|
||||
const request = fetch(keyUrl, {
|
||||
credentials: "same-origin",
|
||||
headers: { Accept: "application/json" }
|
||||
}).then(async (response) => {
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw body;
|
||||
if (!body.keyId || !body.publicKey) {
|
||||
throw new Error("The payload encryption key response is invalid.");
|
||||
}
|
||||
return body;
|
||||
}).catch((error) => {
|
||||
keyRequests.delete(keyUrl);
|
||||
throw error;
|
||||
});
|
||||
keyRequests.set(keyUrl, request);
|
||||
}
|
||||
return keyRequests.get(keyUrl);
|
||||
}
|
||||
|
||||
async function encrypt(payload, options) {
|
||||
if (!window.crypto?.subtle) {
|
||||
throw new Error("Secure payload encryption is not supported by this browser.");
|
||||
}
|
||||
const keyInfo = await fetchKey(options?.keyUrl);
|
||||
const publicKey = await window.crypto.subtle.importKey(
|
||||
"spki",
|
||||
fromBase64Url(keyInfo.publicKey),
|
||||
{ name: "RSA-OAEP", hash: "SHA-256" },
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
const aesKey = await window.crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 }, true, ["encrypt"]
|
||||
);
|
||||
const initializationVector = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
const requestId = window.crypto.randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const additionalData = encoder.encode(`${keyInfo.keyId}|${requestId}|${timestamp}`);
|
||||
const encryptedPayload = await window.crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: initializationVector, additionalData },
|
||||
aesKey,
|
||||
encoder.encode(JSON.stringify(payload))
|
||||
);
|
||||
const rawKey = await window.crypto.subtle.exportKey("raw", aesKey);
|
||||
const encryptedKey = await window.crypto.subtle.encrypt(
|
||||
{ name: "RSA-OAEP" }, publicKey, rawKey
|
||||
);
|
||||
|
||||
return Object.freeze({
|
||||
keyId: keyInfo.keyId,
|
||||
encryptedKey: toBase64Url(encryptedKey),
|
||||
initializationVector: toBase64Url(initializationVector),
|
||||
encryptedPayload: toBase64Url(encryptedPayload),
|
||||
requestId,
|
||||
timestamp
|
||||
});
|
||||
}
|
||||
|
||||
function clearKeyCache(keyUrl) {
|
||||
if (keyUrl) keyRequests.delete(keyUrl);
|
||||
else keyRequests.clear();
|
||||
}
|
||||
|
||||
window.CygnusPayloadCrypto = Object.freeze({ encrypt, clearKeyCache });
|
||||
}(window));
|
||||
@@ -2,8 +2,8 @@ package matrix.nimble.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lib.constants.ApplicationError;
|
||||
import lib.models.ErrorDetails;
|
||||
import lib.constants.ApplicationMessage;
|
||||
import lib.models.MessageDetails;
|
||||
import lib.models.UserSession;
|
||||
import matrix.services.commons.CommonErrorService;
|
||||
import matrix.services.commons.CommonService;
|
||||
@@ -28,22 +28,34 @@ public abstract class AbstractAuthenticatedController {
|
||||
HttpServletResponse response) {
|
||||
UserSession session = commonService.getUserSession(httpSession);
|
||||
if (session == null) {
|
||||
return denied(model, response, httpSession, ApplicationError.SESSION_REQUIRED);
|
||||
return denied(model, response, httpSession, ApplicationMessage.SESSION_REQUIRED);
|
||||
}
|
||||
if (!commonService.hasPageAccess(session, pageRoute)) {
|
||||
return denied(model, response, httpSession, ApplicationError.ACCESS_DENIED);
|
||||
return denied(model, response, httpSession, ApplicationMessage.ACCESS_DENIED);
|
||||
}
|
||||
model.addAttribute(CommonService.USER_SESSION_ATTRIBUTE, session);
|
||||
return PageAuthorization.granted(session);
|
||||
}
|
||||
|
||||
protected final ApiAuthorization authorizeApiPage(
|
||||
String pageRoute, HttpSession httpSession) {
|
||||
UserSession session = commonService.getUserSession(httpSession);
|
||||
if (session == null) {
|
||||
return ApiAuthorization.denied(new MessageDetails(ApplicationMessage.SESSION_REQUIRED));
|
||||
}
|
||||
if (!commonService.hasPageAccess(session, pageRoute)) {
|
||||
return ApiAuthorization.denied(new MessageDetails(ApplicationMessage.ACCESS_DENIED));
|
||||
}
|
||||
return ApiAuthorization.granted(session);
|
||||
}
|
||||
|
||||
private PageAuthorization denied(
|
||||
ModelMap model,
|
||||
HttpServletResponse response,
|
||||
HttpSession httpSession,
|
||||
ApplicationError error) {
|
||||
ApplicationMessage error) {
|
||||
String viewName = errorService.render(
|
||||
model, response, httpSession, new ErrorDetails(error));
|
||||
model, response, httpSession, new MessageDetails(error));
|
||||
return PageAuthorization.denied(viewName);
|
||||
}
|
||||
|
||||
@@ -60,4 +72,16 @@ public abstract class AbstractAuthenticatedController {
|
||||
return session != null;
|
||||
}
|
||||
}
|
||||
|
||||
protected record ApiAuthorization(UserSession session, MessageDetails error) {
|
||||
private static ApiAuthorization granted(UserSession session) {
|
||||
return new ApiAuthorization(session, null);
|
||||
}
|
||||
|
||||
private static ApiAuthorization denied(MessageDetails error) {
|
||||
return new ApiAuthorization(null, error);
|
||||
}
|
||||
|
||||
public boolean isGranted() { return session != null; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lib.models.ErrorDetails;
|
||||
import lib.constants.ApplicationError;
|
||||
import lib.models.MessageDetails;
|
||||
import lib.constants.ApplicationMessage;
|
||||
import matrix.services.commons.CommonErrorService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
@@ -37,7 +37,7 @@ public class ApplicationErrorController {
|
||||
return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
|
||||
private ErrorDetails details(int status) {
|
||||
return new ErrorDetails(ApplicationError.fromHttpStatus(status));
|
||||
private MessageDetails details(int status) {
|
||||
return new MessageDetails(ApplicationMessage.fromHttpStatus(status));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -15,17 +19,78 @@ import matrix.services.commons.CommonService;
|
||||
import matrix.services.edp.PunchingService;
|
||||
import matrix.services.commons.CommonErrorService;
|
||||
import lib.models.CasePunching;
|
||||
import lib.models.ApplicationDetails;
|
||||
import lib.models.ApiResponse;
|
||||
import lib.models.ApplicationSaveData;
|
||||
import lib.models.ApplicationSaveResult;
|
||||
import lib.models.CaseSaveRequest;
|
||||
import lib.models.MessageDetails;
|
||||
import lib.constants.ApplicationMessage;
|
||||
import matrix.nimble.security.PayloadCryptoService;
|
||||
import lib.exceptions.ApplicationException;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@Controller
|
||||
|
||||
public class PunchingController extends AbstractAuthenticatedController {
|
||||
private static final Logger LOGGER = Logger.getLogger(PunchingController.class.getName());
|
||||
private static final String INIT_VIEW_ROUTE = "initview";
|
||||
private static final String CASE_ADD_ROUTE = "caseadd";
|
||||
private static final String CASE_EDIT_ROUTE = "caseedit";
|
||||
private final PunchingService punchingService;
|
||||
private final PayloadCryptoService cryptoService;
|
||||
|
||||
public PunchingController(CommonService commonService, CommonErrorService errorService,
|
||||
PunchingService punchingService) {
|
||||
PunchingService punchingService, PayloadCryptoService cryptoService) {
|
||||
super(commonService, errorService);
|
||||
this.punchingService = punchingService;
|
||||
this.cryptoService = cryptoService;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "payload-key", method = RequestMethod.GET,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> caseSaveKey(HttpSession httpSession) {
|
||||
ApiAuthorization authorization = authorizeApiPage(INIT_VIEW_ROUTE, httpSession);
|
||||
if (!authorization.isGranted()) return error(authorization.error());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"keyId", cryptoService.keyId(),
|
||||
"algorithm", "RSA-OAEP-256",
|
||||
"publicKey", cryptoService.encodedPublicKey()));
|
||||
}
|
||||
|
||||
@RequestMapping(value = "application-save", method = RequestMethod.POST,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<ApiResponse<ApplicationSaveData>> saveApplication(
|
||||
@RequestBody CaseSaveRequest request, HttpSession httpSession) {
|
||||
ApiAuthorization authorization = authorizeApiPage(INIT_VIEW_ROUTE, httpSession);
|
||||
if (!authorization.isGranted()) return applicationError(authorization.error());
|
||||
try {
|
||||
ApplicationDetails details = cryptoService.decrypt(request, ApplicationDetails.class);
|
||||
ApplicationSaveResult saved = punchingService.saveApplication(details, authorization.session());
|
||||
return ResponseEntity.ok(ApiResponse.success(
|
||||
new ApplicationSaveData(saved.getOperation(), saved.getApplicationId(), saved.getMvCode()),
|
||||
new MessageDetails(ApplicationMessage.APPLICATION_SAVED)));
|
||||
} catch (ApplicationException exception) {
|
||||
LOGGER.log(Level.WARNING, "Application save was rejected", exception);
|
||||
return applicationError(new MessageDetails(exception.getApplicationMessage()));
|
||||
} catch (RuntimeException exception) {
|
||||
LOGGER.log(Level.SEVERE, "Unexpected case-save failure", exception);
|
||||
return applicationError(new MessageDetails(ApplicationMessage.APPLICATION_SAVE_FAILED));
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<?> error(MessageDetails details) {
|
||||
return ResponseEntity.status(details.getHttpStatus()).body(details);
|
||||
}
|
||||
|
||||
private ResponseEntity<ApiResponse<ApplicationSaveData>> applicationError(MessageDetails details) {
|
||||
return ResponseEntity.status(details.getHttpStatus())
|
||||
.body(ApiResponse.failure(details));
|
||||
}
|
||||
|
||||
@RequestMapping(value = INIT_VIEW_ROUTE, method = RequestMethod.POST)
|
||||
@@ -45,8 +110,8 @@ public class PunchingController extends AbstractAuthenticatedController {
|
||||
return "edp/punching/initcase";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "caseadd", method = RequestMethod.POST)
|
||||
public String addCase(
|
||||
@RequestMapping(value = CASE_ADD_ROUTE, method = RequestMethod.POST)
|
||||
public String prepareNewApplication(
|
||||
@ModelAttribute("model") CasePunching submitted,
|
||||
ModelMap model,
|
||||
HttpSession httpSession,
|
||||
@@ -58,13 +123,13 @@ public class PunchingController extends AbstractAuthenticatedController {
|
||||
}
|
||||
|
||||
UserSession session = authorization.session();
|
||||
model.addAttribute("model", punchingService.addCase(
|
||||
model.addAttribute("model", punchingService.prepareNewApplication(
|
||||
submitted, session.getBranchId(), session.getCompanyId(), session.getUserId()));
|
||||
return "edp/punching/initcase";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "caseedit", method = RequestMethod.POST)
|
||||
public String editCase(
|
||||
@RequestMapping(value = CASE_EDIT_ROUTE, method = RequestMethod.POST)
|
||||
public String prepareApplicationEdit(
|
||||
@RequestParam("PortfolioId") Short portfolioId,
|
||||
@RequestParam("uuid") String documentCaseId,
|
||||
ModelMap model,
|
||||
@@ -77,7 +142,7 @@ public class PunchingController extends AbstractAuthenticatedController {
|
||||
}
|
||||
|
||||
UserSession session = authorization.session();
|
||||
model.addAttribute("model", punchingService.editCase(
|
||||
model.addAttribute("model", punchingService.prepareApplicationEdit(
|
||||
portfolioId, documentCaseId, session.getBranchId(),
|
||||
session.getCompanyId(), session.getUserId()));
|
||||
return "edp/punching/initcase";
|
||||
|
||||
@@ -14,4 +14,10 @@ public final class CloudQueryDefinitionProvider implements QueryDefinitionProvid
|
||||
public QueryDefinition get(int queryId) {
|
||||
return QueryDefinition.parse(queryId, provider.getQuery(queryId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryDefinition get(String queryKey) {
|
||||
int diagnosticId = Math.max(1, queryKey.hashCode() & Integer.MAX_VALUE);
|
||||
return QueryDefinition.parse(diagnosticId, provider.getQuery(queryKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,11 @@ public final class CloudQuerySource implements QuerySource {
|
||||
.orElseThrow(() -> new QueryProviderException(
|
||||
"Cloud query was empty: " + queryId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fetch(String queryKey) {
|
||||
return client.fetchQuery(queryKey).map(response -> response.query()).blockOptional()
|
||||
.filter(query -> !query.isBlank())
|
||||
.orElseThrow(() -> new QueryProviderException("Cloud query was empty: " + queryKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface QueryProvider {
|
||||
|
||||
String getQuery(int queryId);
|
||||
default String getQuery(String queryKey) {
|
||||
throw new UnsupportedOperationException("Query keys are not supported by this provider");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,7 @@ package matrix.nimble.query;
|
||||
public interface QuerySource {
|
||||
|
||||
String fetch(int queryId);
|
||||
default String fetch(String queryKey) {
|
||||
throw new UnsupportedOperationException("Query keys are not supported by this source");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,18 @@ public final class RedisCachingQueryProvider implements QueryProvider {
|
||||
|
||||
@Override
|
||||
public String getQuery(int queryId) {
|
||||
String cacheKey = cacheKey(queryId);
|
||||
return get("id:" + cacheKey(queryId), () -> cloudSource.fetch(queryId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuery(String queryKey) {
|
||||
if (queryKey == null || !queryKey.matches("[A-Z][A-Z0-9_]{2,99}")) {
|
||||
throw new IllegalArgumentException("Invalid query key");
|
||||
}
|
||||
return get("key:" + queryKey, () -> cloudSource.fetch(queryKey));
|
||||
}
|
||||
|
||||
private String get(String cacheKey, java.util.function.Supplier<String> source) {
|
||||
Optional<String> cached = cached(cacheKey);
|
||||
if (cached.isPresent()) {
|
||||
return cached.get();
|
||||
@@ -42,14 +53,14 @@ public final class RedisCachingQueryProvider implements QueryProvider {
|
||||
return cached.get();
|
||||
}
|
||||
try {
|
||||
String query = cloudSource.fetch(queryId);
|
||||
String query = source.get();
|
||||
cache.put(cacheKey, cipher.encrypt(cacheKey, query), QUERY_TTL);
|
||||
return query;
|
||||
} catch (QueryProviderException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw new QueryProviderException(
|
||||
"Unable to retrieve query: " + queryId, exception);
|
||||
"Unable to retrieve query: " + cacheKey, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package matrix.nimble.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.OAEPParameterSpec;
|
||||
import javax.crypto.spec.PSource;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import lib.constants.ApplicationMessage;
|
||||
import lib.exceptions.ApplicationException;
|
||||
import lib.models.CaseSaveRequest;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Thread-safe decryption for short-lived form envelopes created with Web Crypto. */
|
||||
@Service
|
||||
public final class PayloadCryptoService {
|
||||
private static final Duration REQUEST_TTL = Duration.ofMinutes(5);
|
||||
private static final int GCM_TAG_BITS = 128;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final PrivateKey privateKey;
|
||||
private final PublicKey publicKey;
|
||||
private final String keyId;
|
||||
private final Map<String, Instant> acceptedRequests = new ConcurrentHashMap<>();
|
||||
|
||||
public PayloadCryptoService(
|
||||
ObjectMapper objectMapper,
|
||||
@Value("${CYGNUS_PAYLOAD_PRIVATE_KEY:${CYGNUS_CASE_SAVE_PRIVATE_KEY:}}") String privateKeyLocation,
|
||||
@Value("${CYGNUS_PAYLOAD_PUBLIC_KEY:${CYGNUS_CASE_SAVE_PUBLIC_KEY:}}") String publicKeyLocation) {
|
||||
this.objectMapper = objectMapper;
|
||||
KeyPair pair = loadOrGenerate(privateKeyLocation, publicKeyLocation);
|
||||
this.privateKey = pair.getPrivate();
|
||||
this.publicKey = pair.getPublic();
|
||||
this.keyId = fingerprint(publicKey);
|
||||
}
|
||||
|
||||
public String keyId() { return keyId; }
|
||||
|
||||
public String encodedPublicKey() {
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(publicKey.getEncoded());
|
||||
}
|
||||
|
||||
public <T> T decrypt(CaseSaveRequest request, Class<T> payloadType) {
|
||||
validateEnvelope(request);
|
||||
Instant now = Instant.now();
|
||||
Instant submittedAt;
|
||||
try {
|
||||
submittedAt = Instant.parse(request.timestamp());
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw invalid("Invalid request timestamp", exception);
|
||||
}
|
||||
if (submittedAt.isBefore(now.minus(REQUEST_TTL))
|
||||
|| submittedAt.isAfter(now.plusSeconds(30))) {
|
||||
throw invalid("Expired request payload");
|
||||
}
|
||||
discardExpired(now);
|
||||
if (acceptedRequests.putIfAbsent(request.requestId(), now) != null) {
|
||||
throw invalid("Duplicate request payload");
|
||||
}
|
||||
|
||||
try {
|
||||
Base64.Decoder decoder = Base64.getUrlDecoder();
|
||||
Cipher keyCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
||||
keyCipher.init(Cipher.DECRYPT_MODE, privateKey,
|
||||
new OAEPParameterSpec("SHA-256", "MGF1",
|
||||
MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT));
|
||||
byte[] aesKey = keyCipher.doFinal(decoder.decode(request.encryptedKey()));
|
||||
|
||||
byte[] iv = decoder.decode(request.initializationVector());
|
||||
if (iv.length != 12) throw invalid("Invalid initialization vector");
|
||||
Cipher payloadCipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
payloadCipher.init(Cipher.DECRYPT_MODE,
|
||||
new SecretKeySpec(aesKey, "AES"), new GCMParameterSpec(GCM_TAG_BITS, iv));
|
||||
payloadCipher.updateAAD(aad(request));
|
||||
byte[] json = payloadCipher.doFinal(decoder.decode(request.encryptedPayload()));
|
||||
return objectMapper.readValue(json, payloadType);
|
||||
} catch (ApplicationException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
acceptedRequests.remove(request.requestId());
|
||||
throw invalid("Unable to decrypt payload", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] aad(CaseSaveRequest request) {
|
||||
return String.join("|", request.keyId(), request.requestId(), request.timestamp())
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void validateEnvelope(CaseSaveRequest request) {
|
||||
if (request == null || blank(request.keyId()) || blank(request.encryptedKey())
|
||||
|| blank(request.initializationVector()) || blank(request.encryptedPayload())
|
||||
|| blank(request.requestId()) || blank(request.timestamp())) {
|
||||
throw invalid("Incomplete encrypted request");
|
||||
}
|
||||
if (!keyId.equals(request.keyId())) throw invalid("Unsupported encryption key");
|
||||
}
|
||||
|
||||
private void discardExpired(Instant now) {
|
||||
Instant cutoff = now.minus(REQUEST_TTL);
|
||||
acceptedRequests.entrySet().removeIf(entry -> entry.getValue().isBefore(cutoff));
|
||||
}
|
||||
|
||||
private KeyPair loadOrGenerate(String privateLocation, String publicLocation) {
|
||||
if (blank(privateLocation) && blank(publicLocation)) {
|
||||
try {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(3072);
|
||||
return generator.generateKeyPair();
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to generate payload key pair", exception);
|
||||
}
|
||||
}
|
||||
if (blank(privateLocation) || blank(publicLocation)) {
|
||||
throw new IllegalStateException(
|
||||
"Both CYGNUS_PAYLOAD_PRIVATE_KEY and CYGNUS_PAYLOAD_PUBLIC_KEY are required");
|
||||
}
|
||||
try {
|
||||
KeyFactory factory = KeyFactory.getInstance("RSA");
|
||||
return new KeyPair(
|
||||
factory.generatePublic(new X509EncodedKeySpec(pem(publicLocation, "PUBLIC KEY"))),
|
||||
factory.generatePrivate(new PKCS8EncodedKeySpec(pem(privateLocation, "PRIVATE KEY"))));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to load payload key pair", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] pem(String location, String type) throws Exception {
|
||||
String path = location.startsWith("file:") ? location.substring(5) : location;
|
||||
String value = Files.readString(Path.of(path), StandardCharsets.US_ASCII)
|
||||
.replace("-----BEGIN " + type + "-----", "")
|
||||
.replace("-----END " + type + "-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
return Base64.getDecoder().decode(value);
|
||||
}
|
||||
|
||||
private String fingerprint(PublicKey key) {
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(key.getEncoded());
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to identify payload key", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean blank(String value) { return value == null || value.isBlank(); }
|
||||
|
||||
private ApplicationException invalid(String technicalMessage) {
|
||||
return new ApplicationException(ApplicationMessage.PAYLOAD_INVALID, technicalMessage);
|
||||
}
|
||||
|
||||
private ApplicationException invalid(String technicalMessage, Throwable cause) {
|
||||
return new ApplicationException(ApplicationMessage.PAYLOAD_INVALID, technicalMessage, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package matrix.nimble.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class WebJsonConfiguration {
|
||||
@Bean
|
||||
ObjectMapper webObjectMapper() {
|
||||
return new ObjectMapper()
|
||||
.findAndRegisterModules()
|
||||
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@ package matrix.services.commons;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lib.models.ErrorDetails;
|
||||
import lib.models.MessageDetails;
|
||||
import matrix.nimble.model.Session;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ui.ModelMap;
|
||||
|
||||
@Service
|
||||
public class CommonErrorService {
|
||||
public static final String ERROR_ATTRIBUTE = "errorDetails";
|
||||
public static final String ERROR_ATTRIBUTE = "messageDetails";
|
||||
private final CommonService commonService;
|
||||
|
||||
public CommonErrorService(CommonService commonService) {
|
||||
@@ -20,9 +20,9 @@ public class CommonErrorService {
|
||||
ModelMap model,
|
||||
HttpServletResponse response,
|
||||
HttpSession httpSession,
|
||||
ErrorDetails errorDetails) {
|
||||
response.setStatus(errorDetails.getHttpStatus());
|
||||
model.addAttribute(ERROR_ATTRIBUTE, errorDetails);
|
||||
MessageDetails messageDetails) {
|
||||
response.setStatus(messageDetails.getHttpStatus());
|
||||
model.addAttribute(ERROR_ATTRIBUTE, messageDetails);
|
||||
|
||||
Session session = commonService.getSession(httpSession);
|
||||
if (session != null) {
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
package matrix.services.edp;
|
||||
|
||||
import com.cygnus.db.CygnusDbExecutor;
|
||||
import com.cygnus.db.SqlArrayParameter;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import lib.models.ApplicationDetails;
|
||||
import lib.models.CasePunching;
|
||||
import lib.models.ErrorDetails;
|
||||
import lib.models.ApplicationSaveResult;
|
||||
import lib.models.MessageDetails;
|
||||
import lib.models.Option;
|
||||
import lib.constants.ApplicationError;
|
||||
import lib.models.UserSession;
|
||||
import lib.constants.ApplicationMessage;
|
||||
import lib.exceptions.ApplicationException;
|
||||
import matrix.nimble.edp.punching.model.PunchingHandler;
|
||||
import matrix.nimble.utilities.GlobalClass;
|
||||
import matrix.services.commons.CommonService;
|
||||
@@ -35,11 +44,115 @@ public class PunchingService {
|
||||
SqlArrayParameter.text("CATEGORY", "PRODUCT");
|
||||
|
||||
private final CommonService commonService;
|
||||
private final CygnusDbExecutor dbExecutor;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PunchingService(CommonService commonService) {
|
||||
this.commonService = commonService;
|
||||
this.dbExecutor = null;
|
||||
this.objectMapper = null;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public PunchingService(
|
||||
CommonService commonService, CygnusDbExecutor dbExecutor, ObjectMapper objectMapper) {
|
||||
this.commonService = commonService;
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public ApplicationSaveResult saveApplication(ApplicationDetails details, UserSession session) {
|
||||
if (dbExecutor == null || objectMapper == null) {
|
||||
throw new IllegalStateException("Application-save dependencies are not configured");
|
||||
}
|
||||
validate(details, session);
|
||||
sanitizeDynamicFields(details);
|
||||
try {
|
||||
String json = objectMapper.writeValueAsString(details);
|
||||
ApplicationSaveResult result = dbExecutor.procedure("APPLICATION_SAVE",
|
||||
new Object[] {json, session.getCompanyId(), session.getBranchId(),
|
||||
session.getUserId()}, ApplicationSaveResult.class);
|
||||
if (result == null || result.getApplicationId() == null) {
|
||||
throw new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_SAVE_FAILED, "Database returned no application result");
|
||||
}
|
||||
return result;
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new ApplicationException(
|
||||
ApplicationMessage.PAYLOAD_INVALID, "Unable to serialize application details", exception);
|
||||
} catch (ApplicationException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw databaseFailure(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(ApplicationDetails details, UserSession session) {
|
||||
if (details == null || session == null || details.getPortfolioId() == null
|
||||
|| details.getPortfolioId() <= 0) {
|
||||
throw new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_VALIDATION_FAILED, "Portfolio is required");
|
||||
}
|
||||
if (details.getBankBranchId() == null || details.getBankBranchId() <= 0) {
|
||||
throw new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_VALIDATION_FAILED, "Bank branch is required");
|
||||
}
|
||||
if (blank(details.getApplicationNumber()) || blank(details.getProduct())
|
||||
|| blank(details.getCustomerName()) || blank(details.getApplicationType())) {
|
||||
throw new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_VALIDATION_FAILED, "Required application fields are missing");
|
||||
}
|
||||
if (!Boolean.TRUE.equals(details.getResidenceVerification())
|
||||
&& !Boolean.TRUE.equals(details.getResidenceTelephoneVerification())
|
||||
&& !Boolean.TRUE.equals(details.getOfficeVerification())
|
||||
&& !Boolean.TRUE.equals(details.getOfficeTelephoneVerification())
|
||||
&& !Boolean.TRUE.equals(details.getPropertyVerification())
|
||||
&& !Boolean.TRUE.equals(details.getReferenceVerification())
|
||||
&& !Boolean.TRUE.equals(details.getDocumentVerification())) {
|
||||
throw new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_VALIDATION_FAILED,
|
||||
"At least one verification type is required");
|
||||
}
|
||||
}
|
||||
|
||||
private void sanitizeDynamicFields(ApplicationDetails details) {
|
||||
if (details.getDynamicFields() == null || details.getDynamicFields().isEmpty()) {
|
||||
details.setDynamicFields(new LinkedHashMap<>());
|
||||
return;
|
||||
}
|
||||
Map<String, String> allowed = new LinkedHashMap<>();
|
||||
details.getDynamicFields().forEach((name, value) -> {
|
||||
if (name != null && (name.matches("field(?:[1-9]|1[0-2])")
|
||||
|| "dtfield1".equals(name))) {
|
||||
allowed.put(name, value == null ? null : value.trim());
|
||||
}
|
||||
});
|
||||
details.setDynamicFields(allowed);
|
||||
}
|
||||
|
||||
private ApplicationException databaseFailure(RuntimeException exception) {
|
||||
Throwable cause = exception;
|
||||
while (cause != null) {
|
||||
if (cause instanceof SQLException sqlException) {
|
||||
return switch (sqlException.getSQLState()) {
|
||||
case "42501" -> new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_ACCESS_DENIED, sqlException.getMessage(), exception);
|
||||
case "22023", "23502", "23503" -> new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_VALIDATION_FAILED, sqlException.getMessage(), exception);
|
||||
case "23505" -> new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_SAVE_CONFLICT, sqlException.getMessage(), exception);
|
||||
default -> new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_SAVE_FAILED, "Database save failed", exception);
|
||||
};
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return new ApplicationException(
|
||||
ApplicationMessage.APPLICATION_SAVE_FAILED, "Database save failed", exception);
|
||||
}
|
||||
|
||||
private boolean blank(String value) { return value == null || value.isBlank(); }
|
||||
|
||||
public CasePunching init(
|
||||
Short branchId,
|
||||
Short userId,
|
||||
@@ -66,7 +179,7 @@ public class PunchingService {
|
||||
return casePunching;
|
||||
}
|
||||
|
||||
public CasePunching addCase(
|
||||
public CasePunching prepareNewApplication(
|
||||
CasePunching submitted,
|
||||
Short branchId,
|
||||
Short companyId,
|
||||
@@ -83,7 +196,7 @@ public class PunchingService {
|
||||
result.setVisibleSections(List.of());
|
||||
result.setDynamicFields("");
|
||||
result.setDynamicHtml("");
|
||||
result.setErrorDetails(new ErrorDetails(ApplicationError.BAD_REQUEST));
|
||||
result.setMessageDetails(new MessageDetails(ApplicationMessage.BAD_REQUEST));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -104,7 +217,7 @@ public class PunchingService {
|
||||
result.setDynamicFields("");
|
||||
result.setDynamicHtml("");
|
||||
result.setVisibleSections(List.of());
|
||||
result.setErrorDetails(new ErrorDetails(ApplicationError.INTERNAL_SERVER_ERROR));
|
||||
result.setMessageDetails(new MessageDetails(ApplicationMessage.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
|
||||
result.setOptions(loadOptions(branchId, companyId, portfolioId));
|
||||
@@ -126,7 +239,7 @@ public class PunchingService {
|
||||
return freezeOptions(options);
|
||||
}
|
||||
|
||||
public CasePunching editCase(
|
||||
public CasePunching prepareApplicationEdit(
|
||||
Short portfolioId,
|
||||
String documentCaseId,
|
||||
Short branchId,
|
||||
@@ -135,7 +248,7 @@ public class PunchingService {
|
||||
CasePunching submitted = new CasePunching();
|
||||
submitted.setPortfolioId(portfolioId);
|
||||
submitted.setDocumentCaseId(documentCaseId);
|
||||
CasePunching result = addCase(submitted, branchId, companyId, userId);
|
||||
CasePunching result = prepareNewApplication(submitted, branchId, companyId, userId);
|
||||
result.setFormMode(2);
|
||||
result.setDocumentCaseId(documentCaseId);
|
||||
return result;
|
||||
|
||||
@@ -523,7 +523,8 @@ class RoleVisibleBatchMigrationTest {
|
||||
|
||||
String initialCase = Files.readString(punching.resolve("initcase.jsp"));
|
||||
assertTrue(initialCase.contains("GotoRecord(this,'recno')"));
|
||||
assertTrue(initialCase.contains("ValidateSubmitForm('casedetails')"));
|
||||
assertTrue(initialCase.contains("CygnusInitiation.save(event)"));
|
||||
assertTrue(initialCase.contains("/matrix/js/edp/initiation.js"));
|
||||
assertTrue(initialCase.contains("AddNewRecord()"));
|
||||
|
||||
String initialDocs = Files.readString(punching.resolve("initdocs.jsp"));
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package matrix.nimble.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.OAEPParameterSpec;
|
||||
import javax.crypto.spec.PSource;
|
||||
import lib.models.ApplicationDetails;
|
||||
import lib.models.CaseSaveRequest;
|
||||
import lib.exceptions.ApplicationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PayloadCryptoServiceTest {
|
||||
|
||||
@Test
|
||||
void decryptsBrowserCompatibleEnvelopeAndRejectsReplay() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
|
||||
PayloadCryptoService service = new PayloadCryptoService(mapper, "", "");
|
||||
ApplicationDetails details = new ApplicationDetails();
|
||||
details.setApplicationId(42);
|
||||
details.setApplicationNumber("APP-42");
|
||||
|
||||
String requestId = UUID.randomUUID().toString();
|
||||
String timestamp = Instant.now().toString();
|
||||
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(
|
||||
new X509EncodedKeySpec(Base64.getUrlDecoder().decode(service.encodedPublicKey())));
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(256);
|
||||
SecretKey aesKey = generator.generateKey();
|
||||
byte[] iv = new byte[12];
|
||||
java.security.SecureRandom.getInstanceStrong().nextBytes(iv);
|
||||
byte[] aad = String.join("|", service.keyId(), requestId, timestamp)
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
Cipher payload = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
payload.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(128, iv));
|
||||
payload.updateAAD(aad);
|
||||
byte[] encryptedPayload = payload.doFinal(mapper.writeValueAsBytes(details));
|
||||
|
||||
Cipher wrapper = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
|
||||
wrapper.init(Cipher.ENCRYPT_MODE, publicKey,
|
||||
new OAEPParameterSpec("SHA-256", "MGF1",
|
||||
MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT));
|
||||
byte[] encryptedKey = wrapper.doFinal(aesKey.getEncoded());
|
||||
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
CaseSaveRequest request = new CaseSaveRequest(
|
||||
service.keyId(), encoder.encodeToString(encryptedKey), encoder.encodeToString(iv),
|
||||
encoder.encodeToString(encryptedPayload), requestId, timestamp);
|
||||
|
||||
assertEquals(42, service.decrypt(request, ApplicationDetails.class).getApplicationId());
|
||||
assertThrows(ApplicationException.class, () -> service.decrypt(request, ApplicationDetails.class));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package matrix.services.commons;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import lib.models.ErrorDetails;
|
||||
import lib.constants.ApplicationError;
|
||||
import lib.models.MessageDetails;
|
||||
import lib.constants.ApplicationMessage;
|
||||
import matrix.nimble.model.Session;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
@@ -21,7 +21,7 @@ class CommonErrorServiceTest {
|
||||
Session session = new Session();
|
||||
session.setMenuHtml("<div id='MenuBar'></div>");
|
||||
commonService.storeSession(httpSession, session);
|
||||
ErrorDetails details = new ErrorDetails(ApplicationError.ACCESS_DENIED);
|
||||
MessageDetails details = new MessageDetails(ApplicationMessage.ACCESS_DENIED);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
ModelMap model = new ModelMap();
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ public interface CygnusDbExecutor {
|
||||
int update(int queryId, Object[] parameters);
|
||||
int delete(int queryId, Object[] parameters);
|
||||
<T> T procedure(int queryId, Object[] parameters, Class<T> responseType);
|
||||
default <T> T procedure(String queryKey, Object[] parameters, Class<T> responseType) {
|
||||
throw new UnsupportedOperationException("Query keys are not supported by this executor");
|
||||
}
|
||||
<T> List<T> procedureRows(int queryId, Object[] parameters, Class<T> responseType);
|
||||
<T> void stream(int queryId, Object[] parameters, RowMapper<T> mapper, RowConsumer<T> consumer);
|
||||
<T> T transaction(TransactionCallback<T> callback);
|
||||
|
||||
@@ -109,6 +109,15 @@ public final class JdbcCygnusDbExecutor implements CygnusDbExecutor {
|
||||
return rows.isEmpty() ? null : rows.getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T procedure(String queryKey, Object[] parameters, Class<T> responseType) {
|
||||
QueryDefinition definition = queryProvider.get(queryKey);
|
||||
if (definition.type() != QueryType.PROCEDURE) throw new QueryDefinitionException(
|
||||
"Query " + queryKey + " is " + definition.type() + ", not " + QueryType.PROCEDURE);
|
||||
DbResult<T> result = executeRowsOrCount(definition, parameters, responseType);
|
||||
return result.rows().isEmpty() ? null : result.rows().getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> procedureRows(int queryId, Object[] parameters, Class<T> responseType) {
|
||||
DbResult<T> result = executeRowsOrCount(
|
||||
|
||||
@@ -3,4 +3,7 @@ package com.cygnus.db;
|
||||
@FunctionalInterface
|
||||
public interface QueryDefinitionProvider {
|
||||
QueryDefinition get(int queryId);
|
||||
default QueryDefinition get(String queryKey) {
|
||||
throw new UnsupportedOperationException("Query keys are not supported by this provider");
|
||||
}
|
||||
}
|
||||
|
||||
293
cygnus-onprem-db/src/main/resources/db/003_application_save.sql
Normal file
293
cygnus-onprem-db/src/main/resources/db/003_application_save.sql
Normal file
@@ -0,0 +1,293 @@
|
||||
-- Atomic application initiation save.
|
||||
BEGIN;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.save_application_details(
|
||||
p_application jsonb,
|
||||
p_company_id smallint,
|
||||
p_branch_id smallint,
|
||||
p_user_id smallint
|
||||
)
|
||||
RETURNS TABLE(operation text, application_id integer, mv_code text, internal_uuid text)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public, pg_temp
|
||||
AS $function$
|
||||
DECLARE
|
||||
d record;
|
||||
v_case_id integer;
|
||||
v_uuid text;
|
||||
v_mvcode text;
|
||||
v_operation text;
|
||||
v_mvnumber integer;
|
||||
v_mvprefix text;
|
||||
v_batch text;
|
||||
v_earlier smallint := 1;
|
||||
v_tele_denied smallint := 0;
|
||||
v_reference_denied smallint := 0;
|
||||
v_now timestamp without time zone := clock_timestamp();
|
||||
BEGIN
|
||||
IF p_application IS NULL OR p_company_id IS NULL OR p_branch_id IS NULL OR p_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Incomplete case context' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
SELECT * INTO d
|
||||
FROM jsonb_to_record(p_application) AS x(
|
||||
"applicationId" integer, "portfolioId" smallint, "bankBranchId" smallint,
|
||||
"applicationNumber" text, "bankCode" text, "product" text, "loanAmount" text,
|
||||
"customerName" text, "fatherName" text, "applicationType" text, "category" text,
|
||||
"dateOfBirth" text, "contactPerson" text, "mobileNumber" text,
|
||||
"specialInstruction" text, "residenceVerification" boolean,
|
||||
"residenceTelephoneVerification" boolean, "officeVerification" boolean,
|
||||
"officeTelephoneVerification" boolean, "propertyVerification" boolean,
|
||||
"referenceVerification" boolean, "documentVerification" boolean,
|
||||
"residenceCoApplicant" boolean, "residenceCoProprietor" boolean,
|
||||
"officeCoProprietor" boolean, "sameResidenceAddress" boolean,
|
||||
"sameOfficeAddress" boolean, "samePropertyAddress" boolean,
|
||||
"residenceAddress1" text, "residenceAddress2" text, "residenceAddress3" text,
|
||||
"residenceLandmark" text, "residenceColonyId" integer, "residenceCity" text,
|
||||
"residencePincode" text, "residencePhone" text, "companyName" text,
|
||||
"officeAddress1" text, "officeAddress2" text, "officeAddress3" text,
|
||||
"officeLandmark" text, "officeColonyId" integer, "officeCity" text,
|
||||
"officePincode" text, "department" text, "designation" text,
|
||||
"officePhone" text, "extension" text, "propertyAddress1" text,
|
||||
"propertyAddress2" text, "propertyAddress3" text, "propertyLandmark" text,
|
||||
"propertyColonyId" integer, "propertyCity" text, "propertyPincode" text,
|
||||
"referenceName1" text, "referenceAddress1" text, "referenceContactNumber1" text,
|
||||
"referenceName2" text, "referenceAddress2" text, "referenceContactNumber2" text,
|
||||
"autoCutOff" boolean, "formMode" integer
|
||||
);
|
||||
|
||||
IF d."portfolioId" IS NULL OR d."portfolioId" <= 0
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM portfolio p
|
||||
WHERE p.portfolio_id = d."portfolioId"
|
||||
AND p.company_id = p_company_id
|
||||
AND p.branch_id = p_branch_id
|
||||
AND p.isactive = 1) THEN
|
||||
RAISE EXCEPTION 'Portfolio is not available to this session' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF coalesce(trim(d."applicationNumber"), '') = ''
|
||||
OR coalesce(d."bankBranchId", 0) <= 0
|
||||
OR coalesce(trim(d."product"), '') = ''
|
||||
OR coalesce(trim(d."customerName"), '') = ''
|
||||
OR coalesce(trim(d."applicationType"), '') = '' THEN
|
||||
RAISE EXCEPTION 'Required case fields are missing' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
IF NOT (coalesce(d."residenceVerification", false)
|
||||
OR coalesce(d."residenceTelephoneVerification", false)
|
||||
OR coalesce(d."officeVerification", false)
|
||||
OR coalesce(d."officeTelephoneVerification", false)
|
||||
OR coalesce(d."propertyVerification", false)
|
||||
OR coalesce(d."referenceVerification", false)
|
||||
OR coalesce(d."documentVerification", false)) THEN
|
||||
RAISE EXCEPTION 'At least one verification is required' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
v_case_id := coalesce(d."applicationId", 0);
|
||||
IF v_case_id = 0 THEN
|
||||
UPDATE mvcode
|
||||
SET lastmvcode = lastmvcode + 1
|
||||
WHERE portfolio_id = d."portfolioId"
|
||||
RETURNING lastmvcode, trim(mvcprefix) INTO v_mvnumber, v_mvprefix;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'MV code configuration is missing' USING ERRCODE = '23503';
|
||||
END IF;
|
||||
v_mvcode := coalesce(v_mvprefix, '') || v_mvnumber;
|
||||
v_uuid := gen_random_uuid()::text;
|
||||
|
||||
INSERT INTO main (
|
||||
portfolio_id, bank_branch_id, mvcode, applno, bankcode, product, loanamount,
|
||||
customername, fathername, apptype, category, dob, contactperson, mobileno,
|
||||
specialinst, rv, rtv, ov, otv, pv, refv, docv, rco, rcp, ocp, sradd, soadd,
|
||||
spadd, raddr1, raddr2, raddr3, rlandmark, rcolony, rcity, rpincode, rphone,
|
||||
companyname, oaddr1, oaddr2, oaddr3, olandmark, ocolony, ocity, opincode,
|
||||
department, designation, ophone, extension, paddr1, paddr2, paddr3, plandmark,
|
||||
pcolony, pcity, ppincode, refname1, refaddress1, refcontactno1, refname2,
|
||||
refaddress2, refcontactno2, field1, field2, field3, field4, field5, field6,
|
||||
field7, field8, field9, field10, field11, field12, dtfield1, receivedate,
|
||||
addedby, addedon, company_id, branch_id, uuid)
|
||||
VALUES (
|
||||
d."portfolioId", coalesce(d."bankBranchId", 0), v_mvcode,
|
||||
trim(d."applicationNumber"), nullif(trim(d."bankCode"), ''), trim(d."product"),
|
||||
nullif(trim(d."loanAmount"), ''), trim(d."customerName"),
|
||||
nullif(trim(d."fatherName"), ''), trim(d."applicationType"),
|
||||
nullif(trim(d."category"), ''), nullif(trim(d."dateOfBirth"), ''),
|
||||
nullif(trim(d."contactPerson"), ''), nullif(trim(d."mobileNumber"), ''),
|
||||
coalesce(d."specialInstruction", ''),
|
||||
coalesce(d."residenceVerification", false)::int,
|
||||
coalesce(d."residenceTelephoneVerification", false)::int,
|
||||
coalesce(d."officeVerification", false)::int,
|
||||
coalesce(d."officeTelephoneVerification", false)::int,
|
||||
coalesce(d."propertyVerification", false)::int,
|
||||
coalesce(d."referenceVerification", false)::int,
|
||||
coalesce(d."documentVerification", false)::int,
|
||||
coalesce(d."residenceCoApplicant", false)::int,
|
||||
coalesce(d."residenceCoProprietor", false)::int,
|
||||
coalesce(d."officeCoProprietor", false)::int,
|
||||
coalesce(d."sameResidenceAddress", false)::int,
|
||||
coalesce(d."sameOfficeAddress", false)::int,
|
||||
coalesce(d."samePropertyAddress", false)::int,
|
||||
d."residenceAddress1", d."residenceAddress2", d."residenceAddress3",
|
||||
d."residenceLandmark", coalesce(d."residenceColonyId", 0), d."residenceCity",
|
||||
d."residencePincode", d."residencePhone", d."companyName", d."officeAddress1",
|
||||
d."officeAddress2", d."officeAddress3", d."officeLandmark",
|
||||
coalesce(d."officeColonyId", 0), d."officeCity", d."officePincode",
|
||||
d."department", d."designation", d."officePhone", d."extension",
|
||||
d."propertyAddress1", d."propertyAddress2", d."propertyAddress3",
|
||||
d."propertyLandmark", coalesce(d."propertyColonyId", 0), d."propertyCity",
|
||||
d."propertyPincode", d."referenceName1", d."referenceAddress1",
|
||||
d."referenceContactNumber1", d."referenceName2", d."referenceAddress2",
|
||||
d."referenceContactNumber2", p_application->'dynamicFields'->>'field1',
|
||||
p_application->'dynamicFields'->>'field2', p_application->'dynamicFields'->>'field3',
|
||||
p_application->'dynamicFields'->>'field4', p_application->'dynamicFields'->>'field5',
|
||||
p_application->'dynamicFields'->>'field6', p_application->'dynamicFields'->>'field7',
|
||||
p_application->'dynamicFields'->>'field8', p_application->'dynamicFields'->>'field9',
|
||||
p_application->'dynamicFields'->>'field10', p_application->'dynamicFields'->>'field11',
|
||||
p_application->'dynamicFields'->>'field12',
|
||||
nullif(p_application->'dynamicFields'->>'dtfield1', '')::date,
|
||||
current_date, p_user_id, v_now, p_company_id, p_branch_id, v_uuid)
|
||||
RETURNING main.case_id INTO v_case_id;
|
||||
|
||||
INSERT INTO main_operations (uuid, company_id, branch_id, islocked, portfolio_id)
|
||||
VALUES (v_uuid, p_company_id, p_branch_id, 0, d."portfolioId");
|
||||
v_operation := 'INSERT';
|
||||
ELSE
|
||||
SELECT m.uuid, m.mvcode INTO v_uuid, v_mvcode
|
||||
FROM main m
|
||||
WHERE m.case_id = v_case_id
|
||||
AND m.company_id = p_company_id
|
||||
AND m.branch_id = p_branch_id
|
||||
AND m.portfolio_id = d."portfolioId"
|
||||
AND m.isdeleted = 0
|
||||
FOR UPDATE;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Case is not available to this session' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
|
||||
UPDATE main SET
|
||||
bank_branch_id=coalesce(d."bankBranchId",0), applno=trim(d."applicationNumber"),
|
||||
bankcode=nullif(trim(d."bankCode"),''), product=trim(d."product"),
|
||||
loanamount=nullif(trim(d."loanAmount"),''), customername=trim(d."customerName"),
|
||||
fathername=nullif(trim(d."fatherName"),''), apptype=trim(d."applicationType"),
|
||||
category=nullif(trim(d."category"),''), dob=nullif(trim(d."dateOfBirth"),''),
|
||||
contactperson=nullif(trim(d."contactPerson"),''), mobileno=nullif(trim(d."mobileNumber"),''),
|
||||
specialinst=coalesce(d."specialInstruction",''),
|
||||
rv=coalesce(d."residenceVerification",false)::int,
|
||||
rtv=coalesce(d."residenceTelephoneVerification",false)::int,
|
||||
ov=coalesce(d."officeVerification",false)::int,
|
||||
otv=coalesce(d."officeTelephoneVerification",false)::int,
|
||||
pv=coalesce(d."propertyVerification",false)::int,
|
||||
refv=coalesce(d."referenceVerification",false)::int,
|
||||
docv=coalesce(d."documentVerification",false)::int,
|
||||
rco=coalesce(d."residenceCoApplicant",false)::int,
|
||||
rcp=coalesce(d."residenceCoProprietor",false)::int,
|
||||
ocp=coalesce(d."officeCoProprietor",false)::int,
|
||||
sradd=coalesce(d."sameResidenceAddress",false)::int,
|
||||
soadd=coalesce(d."sameOfficeAddress",false)::int,
|
||||
spadd=coalesce(d."samePropertyAddress",false)::int,
|
||||
raddr1=d."residenceAddress1", raddr2=d."residenceAddress2", raddr3=d."residenceAddress3",
|
||||
rlandmark=d."residenceLandmark", rcolony=coalesce(d."residenceColonyId",0),
|
||||
rcity=d."residenceCity", rpincode=d."residencePincode", rphone=d."residencePhone",
|
||||
companyname=d."companyName", oaddr1=d."officeAddress1", oaddr2=d."officeAddress2",
|
||||
oaddr3=d."officeAddress3", olandmark=d."officeLandmark",
|
||||
ocolony=coalesce(d."officeColonyId",0), ocity=d."officeCity",
|
||||
opincode=d."officePincode", department=d."department", designation=d."designation",
|
||||
ophone=d."officePhone", extension=d."extension", paddr1=d."propertyAddress1",
|
||||
paddr2=d."propertyAddress2", paddr3=d."propertyAddress3",
|
||||
plandmark=d."propertyLandmark", pcolony=coalesce(d."propertyColonyId",0),
|
||||
pcity=d."propertyCity", ppincode=d."propertyPincode", refname1=d."referenceName1",
|
||||
refaddress1=d."referenceAddress1", refcontactno1=d."referenceContactNumber1",
|
||||
refname2=d."referenceName2", refaddress2=d."referenceAddress2",
|
||||
refcontactno2=d."referenceContactNumber2", field1=p_application->'dynamicFields'->>'field1',
|
||||
field2=p_application->'dynamicFields'->>'field2', field3=p_application->'dynamicFields'->>'field3',
|
||||
field4=p_application->'dynamicFields'->>'field4', field5=p_application->'dynamicFields'->>'field5',
|
||||
field6=p_application->'dynamicFields'->>'field6', field7=p_application->'dynamicFields'->>'field7',
|
||||
field8=p_application->'dynamicFields'->>'field8', field9=p_application->'dynamicFields'->>'field9',
|
||||
field10=p_application->'dynamicFields'->>'field10', field11=p_application->'dynamicFields'->>'field11',
|
||||
field12=p_application->'dynamicFields'->>'field12',
|
||||
dtfield1=nullif(p_application->'dynamicFields'->>'dtfield1','')::date,
|
||||
addedon=CASE WHEN addedby=0 THEN v_now ELSE addedon END,
|
||||
addedby=CASE WHEN addedby=0 THEN p_user_id ELSE addedby END,
|
||||
lasteditedon=v_now, lasteditedby=p_user_id
|
||||
WHERE main.case_id=v_case_id;
|
||||
|
||||
IF coalesce(d."formMode", 0) = 2 THEN
|
||||
INSERT INTO caseedit_history(user_id, operation, uuid, operationon)
|
||||
VALUES (p_user_id, 'CASEEDIT', v_uuid, v_now);
|
||||
|
||||
UPDATE temp_reporting SET
|
||||
bank_branch_id=coalesce(d."bankBranchId",0),
|
||||
applno=trim(d."applicationNumber"), product=trim(d."product"),
|
||||
customername=trim(d."customerName"), apptype=trim(d."applicationType"),
|
||||
category=nullif(trim(d."category"),''),
|
||||
rv=coalesce(d."residenceVerification",false)::int,
|
||||
rtv=coalesce(d."residenceTelephoneVerification",false)::int,
|
||||
ov=coalesce(d."officeVerification",false)::int,
|
||||
otv=coalesce(d."officeTelephoneVerification",false)::int,
|
||||
pv=coalesce(d."propertyVerification",false)::int,
|
||||
refv=coalesce(d."referenceVerification",false)::int,
|
||||
docv=coalesce(d."documentVerification",false)::int,
|
||||
rco=coalesce(d."residenceCoApplicant",false)::int,
|
||||
rcp=coalesce(d."residenceCoProprietor",false)::int,
|
||||
ocp=coalesce(d."officeCoProprietor",false)::int,
|
||||
sradd=coalesce(d."sameResidenceAddress",false)::int,
|
||||
soadd=coalesce(d."sameOfficeAddress",false)::int,
|
||||
spadd=coalesce(d."samePropertyAddress",false)::int
|
||||
WHERE uuid=v_uuid;
|
||||
|
||||
IF coalesce(d."residenceVerification", false) THEN
|
||||
INSERT INTO residence(uuid)
|
||||
SELECT v_uuid WHERE NOT EXISTS (SELECT 1 FROM residence WHERE uuid=v_uuid);
|
||||
END IF;
|
||||
IF coalesce(d."officeVerification", false) THEN
|
||||
INSERT INTO office(uuid)
|
||||
SELECT v_uuid WHERE NOT EXISTS (SELECT 1 FROM office WHERE uuid=v_uuid);
|
||||
END IF;
|
||||
IF coalesce(d."residenceTelephoneVerification", false)
|
||||
OR coalesce(d."officeTelephoneVerification", false) THEN
|
||||
SELECT coalesce(max(isdenied), 0) INTO v_tele_denied
|
||||
FROM denied_process
|
||||
WHERE portfolio_id=d."portfolioId" AND process_id=14 AND isdenied=1;
|
||||
IF v_tele_denied=0 THEN
|
||||
INSERT INTO telecalling(uuid)
|
||||
SELECT v_uuid WHERE NOT EXISTS (SELECT 1 FROM telecalling WHERE uuid=v_uuid);
|
||||
ELSE
|
||||
UPDATE temp_reporting SET rtv=0, otv=0 WHERE uuid=v_uuid;
|
||||
END IF;
|
||||
END IF;
|
||||
IF coalesce(d."referenceVerification", false) THEN
|
||||
SELECT coalesce(max(isdenied), 0) INTO v_reference_denied
|
||||
FROM denied_process
|
||||
WHERE portfolio_id=d."portfolioId" AND process_id=15 AND isdenied=1;
|
||||
IF v_reference_denied=0 THEN
|
||||
INSERT INTO refrencecalling(uuid)
|
||||
SELECT v_uuid WHERE NOT EXISTS (SELECT 1 FROM refrencecalling WHERE uuid=v_uuid);
|
||||
ELSE
|
||||
UPDATE temp_reporting SET refv=0 WHERE uuid=v_uuid;
|
||||
END IF;
|
||||
END IF;
|
||||
IF coalesce(d."propertyVerification", false) THEN
|
||||
INSERT INTO property(uuid)
|
||||
SELECT v_uuid WHERE NOT EXISTS (SELECT 1 FROM property WHERE uuid=v_uuid);
|
||||
END IF;
|
||||
END IF;
|
||||
v_operation := 'UPDATE';
|
||||
END IF;
|
||||
|
||||
IF coalesce(d."autoCutOff", false) THEN
|
||||
v_batch := CASE WHEN localtime <= time '14:30' THEN 'AM' ELSE 'PM' END;
|
||||
IF d."portfolioId" IN (68,129) THEN v_earlier := -2; END IF;
|
||||
UPDATE main_operations SET allocation=1, sms=1, telesheet=-2,
|
||||
earlier=v_earlier, negative=v_earlier, cutoffby=p_user_id,
|
||||
islocked=p_user_id, batch=v_batch, cutoffon=v_now
|
||||
WHERE (cutoffby IS NULL OR cutoffby=0) AND uuid=v_uuid;
|
||||
END IF;
|
||||
|
||||
RETURN QUERY SELECT v_operation, v_case_id, v_mvcode, v_uuid;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.save_application_details(jsonb,smallint,smallint,smallint) FROM PUBLIC;
|
||||
GRANT EXECUTE ON FUNCTION public.save_application_details(jsonb,smallint,smallint,smallint) TO postgres;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user