Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

870: Support TPP testing VRP periodic limits breaches #498

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ ResponseEntity<OBDomesticVRPDetails> domesticVrpPaymentDetailsGet(
* @param xFapiCustomerIpAddress The PSU&#39;s IP address if the PSU is currently logged in with the TPP. (optional)
* @param xFapiInteractionId An RFC4122 UID used as a correlation id. (optional)
* @param xCustomerUserAgent Indicates the user-agent that the PSU is using. (optional)
* @param xVrpLimitBreachResponseSimulation Custom header used to simulate a PeriodicLimit breach response for testing purposes. Values should be of the form PeriodType-PeriodAlignment e.g. Year-Calendar. (optional)
* @return Default response (status code 201)
* or Bad request (status code 400)
* or Unauthorized (status code 401)
Expand Down Expand Up @@ -258,6 +259,9 @@ ResponseEntity<OBDomesticVRPResponse> domesticVrpPost(
@ApiParam(value = "Indicates the user-agent that the PSU is using.")
@RequestHeader(value = "x-customer-user-agent", required = false) String xCustomerUserAgent,

@ApiParam(value = "Custom header used to simulate a PeriodicLimit breach response for testing purposes. Values should be of the form PeriodType-PeriodAlignment e.g. Year-Calendar.")
@RequestHeader(value = "x-vrp-limit-breach-response-simulation", required = false) String xVrpLimitBreachResponseSimulation,

HttpServletRequest request,

Principal principal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,14 @@ public ResponseEntity<OBDomesticVRPDetails> domesticVrpPaymentDetailsGet(
*
* @ApiParam(value = "Indicates the user-agent that the PSU is using.")
* @RequestHeader(value = "x-customer-user-agent", required = false) String xCustomerUserAgent,
*
* @ApiParam(value = "Custom header used to simulate a PeriodicLimit breach response for testing purposes. Values should be of the form PeriodType-PeriodAlignment e.g. Year-Calendar.")
* @RequestHeader(value = "x-vrp-limit-breach-response-simulation", required = false) String xVrpLimitBreachResponseSimulation
*/
public ResponseEntity<OBDomesticVRPResponse> domesticVrpPost(
String authorization, String xJwsSignature, OBDomesticVRPRequest obDomesticVRPRequest, String xFapiAuthDate,
String xFapiCustomerIpAddress, String xFapiInteractionId, String xCustomerUserAgent,
String xVrpLimitBreachResponseSimulation,
HttpServletRequest request, Principal principal
) throws OBErrorResponseException {
log.debug("domesticVrpPost() Recieved OBDomesticVrpRequest {}", obDomesticVRPRequest);
Expand All @@ -143,7 +147,7 @@ public ResponseEntity<OBDomesticVRPResponse> domesticVrpPost(
f.validateRisk(obDomesticVRPRequest.getRisk());
f.checkRequestAndConsentInitiationMatch(initiation, consent);
f.checkRequestAndConsentRiskMatch(obDomesticVRPRequest, consent);
f.checkControlParameters(obDomesticVRPRequest, consent);
f.checkControlParameters(obDomesticVRPRequest, consent, xVrpLimitBreachResponseSimulation);
f.checkCreditorAccountIsInInstructionIfNotInConsent(obDomesticVRPRequest, consent);
});
ResponseEntity responseEntity = vrpPaymentsEndpointWrapper.execute((String tppId) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ ResponseEntity<OBDomesticVRPDetails> domesticVrpPaymentDetailsGet(
* @param xFapiCustomerIpAddress The PSU&#39;s IP address if the PSU is currently logged in with the TPP. (optional)
* @param xFapiInteractionId An RFC4122 UID used as a correlation id. (optional)
* @param xCustomerUserAgent Indicates the user-agent that the PSU is using. (optional)
* @param xVrpLimitBreachResponseSimulation Custom header used to simulate a PeriodicLimit breach response for testing purposes. Values should be of the form PeriodType-PeriodAlignment e.g. Year-Calendar. (optional)
* @return Default response (status code 201)
* or Bad request (status code 400)
* or Unauthorized (status code 401)
Expand Down Expand Up @@ -258,6 +259,9 @@ ResponseEntity<OBDomesticVRPResponse> domesticVrpPost(
@ApiParam(value = "Indicates the user-agent that the PSU is using.")
@RequestHeader(value = "x-customer-user-agent", required = false) String xCustomerUserAgent,

@ApiParam(value = "Custom header used to simulate a PeriodicLimit breach response for testing purposes. Values should be of the form PeriodType-PeriodAlignment e.g. Year-Calendar.")
@RequestHeader(value = "x-vrp-limit-breach-response-simulation", required = false) String xVrpLimitBreachResponseSimulation,

HttpServletRequest request,

Principal principal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,14 @@ public ResponseEntity<OBDomesticVRPDetails> domesticVrpPaymentDetailsGet(
*
* @ApiParam(value = "Indicates the user-agent that the PSU is using.")
* @RequestHeader(value = "x-customer-user-agent", required = false) String xCustomerUserAgent,
*
* @ApiParam(value = "Custom header used to simulate a PeriodicLimit breach response for testing purposes. Values should be of the form PeriodType-PeriodAlignment e.g. Year-Calendar.")
* @RequestHeader(value = "x-vrp-limit-breach-response-simulation", required = false) String xVrpLimitBreachResponseSimulation
*/
public ResponseEntity<OBDomesticVRPResponse> domesticVrpPost(
String authorization, String xJwsSignature, OBDomesticVRPRequest obDomesticVRPRequest, String xFapiAuthDate,
String xFapiCustomerIpAddress, String xFapiInteractionId, String xCustomerUserAgent,
String xVrpLimitBreachResponseSimulation,
HttpServletRequest request, Principal principal
) throws OBErrorResponseException {
log.debug("domesticVrpPost() Recieved OBDomesticVrpRequest {}", obDomesticVRPRequest);
Expand All @@ -144,7 +148,7 @@ public ResponseEntity<OBDomesticVRPResponse> domesticVrpPost(
f.validateRisk(obDomesticVRPRequest.getRisk());
f.checkRequestAndConsentInitiationMatch(initiation, consent);
f.checkRequestAndConsentRiskMatch(obDomesticVRPRequest, consent);
f.checkControlParameters(obDomesticVRPRequest, consent);
f.checkControlParameters(obDomesticVRPRequest, consent, xVrpLimitBreachResponseSimulation);
f.checkCreditorAccountIsInInstructionIfNotInConsent(obDomesticVRPRequest, consent);
});
ResponseEntity responseEntity = vrpPaymentsEndpointWrapper.execute((String tppId) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
import com.forgerock.openbanking.aspsp.rs.wrappper.RSEndpointWrapperService;
import com.forgerock.openbanking.common.model.openbanking.persistence.vrp.FRDomesticVRPConsent;
import com.forgerock.openbanking.common.model.openbanking.persistence.vrp.FRDomesticVRPControlParameters;
import com.forgerock.openbanking.common.model.openbanking.persistence.vrp.FRPeriodicLimits;
import com.forgerock.openbanking.common.model.openbanking.persistence.vrp.FRPeriodicLimits.PeriodAlignmentEnum;
import com.forgerock.openbanking.common.model.openbanking.persistence.vrp.FRPeriodicLimits.PeriodTypeEnum;
import com.forgerock.openbanking.common.model.openbanking.persistence.vrp.FRWriteDomesticVRPDataInitiation;
import com.forgerock.openbanking.common.services.store.tpp.TppStoreService;
import com.forgerock.openbanking.constants.OIDCConstants;
Expand All @@ -39,7 +42,10 @@
import uk.org.openbanking.datamodel.vrp.OBDomesticVRPRequest;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.forgerock.openbanking.common.services.openbanking.converter.vrp.FRDomesticVRPConsentConverter.toOBDomesticVRPInitiation;
import static com.forgerock.openbanking.common.services.openbanking.converter.vrp.FRDomesticVRPConsentConverter.toOBRisk1;
Expand All @@ -51,6 +57,7 @@ public class DomesticVrpPaymentsEndpointWrapper extends RSEndpointWrapper<Domest
private final OBRisk1Validator riskValidator;
private boolean isAuthorizationCodeGrantType;
private boolean isModeTest;
private final PeriodicLimitBreachResponseSimulator periodicLimitBreachResponseSimulator;

public DomesticVrpPaymentsEndpointWrapper(RSEndpointWrapperService RSEndpointWrapperService,
TppStoreService tppStoreService,
Expand All @@ -59,6 +66,7 @@ public DomesticVrpPaymentsEndpointWrapper(RSEndpointWrapperService RSEndpointWra
this.riskValidator = riskValidator;
this.isAuthorizationCodeGrantType = false;
this.isModeTest = false;
this.periodicLimitBreachResponseSimulator = new PeriodicLimitBreachResponseSimulator();
}

public DomesticVrpPaymentsEndpointWrapper payment(FRDomesticVRPConsent consent) {
Expand Down Expand Up @@ -135,14 +143,21 @@ public void checkCreditorAccountIsInInstructionIfNotInConsent(OBDomesticVRPReque
}
}

// When a payment would breach a limitation set by one or more ControlParameters,
// the ASPSP must return an error with code UK.OBIE.Rules.FailsControlParameters
// and pass in the control parameter field that caused the error
public void checkControlParameters(
OBDomesticVRPRequest vrpRequest, FRDomesticVRPConsent frConsent
) throws OBErrorException {
/**
* When a payment would breach a limitation set by one or more ControlParameters, the ASPSP must return an error
* with code UK.OBIE.Rules.FailsControlParameters and pass in the control parameter field that caused the error.
*
* The following checks are supported:
* - validating that the requested payment amount is less than the configured maximum individual amount
* - Optionally, simulating a periodic payment limit breach, if xVrpLimitBreachResponseSimulation param is supplied.
*/
public void checkControlParameters(OBDomesticVRPRequest vrpRequest, FRDomesticVRPConsent frConsent,
String xVrpLimitBreachResponseSimulation) throws OBErrorException {
// TODO Shall we validate the instructed amount against the control parameter periodic limits?
validateMaximumIndividualAmount(vrpRequest, frConsent);
if (xVrpLimitBreachResponseSimulation != null) {
periodicLimitBreachResponseSimulator.processRequest(xVrpLimitBreachResponseSimulation, consent);
}
}

private void validateMaximumIndividualAmount(
Expand All @@ -163,4 +178,56 @@ public interface DomesticVrpPaymentRestEndpointContent {
ResponseEntity run(String tppId) throws OBErrorException;
}

/**
* Simulates VRP payment breaches for PeriodicLimits specified in the consent.
*
* This allows TPPs to test their error handling for this condition, the simulator can be triggered by specifying
* a custom header on the payment request.
*/
static class PeriodicLimitBreachResponseSimulator {
private static final Set<String> LIMIT_BREACH_HEADER_VALUES;
static {
final Set<String> limitBreaches = new HashSet<>();
for (PeriodTypeEnum periodType : PeriodTypeEnum.values()) {
for (PeriodAlignmentEnum periodAlignment : PeriodAlignmentEnum.values()) {
limitBreaches.add(periodType.getValue() + "-" + periodAlignment.getValue());
}
}
LIMIT_BREACH_HEADER_VALUES = Collections.unmodifiableSet(limitBreaches);
}

void processRequest(String xVrpLimitBreachResponseSimulation, FRDomesticVRPConsent consent) throws OBErrorException {
if (LIMIT_BREACH_HEADER_VALUES.contains(xVrpLimitBreachResponseSimulation)) {
final FRPeriodicLimits periodicLimits = findPeriodicLimitsForHeader(xVrpLimitBreachResponseSimulation, consent);
simulateLimitBreachResponse(periodicLimits);
} else {
throw new OBErrorException(OBRIErrorType.REQUEST_VRP_LIMIT_BREACH_SIMULATION_INVALID_HEADER_VALUE,
xVrpLimitBreachResponseSimulation);
}
}

private FRPeriodicLimits findPeriodicLimitsForHeader(String xVrpLimitBreachResponseSimulation,
FRDomesticVRPConsent consent) throws OBErrorException {
final List<FRPeriodicLimits> periodicLimits = consent.getVrpDetails().getData().getControlParameters().getPeriodicLimits();
if (periodicLimits != null) {
final int separatorIndex = xVrpLimitBreachResponseSimulation.indexOf('-');
final String periodType = xVrpLimitBreachResponseSimulation.substring(0, separatorIndex);
final String periodAlignment = xVrpLimitBreachResponseSimulation.substring(separatorIndex + 1);
for (FRPeriodicLimits periodicLimit : periodicLimits) {
if (periodicLimit.getPeriodAlignment().getValue().equals(periodAlignment)
&& periodicLimit.getPeriodType().getValue().equals(periodType)) {
return periodicLimit;
}
}
}
throw new OBErrorException(OBRIErrorType.REQUEST_VRP_LIMIT_BREACH_SIMULATION_NO_MATCHING_LIMIT_IN_CONSENT,
xVrpLimitBreachResponseSimulation);
}

private void simulateLimitBreachResponse(FRPeriodicLimits periodicLimits) throws OBErrorException {
throw new OBErrorException(OBRIErrorType.REQUEST_VRP_CONTROL_PARAMETERS_PAYMENT_PERIODIC_LIMIT_BREACH,
periodicLimits.getAmount(), periodicLimits.getCurrency(),
periodicLimits.getPeriodType(), periodicLimits.getPeriodAlignment());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public void createVrpPaymentConsent() throws Exception {
// When
ResponseEntity<OBDomesticVRPResponse> response = domesticVrpController.domesticVrpPost("test", "test_sig",
obDomesticVRPRequest, new DateTime().toString(),
"127..0.0.1", "x-fapi-interaction_id", "user-agent", request, principal);
"127..0.0.1", "x-fapi-interaction_id", "user-agent", null, request, principal);

// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
Expand Down
Loading