diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index 582a3aff9..8297bb1f7 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -5515,6 +5515,32 @@ object SwaggerDefinitionsJSON { val consumersJsonV510 = ConsumersJsonV510( List(consumerJsonV510) ) + + val agentIdJson = AgentIdJson("") + + val transactionRequestBodyAgentJsonV510 = TransactionRequestBodyAgentJsonV510( + to = agentIdJson, + value = amountOfMoneyJsonV121, + description = descriptionExample.value, + charge_policy = chargePolicyExample.value, + future_date = Some(futureDateExample.value) + ) + + val postAgentJsonV510 = PostAgentJsonV510( + legal_name = legalNameExample.value, + mobile_phone_number = mobilePhoneNumberExample.value, + agent_number = agentNumberExample.value, + currency = currencyExample.value + ) + + val agentJsonV510 = AgentJsonV510( + agent_id = agentIdExample.value, + legal_name = legalNameExample.value, + mobile_phone_number = mobilePhoneNumberExample.value, + agent_number = agentNumberExample.value, + currency = currencyExample.value + ) + //The common error or success format. //Just some helper format to use in Json case class NotSupportedYet() diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 3aaa5eb97..91defb78f 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -104,6 +104,12 @@ object ApiRole extends MdcLoggable{ case class CanCreateCustomer(requiresBankId: Boolean = true) extends ApiRole lazy val canCreateCustomer = CanCreateCustomer() + case class CanCreateAgent(requiresBankId: Boolean = true) extends ApiRole + lazy val canCreateAgent = CanCreateAgent() + + case class CanGetAgent(requiresBankId: Boolean = true) extends ApiRole + lazy val canGetAgent = CanGetAgent() + case class CanUpdateCustomerEmail(requiresBankId: Boolean = true) extends ApiRole lazy val canUpdateCustomerEmail = CanUpdateCustomerEmail() @@ -134,6 +140,9 @@ object ApiRole extends MdcLoggable{ case class CanCreateCustomerAtAnyBank(requiresBankId: Boolean = false) extends ApiRole lazy val canCreateCustomerAtAnyBank = CanCreateCustomerAtAnyBank() + case class CanCreateAgentAtAnyBank(requiresBankId: Boolean = false) extends ApiRole + lazy val canCreateAgentAtAnyBank = CanCreateAgentAtAnyBank() + case class CanGetCorrelatedUsersInfo(requiresBankId: Boolean = true) extends ApiRole lazy val canGetCorrelatedUsersInfo = CanGetCorrelatedUsersInfo() diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 3fc845f73..7006390be 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -452,6 +452,7 @@ object ErrorMessages { val UpdateProductFeeError = "OBP-30119: Could not update the Product Fee." val InvalidCardNumber = "OBP-30200: Card not found. Please specify a valid value for CARD_NUMBER. " + val AgentNotFound = "OBP-30201: Agent not found. Please specify a valid value for AGENT_ID. " val CustomerAccountLinkNotFound = "OBP-30204: Customer Account Link not found" @@ -523,6 +524,7 @@ object ErrorMessages { val GetChargeValueError = "OBP-30323: Could not get the Charge Value." val GetTransactionRequestTypeChargesError = "OBP-30324: Could not get Transaction Request Type Charges." + val AgentAccountLinkNotFound = "OBP-30325: Agent Account Link not found." // Branch related messages val BranchesNotFoundLicense = "OBP-32001: No branches available. License may not be set." diff --git a/obp-api/src/main/scala/code/api/util/ExampleValue.scala b/obp-api/src/main/scala/code/api/util/ExampleValue.scala index 8d09a2ec3..9d32c4727 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -65,6 +65,9 @@ object ExampleValue { lazy val customerIdExample = ConnectorField("7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh", s"A non human friendly string that identifies the customer and is used in URLs. This SHOULD NOT be the customer number. The combination of customerId and bankId MUST be unique on an OBP instance. customerId SHOULD be unique on an OBP instance. Ideally customerId is a UUID. A mapping between customer number and customer id is kept in OBP.") glossaryItems += makeGlossaryItem("Customer.customerId", customerIdExample) + + lazy val agentIdExample = ConnectorField("7uy8a7e4-6d02-40e3-a129-0b2bf89de8uh", s"A non human friendly string that identifies the agent and is used in URLs. This SHOULD NOT be the agent number. The combination of agentId and bankId MUST be unique on an OBP instance. AgentId SHOULD be unique on an OBP instance. Ideally agentId is a UUID. A mapping between agent number and agent id is kept in OBP.") + glossaryItems += makeGlossaryItem("Agent.agent_id", agentIdExample) lazy val customerAccountLinkIdExample = ConnectorField("xyz8a7e4-6d02-40e3-a129-0b2bf89de8uh", s"A non human friendly string that identifies the Customer Account Link and is used in URLs. ") glossaryItems += makeGlossaryItem("Customer.customerAccountLinkId", customerAccountLinkIdExample) @@ -114,6 +117,9 @@ object ExampleValue { lazy val customerNumberExample = ConnectorField("5987953", s"The human friendly customer identifier that MUST uniquely identify the Customer at the Bank ID. Customer Number is NOT used in URLs.") glossaryItems += makeGlossaryItem("Customer.customerNumber", customerNumberExample) + lazy val agentNumberExample = ConnectorField("5987953", s"The human friendly agent identifier that MUST uniquely identify the Agent at the Bank ID. Agent Number is NOT used in URLs.") + glossaryItems += makeGlossaryItem("Agent.agent_number", agentNumberExample) + lazy val licenseIdExample = ConnectorField("ODbL-1.0", s"") glossaryItems += makeGlossaryItem("License.id", licenseIdExample) diff --git a/obp-api/src/main/scala/code/api/util/NewStyle.scala b/obp-api/src/main/scala/code/api/util/NewStyle.scala index 342a6584a..80342ac23 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -748,6 +748,12 @@ object NewStyle extends MdcLoggable{ unboxFullOrFail(_, callContext, s"$CustomerNotFoundByCustomerId. Current CustomerId($customerId)", 404) } } + + def getAgentByAgentId(agentId : String, callContext: Option[CallContext]): OBPReturnType[Customer] = { + Connector.connector.vend.getCustomerByCustomerId(agentId, callContext) map { + unboxFullOrFail(_, callContext, s"$AgentNotFound. Current AGENT_ID($agentId)", 404) + } + } def checkCustomerNumberAvailable(bankId: BankId, customerNumber: String, callContext: Option[CallContext]): OBPReturnType[Boolean] = { Connector.connector.vend.checkCustomerNumberAvailable(bankId: BankId, customerNumber: String, callContext: Option[CallContext]) map { i => (unboxFullOrFail(i._1, callContext, s"$InvalidConnectorResponse", 400), i._2) diff --git a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala index d3be8f537..d8eee9ed1 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/APIMethods400.scala @@ -44,7 +44,9 @@ import code.api.dynamic.endpoint.helper._ import code.api.dynamic.endpoint.helper.practise.PractiseEndpoint import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo} import code.api.util.FutureUtil.EndpointContext +import code.api.v4_0_0.APIMethods400.{createTransactionRequest, exchangeRates, lowAmount, sharedChargePolicy, transactionRequestGeneralText} import code.api.v5_0_0.OBPAPI5_0_0 +import code.api.v5_1_0.TransactionRequestBodyAgentJsonV510 import code.api.{ChargePolicy, Constant, JsonResponseException} import code.apicollection.MappedApiCollectionsProvider import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider @@ -595,71 +597,6 @@ trait APIMethods400 extends MdcLoggable { } } - val exchangeRates = - APIUtil.getPropsValue("webui_api_explorer_url", "") + - "/more?version=OBPv4.0.0&list-all-banks=false&core=&psd2=&obwg=#OBPv2_2_0-getCurrentFxRate" - - - // This text is used in the various Create Transaction Request resource docs - val transactionRequestGeneralText = - s"""Initiate a Payment via creating a Transaction Request. - | - |In OBP, a `transaction request` may or may not result in a `transaction`. However, a `transaction` only has one possible state: completed. - | - |A `Transaction Request` can have one of several states: INITIATED, NEXT_CHALLENGE_PENDING etc. - | - |`Transactions` are modeled on items in a bank statement that represent the movement of money. - | - |`Transaction Requests` are requests to move money which may or may not succeed and thus result in a `Transaction`. - | - |A `Transaction Request` might create a security challenge that needs to be answered before the `Transaction Request` proceeds. - |In case 1 person needs to answer security challenge we have next flow of state of an `transaction request`: - | INITIATED => COMPLETED - |In case n persons needs to answer security challenge we have next flow of state of an `transaction request`: - | INITIATED => NEXT_CHALLENGE_PENDING => ... => NEXT_CHALLENGE_PENDING => COMPLETED - | - |The security challenge is bound to a user i.e. in case of right answer and the user is different than expected one the challenge will fail. - | - |Rule for calculating number of security challenges: - |If product Account attribute REQUIRED_CHALLENGE_ANSWERS=N then create N challenges - |(one for every user that has a View where permission "can_add_transaction_request_to_any_account"=true) - |In case REQUIRED_CHALLENGE_ANSWERS is not defined as an account attribute default value is 1. - | - |Transaction Requests contain charge information giving the client the opportunity to proceed or not (as long as the challenge level is appropriate). - | - |Transaction Requests can have one of several Transaction Request Types which expect different bodies. The escaped body is returned in the details key of the GET response. - |This provides some commonality and one URL for many different payment or transfer types with enough flexibility to validate them differently. - | - |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL. - | - |In sandbox mode, TRANSACTION_REQUEST_TYPE is commonly set to ACCOUNT. See getTransactionRequestTypesSupportedByBank for all supported types. - | - |In sandbox mode, if the amount is less than 1000 EUR (any currency, unless it is set differently on this server), the transaction request will create a transaction without a challenge, else the Transaction Request will be set to INITIALISED and a challenge will need to be answered. - | - |If a challenge is created you must answer it using Answer Transaction Request Challenge before the Transaction is created. - | - |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account. - | - |The following static FX rates are available in sandbox mode: - | - |${exchangeRates} - | - | - |Transaction Requests satisfy PSD2 requirements thus: - | - |1) A transaction can be initiated by a third party application. - | - |2) The customer is informed of the charge that will incurred. - | - |3) The call supports delegated authentication (OAuth) - | - |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow. - | - |There is further documentation [here](https://github.com/OpenBankProject/OBP-API/wiki/Transaction-Requests) - | - |""" - - // ACCOUNT. (we no longer create a resource doc for the general case) staticResourceDocs += ResourceDoc( createTransactionRequestAccount, @@ -807,10 +744,6 @@ trait APIMethods400 extends MdcLoggable { ), List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2)) - - val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50") - val sharedChargePolicy = ChargePolicy.withName("SHARED") - // Transaction Request (SEPA) staticResourceDocs += ResourceDoc( createTransactionRequestSepa, @@ -925,443 +858,6 @@ trait APIMethods400 extends MdcLoggable { ), List(apiTagTransactionRequest, apiTagPSD2PIS), Some(List(canCreateAnyTransactionRequest))) - - - def createTransactionRequest(bankId: BankId, accountId: AccountId, viewId: ViewId, transactionRequestType: TransactionRequestType, json: JValue): Future[(TransactionRequestWithChargeJSON400, Option[CallContext])] = { - for { - (Full(u), callContext) <- SS.user - - transactionRequestTypeValue <- NewStyle.function.tryons(s"$InvalidTransactionRequestType: '${transactionRequestType.value}'. OBP does not support it.", 400, callContext) { - TransactionRequestTypes.withName(transactionRequestType.value) - } - - (fromAccount, callContext) <- transactionRequestTypeValue match { - case CARD => - for{ - transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { - json.extract[TransactionRequestBodyCardJsonV400] - } - // 1.1 get Card from card_number - (cardFromCbs,callContext) <- NewStyle.function.getPhysicalCardByCardNumber(transactionRequestBodyCard.card.card_number, callContext) - - // 1.2 check card name/expire month. year. - calendar = Calendar.getInstance - _ = calendar.setTime(cardFromCbs.expires) - yearFromCbs = calendar.get(Calendar.YEAR).toString - monthFromCbs = calendar.get(Calendar.MONTH).toString - nameOnCardFromCbs= cardFromCbs.nameOnCard - cvvFromCbs= cardFromCbs.cvv.getOrElse("") - brandFromCbs= cardFromCbs.brand.getOrElse("") - - _ <- Helper.booleanToFuture(s"$InvalidJsonValue brand is not matched", cc=callContext) { - transactionRequestBodyCard.card.brand.equalsIgnoreCase(brandFromCbs) - } - - dateFromJsonBody <- NewStyle.function.tryons(s"$InvalidDateFormat year should be 'yyyy', " + - s"eg: 2023, but current expiry_year(${transactionRequestBodyCard.card.expiry_year}), " + - s"month should be 'xx', eg: 02, but current expiry_month(${transactionRequestBodyCard.card.expiry_month})", 400, callContext) { - DateWithMonthFormat.parse(s"${transactionRequestBodyCard.card.expiry_year}-${transactionRequestBodyCard.card.expiry_month}") - } - _ <- Helper.booleanToFuture(s"$InvalidJsonValue your credit card is expired.", cc=callContext) { - org.apache.commons.lang3.time.DateUtils.addMonths(new Date(), 1).before(dateFromJsonBody) - } - - _ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_year is not matched", cc=callContext) { - transactionRequestBodyCard.card.expiry_year.equalsIgnoreCase(yearFromCbs) - } - _ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_month is not matched", cc=callContext) { - transactionRequestBodyCard.card.expiry_month.toInt.equals(monthFromCbs.toInt+1) - } - - _ <- Helper.booleanToFuture(s"$InvalidJsonValue name_on_card is not matched", cc=callContext) { - transactionRequestBodyCard.card.name_on_card.equalsIgnoreCase(nameOnCardFromCbs) - } - _ <- Helper.booleanToFuture(s"$InvalidJsonValue cvv is not matched", cc=callContext) { - HashUtil.Sha256Hash(transactionRequestBodyCard.card.cvv).equals(cvvFromCbs) - } - - } yield{ - (cardFromCbs.account, callContext) - } - - case _ => NewStyle.function.getBankAccount(bankId,accountId, callContext) - } - _ <- NewStyle.function.isEnabledTransactionRequests(callContext) - _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext) { - isValidID(fromAccount.accountId.value) - } - _ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext) { - isValidID(fromAccount.bankId.value) - } - - _ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), u, callContext) - - _ <- Helper.booleanToFuture(s"${InvalidTransactionRequestType}: '${transactionRequestType.value}'. Current Sandbox does not support it. ", cc=callContext) { - APIUtil.getPropsValue("transactionRequests_supported_types", "").split(",").contains(transactionRequestType.value) - } - - // Check the input JSON format, here is just check the common parts of all four types - transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) { - json.extract[TransactionRequestBodyCommonJSON] - } - - transactionAmountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) { - BigDecimal(transDetailsJson.value.amount) - } - - _ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${transactionAmountNumber}'", cc=callContext) { - transactionAmountNumber > BigDecimal("0") - } - - _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) { - isValidCurrencyISOCode(transDetailsJson.value.currency) - } - - // Prevent default value for transaction request type (at least). - _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) { - isValidCurrencyISOCode(transDetailsJson.value.currency) - } - - (createdTransactionRequest, callContext) <- transactionRequestTypeValue match { - case REFUND => { - for { - transactionRequestBodyRefundJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { - json.extract[TransactionRequestBodyRefundJsonV400] - } - - transactionId = TransactionId(transactionRequestBodyRefundJson.refund.transaction_id) - - (fromAccount, toAccount, transaction, callContext) <- transactionRequestBodyRefundJson.to match { - case Some(refundRequestTo) if refundRequestTo.account_id.isDefined && refundRequestTo.bank_id.isDefined => - val toBankId = BankId(refundRequestTo.bank_id.get) - val toAccountId = AccountId(refundRequestTo.account_id.get) - for { - (transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext) - (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - } yield (fromAccount, toAccount, transaction, callContext) - - case Some(refundRequestTo) if refundRequestTo.counterparty_id.isDefined => - val toCounterpartyId = CounterpartyId(refundRequestTo.counterparty_id.get) - for { - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(toCounterpartyId, callContext) - toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, isOutgoingAccount = true, callContext) - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - (transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext) - } yield (fromAccount, toAccount, transaction, callContext) - - case None if transactionRequestBodyRefundJson.from.isDefined => - val fromCounterpartyId = CounterpartyId(transactionRequestBodyRefundJson.from.get.counterparty_id) - val toAccount = fromAccount - for { - (fromCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(fromCounterpartyId, callContext) - fromAccount <- NewStyle.function.getBankAccountFromCounterparty(fromCounterparty, isOutgoingAccount = false, callContext) - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - fromCounterparty.isBeneficiary - } - (transaction, callContext) <- NewStyle.function.getTransaction(toAccount.bankId, toAccount.accountId, transactionId, callContext) - } yield (fromAccount, toAccount, transaction, callContext) - } - - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyRefundJson)(Serialization.formats(NoTypeHints)) - } - - _ <- Helper.booleanToFuture(s"${RefundedTransaction} Current input amount is: '${transDetailsJson.value.amount}'. It can not be more than the original amount(${(transaction.amount).abs})", cc=callContext) { - (transaction.amount).abs >= transactionAmountNumber - } - //TODO, we need additional field to guarantee the transaction is refunded... - // _ <- Helper.booleanToFuture(s"${RefundedTransaction}") { - // !((transaction.description.toString contains(" Refund to ")) && (transaction.description.toString contains(" and transaction_id("))) - // } - - //we add the extra info (counterparty name + transaction_id) for this special Refund endpoint. - newDescription = s"${transactionRequestBodyRefundJson.description} - Refund for transaction_id: (${transactionId.value}) to ${transaction.otherAccount.counterpartyName}" - - //This is the refund endpoint, the original fromAccount is the `toAccount` which will receive money. - refundToAccount = fromAccount - //This is the refund endpoint, the original toAccount is the `fromAccount` which will lose money. - refundFromAccount = toAccount - - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - refundFromAccount, - refundToAccount, - transactionRequestType, - transactionRequestBodyRefundJson.copy(description = newDescription), - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) //in ACCOUNT, ChargePolicy set default "SHARED" - - _ <- NewStyle.function.createOrUpdateTransactionRequestAttribute( - bankId = bankId, - transactionRequestId = createdTransactionRequest.id, - transactionRequestAttributeId = None, - name = "original_transaction_id", - attributeType = TransactionRequestAttributeType.withName("STRING"), - value = transactionId.value, - callContext = callContext - ) - - refundReasonCode = transactionRequestBodyRefundJson.refund.reason_code - _ <- if (refundReasonCode.nonEmpty) { - NewStyle.function.createOrUpdateTransactionRequestAttribute( - bankId = bankId, - transactionRequestId = createdTransactionRequest.id, - transactionRequestAttributeId = None, - name = "refund_reason_code", - attributeType = TransactionRequestAttributeType.withName("STRING"), - value = refundReasonCode, - callContext = callContext) - } else Future.successful() - - (newTransactionRequestStatus, callContext) <- NewStyle.function.notifyTransactionRequest(refundFromAccount, refundToAccount, createdTransactionRequest, callContext) - _ <- NewStyle.function.saveTransactionRequestStatusImpl(createdTransactionRequest.id, newTransactionRequestStatus.toString, callContext) - createdTransactionRequest <- Future(createdTransactionRequest.copy(status = newTransactionRequestStatus.toString)) - - } yield (createdTransactionRequest, callContext) - } - case ACCOUNT | SANDBOX_TAN => { - for { - transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { - json.extract[TransactionRequestBodySandBoxTanJSON] - } - - toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id) - toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) - (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) - } - - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodySandboxTan, - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) //in ACCOUNT, ChargePolicy set default "SHARED" - } yield (createdTransactionRequest, callContext) - } - case ACCOUNT_OTP => { - for { - transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { - json.extract[TransactionRequestBodySandBoxTanJSON] - } - - toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id) - toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) - (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) - - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) - } - - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodySandboxTan, - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) //in ACCOUNT, ChargePolicy set default "SHARED" - } yield (createdTransactionRequest, callContext) - } - case COUNTERPARTY => { - for { - //For COUNTERPARTY, Use the counterpartyId to find the toCounterparty and set up the toAccount - transactionRequestBodyCounterparty <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) { - json.extract[TransactionRequestBodyCounterpartyJSON] - } - toCounterpartyId = transactionRequestBodyCounterparty.to.counterparty_id - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext) - toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - // Check we can send money to it. - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = transactionRequestBodyCounterparty.charge_policy - _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { - ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) - } - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodyCounterparty, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield (createdTransactionRequest, callContext) - } - case CARD => { - for { - //2rd: get toAccount from counterpartyId - transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { - json.extract[TransactionRequestBodyCardJsonV400] - } - toCounterpartyId = transactionRequestBodyCard.to.counterparty_id - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext) - toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - // Check we can send money to it. - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = ChargePolicy.RECEIVER.toString - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyCard)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodyCard, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield (createdTransactionRequest, callContext) - - } - case SIMPLE => { - for { - //For SAMPLE, we will create/get toCounterparty on site and set up the toAccount - transactionRequestBodySimple <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SIMPLE json format", 400, callContext) { - json.extract[TransactionRequestBodySimpleJsonV400] - } - (toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty( - name = transactionRequestBodySimple.to.name, - description = transactionRequestBodySimple.to.description, - currency = transactionRequestBodySimple.value.currency, - createdByUserId = u.userId, - thisBankId = bankId.value, - thisAccountId = accountId.value, - thisViewId = viewId.value, - otherBankRoutingScheme = transactionRequestBodySimple.to.other_bank_routing_scheme, - otherBankRoutingAddress = transactionRequestBodySimple.to.other_bank_routing_address, - otherBranchRoutingScheme = transactionRequestBodySimple.to.other_branch_routing_scheme, - otherBranchRoutingAddress = transactionRequestBodySimple.to.other_branch_routing_address, - otherAccountRoutingScheme = transactionRequestBodySimple.to.other_account_routing_scheme, - otherAccountRoutingAddress = transactionRequestBodySimple.to.other_account_routing_address, - otherAccountSecondaryRoutingScheme = transactionRequestBodySimple.to.other_account_secondary_routing_scheme, - otherAccountSecondaryRoutingAddress = transactionRequestBodySimple.to.other_account_secondary_routing_address, - callContext: Option[CallContext], - ) - toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - // Check we can send money to it. - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = transactionRequestBodySimple.charge_policy - _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { - ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) - } - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodySimple)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transactionRequestBodySimple, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield (createdTransactionRequest, callContext) - - } - case SEPA => { - for { - //For SEPA, Use the IBAN to find the toCounterparty and set up the toAccount - transDetailsSEPAJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SEPA json format", 400, callContext) { - json.extract[TransactionRequestBodySEPAJsonV400] - } - toIban = transDetailsSEPAJson.to.iban - (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByIbanAndBankAccountId(toIban, fromAccount.bankId, fromAccount.accountId, callContext) - toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) - _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { - toCounterparty.isBeneficiary - } - chargePolicy = transDetailsSEPAJson.charge_policy - _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { - ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) - } - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - toAccount, - transactionRequestType, - transDetailsSEPAJson, - transDetailsSerialized, - chargePolicy, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - transDetailsSEPAJson.reasons.map(_.map(_.transform)), - callContext) - } yield (createdTransactionRequest, callContext) - } - case FREE_FORM => { - for { - transactionRequestBodyFreeForm <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $FREE_FORM json format", 400, callContext) { - json.extract[TransactionRequestBodyFreeFormJSON] - } - // Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge' - transactionRequestAccountJSON = TransactionRequestAccountJsonV140(bankId.value, accountId.value) - transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { - write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints)) - } - (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, - viewId, - fromAccount, - fromAccount, - transactionRequestType, - transactionRequestBodyFreeForm, - transDetailsSerialized, - sharedChargePolicy.toString, - Some(OBP_TRANSACTION_REQUEST_CHALLENGE), - getScaMethodAtInstance(transactionRequestType.value).toOption, - None, - callContext) - } yield - (createdTransactionRequest, callContext) - } - } - (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext) - } yield { - (JSONFactory400.createTransactionRequestWithChargeJSON(createdTransactionRequest, challenges), HttpCode.`201`(callContext)) - } - } lazy val createTransactionRequestAccount: OBPEndpoint = { case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: @@ -12718,5 +12214,543 @@ object APIMethods400 extends RestHelper with APIMethods400 { lazy val newStyleEndpoints: List[(String, String)] = Implementations4_0_0.resourceDocs.map { rd => (rd.partialFunctionName, rd.implementedInApiVersion.toString()) }.toList + + val exchangeRates = + APIUtil.getPropsValue("webui_api_explorer_url", "") + + "/more?version=OBPv4.0.0&list-all-banks=false&core=&psd2=&obwg=#OBPv2_2_0-getCurrentFxRate" + + + // This text is used in the various Create Transaction Request resource docs + val transactionRequestGeneralText = + s"""Initiate a Payment via creating a Transaction Request. + | + |In OBP, a `transaction request` may or may not result in a `transaction`. However, a `transaction` only has one possible state: completed. + | + |A `Transaction Request` can have one of several states: INITIATED, NEXT_CHALLENGE_PENDING etc. + | + |`Transactions` are modeled on items in a bank statement that represent the movement of money. + | + |`Transaction Requests` are requests to move money which may or may not succeed and thus result in a `Transaction`. + | + |A `Transaction Request` might create a security challenge that needs to be answered before the `Transaction Request` proceeds. + |In case 1 person needs to answer security challenge we have next flow of state of an `transaction request`: + | INITIATED => COMPLETED + |In case n persons needs to answer security challenge we have next flow of state of an `transaction request`: + | INITIATED => NEXT_CHALLENGE_PENDING => ... => NEXT_CHALLENGE_PENDING => COMPLETED + | + |The security challenge is bound to a user i.e. in case of right answer and the user is different than expected one the challenge will fail. + | + |Rule for calculating number of security challenges: + |If product Account attribute REQUIRED_CHALLENGE_ANSWERS=N then create N challenges + |(one for every user that has a View where permission "can_add_transaction_request_to_any_account"=true) + |In case REQUIRED_CHALLENGE_ANSWERS is not defined as an account attribute default value is 1. + | + |Transaction Requests contain charge information giving the client the opportunity to proceed or not (as long as the challenge level is appropriate). + | + |Transaction Requests can have one of several Transaction Request Types which expect different bodies. The escaped body is returned in the details key of the GET response. + |This provides some commonality and one URL for many different payment or transfer types with enough flexibility to validate them differently. + | + |The payer is set in the URL. Money comes out of the BANK_ID and ACCOUNT_ID specified in the URL. + | + |In sandbox mode, TRANSACTION_REQUEST_TYPE is commonly set to ACCOUNT. See getTransactionRequestTypesSupportedByBank for all supported types. + | + |In sandbox mode, if the amount is less than 1000 EUR (any currency, unless it is set differently on this server), the transaction request will create a transaction without a challenge, else the Transaction Request will be set to INITIALISED and a challenge will need to be answered. + | + |If a challenge is created you must answer it using Answer Transaction Request Challenge before the Transaction is created. + | + |You can transfer between different currency accounts. (new in 2.0.0). The currency in body must match the sending account. + | + |The following static FX rates are available in sandbox mode: + | + |${exchangeRates} + | + | + |Transaction Requests satisfy PSD2 requirements thus: + | + |1) A transaction can be initiated by a third party application. + | + |2) The customer is informed of the charge that will incurred. + | + |3) The call supports delegated authentication (OAuth) + | + |See [this python code](https://github.com/OpenBankProject/Hello-OBP-DirectLogin-Python/blob/master/hello_payments.py) for a complete example of this flow. + | + |There is further documentation [here](https://github.com/OpenBankProject/OBP-API/wiki/Transaction-Requests) + | + |""" + + val lowAmount = AmountOfMoneyJsonV121("EUR", "12.50") + + val sharedChargePolicy = ChargePolicy.withName("SHARED") + + def createTransactionRequest(bankId: BankId, accountId: AccountId, viewId: ViewId, transactionRequestType: TransactionRequestType, json: JValue): Future[(TransactionRequestWithChargeJSON400, Option[CallContext])] = { + for { + (Full(u), callContext) <- SS.user + + transactionRequestTypeValue <- NewStyle.function.tryons(s"$InvalidTransactionRequestType: '${transactionRequestType.value}'. OBP does not support it.", 400, callContext) { + TransactionRequestTypes.withName(transactionRequestType.value) + } + + (fromAccount, callContext) <- transactionRequestTypeValue match { + case CARD => + for{ + transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { + json.extract[TransactionRequestBodyCardJsonV400] + } + // 1.1 get Card from card_number + (cardFromCbs,callContext) <- NewStyle.function.getPhysicalCardByCardNumber(transactionRequestBodyCard.card.card_number, callContext) + + // 1.2 check card name/expire month. year. + calendar = Calendar.getInstance + _ = calendar.setTime(cardFromCbs.expires) + yearFromCbs = calendar.get(Calendar.YEAR).toString + monthFromCbs = calendar.get(Calendar.MONTH).toString + nameOnCardFromCbs= cardFromCbs.nameOnCard + cvvFromCbs= cardFromCbs.cvv.getOrElse("") + brandFromCbs= cardFromCbs.brand.getOrElse("") + + _ <- Helper.booleanToFuture(s"$InvalidJsonValue brand is not matched", cc=callContext) { + transactionRequestBodyCard.card.brand.equalsIgnoreCase(brandFromCbs) + } + + dateFromJsonBody <- NewStyle.function.tryons(s"$InvalidDateFormat year should be 'yyyy', " + + s"eg: 2023, but current expiry_year(${transactionRequestBodyCard.card.expiry_year}), " + + s"month should be 'xx', eg: 02, but current expiry_month(${transactionRequestBodyCard.card.expiry_month})", 400, callContext) { + DateWithMonthFormat.parse(s"${transactionRequestBodyCard.card.expiry_year}-${transactionRequestBodyCard.card.expiry_month}") + } + _ <- Helper.booleanToFuture(s"$InvalidJsonValue your credit card is expired.", cc=callContext) { + org.apache.commons.lang3.time.DateUtils.addMonths(new Date(), 1).before(dateFromJsonBody) + } + + _ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_year is not matched", cc=callContext) { + transactionRequestBodyCard.card.expiry_year.equalsIgnoreCase(yearFromCbs) + } + _ <- Helper.booleanToFuture(s"$InvalidJsonValue expiry_month is not matched", cc=callContext) { + transactionRequestBodyCard.card.expiry_month.toInt.equals(monthFromCbs.toInt+1) + } + + _ <- Helper.booleanToFuture(s"$InvalidJsonValue name_on_card is not matched", cc=callContext) { + transactionRequestBodyCard.card.name_on_card.equalsIgnoreCase(nameOnCardFromCbs) + } + _ <- Helper.booleanToFuture(s"$InvalidJsonValue cvv is not matched", cc=callContext) { + HashUtil.Sha256Hash(transactionRequestBodyCard.card.cvv).equals(cvvFromCbs) + } + + } yield{ + (cardFromCbs.account, callContext) + } + + case _ => NewStyle.function.getBankAccount(bankId,accountId, callContext) + } + _ <- NewStyle.function.isEnabledTransactionRequests(callContext) + _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext) { + isValidID(fromAccount.accountId.value) + } + _ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext) { + isValidID(fromAccount.bankId.value) + } + + _ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, BankIdAccountId(fromAccount.bankId, fromAccount.accountId), u, callContext) + + _ <- Helper.booleanToFuture(s"${InvalidTransactionRequestType}: '${transactionRequestType.value}'. Current Sandbox does not support it. ", cc=callContext) { + APIUtil.getPropsValue("transactionRequests_supported_types", "").split(",").contains(transactionRequestType.value) + } + + // Check the input JSON format, here is just check the common parts of all four types + transDetailsJson <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $TransactionRequestBodyCommonJSON ", 400, callContext) { + json.extract[TransactionRequestBodyCommonJSON] + } + + transactionAmountNumber <- NewStyle.function.tryons(s"$InvalidNumber Current input is ${transDetailsJson.value.amount} ", 400, callContext) { + BigDecimal(transDetailsJson.value.amount) + } + + _ <- Helper.booleanToFuture(s"${NotPositiveAmount} Current input is: '${transactionAmountNumber}'", cc=callContext) { + transactionAmountNumber > BigDecimal("0") + } + + _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) { + isValidCurrencyISOCode(transDetailsJson.value.currency) + } + + // Prevent default value for transaction request type (at least). + _ <- Helper.booleanToFuture(s"${InvalidISOCurrencyCode} Current input is: '${transDetailsJson.value.currency}'", cc=callContext) { + isValidCurrencyISOCode(transDetailsJson.value.currency) + } + + (createdTransactionRequest, callContext) <- transactionRequestTypeValue match { + case REFUND => { + for { + transactionRequestBodyRefundJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { + json.extract[TransactionRequestBodyRefundJsonV400] + } + + transactionId = TransactionId(transactionRequestBodyRefundJson.refund.transaction_id) + + (fromAccount, toAccount, transaction, callContext) <- transactionRequestBodyRefundJson.to match { + case Some(refundRequestTo) if refundRequestTo.account_id.isDefined && refundRequestTo.bank_id.isDefined => + val toBankId = BankId(refundRequestTo.bank_id.get) + val toAccountId = AccountId(refundRequestTo.account_id.get) + for { + (transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext) + (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) + } yield (fromAccount, toAccount, transaction, callContext) + + case Some(refundRequestTo) if refundRequestTo.counterparty_id.isDefined => + val toCounterpartyId = CounterpartyId(refundRequestTo.counterparty_id.get) + for { + (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(toCounterpartyId, callContext) + toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, isOutgoingAccount = true, callContext) + _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { + toCounterparty.isBeneficiary + } + (transaction, callContext) <- NewStyle.function.getTransaction(fromAccount.bankId, fromAccount.accountId, transactionId, callContext) + } yield (fromAccount, toAccount, transaction, callContext) + + case None if transactionRequestBodyRefundJson.from.isDefined => + val fromCounterpartyId = CounterpartyId(transactionRequestBodyRefundJson.from.get.counterparty_id) + val toAccount = fromAccount + for { + (fromCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(fromCounterpartyId, callContext) + fromAccount <- NewStyle.function.getBankAccountFromCounterparty(fromCounterparty, isOutgoingAccount = false, callContext) + _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { + fromCounterparty.isBeneficiary + } + (transaction, callContext) <- NewStyle.function.getTransaction(toAccount.bankId, toAccount.accountId, transactionId, callContext) + } yield (fromAccount, toAccount, transaction, callContext) + } + + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodyRefundJson)(Serialization.formats(NoTypeHints)) + } + + _ <- Helper.booleanToFuture(s"${RefundedTransaction} Current input amount is: '${transDetailsJson.value.amount}'. It can not be more than the original amount(${(transaction.amount).abs})", cc=callContext) { + (transaction.amount).abs >= transactionAmountNumber + } + //TODO, we need additional field to guarantee the transaction is refunded... + // _ <- Helper.booleanToFuture(s"${RefundedTransaction}") { + // !((transaction.description.toString contains(" Refund to ")) && (transaction.description.toString contains(" and transaction_id("))) + // } + + //we add the extra info (counterparty name + transaction_id) for this special Refund endpoint. + newDescription = s"${transactionRequestBodyRefundJson.description} - Refund for transaction_id: (${transactionId.value}) to ${transaction.otherAccount.counterpartyName}" + + //This is the refund endpoint, the original fromAccount is the `toAccount` which will receive money. + refundToAccount = fromAccount + //This is the refund endpoint, the original toAccount is the `fromAccount` which will lose money. + refundFromAccount = toAccount + + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + refundFromAccount, + refundToAccount, + transactionRequestType, + transactionRequestBodyRefundJson.copy(description = newDescription), + transDetailsSerialized, + sharedChargePolicy.toString, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) //in ACCOUNT, ChargePolicy set default "SHARED" + + _ <- NewStyle.function.createOrUpdateTransactionRequestAttribute( + bankId = bankId, + transactionRequestId = createdTransactionRequest.id, + transactionRequestAttributeId = None, + name = "original_transaction_id", + attributeType = TransactionRequestAttributeType.withName("STRING"), + value = transactionId.value, + callContext = callContext + ) + + refundReasonCode = transactionRequestBodyRefundJson.refund.reason_code + _ <- if (refundReasonCode.nonEmpty) { + NewStyle.function.createOrUpdateTransactionRequestAttribute( + bankId = bankId, + transactionRequestId = createdTransactionRequest.id, + transactionRequestAttributeId = None, + name = "refund_reason_code", + attributeType = TransactionRequestAttributeType.withName("STRING"), + value = refundReasonCode, + callContext = callContext) + } else Future.successful() + + (newTransactionRequestStatus, callContext) <- NewStyle.function.notifyTransactionRequest(refundFromAccount, refundToAccount, createdTransactionRequest, callContext) + _ <- NewStyle.function.saveTransactionRequestStatusImpl(createdTransactionRequest.id, newTransactionRequestStatus.toString, callContext) + createdTransactionRequest <- Future(createdTransactionRequest.copy(status = newTransactionRequestStatus.toString)) + + } yield (createdTransactionRequest, callContext) + } + case ACCOUNT | SANDBOX_TAN => { + for { + transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { + json.extract[TransactionRequestBodySandBoxTanJSON] + } + + toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id) + toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) + (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) + + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) + } + + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + toAccount, + transactionRequestType, + transactionRequestBodySandboxTan, + transDetailsSerialized, + sharedChargePolicy.toString, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) //in ACCOUNT, ChargePolicy set default "SHARED" + } yield (createdTransactionRequest, callContext) + } + case ACCOUNT_OTP => { + for { + transactionRequestBodySandboxTan <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $ACCOUNT json format", 400, callContext) { + json.extract[TransactionRequestBodySandBoxTanJSON] + } + + toBankId = BankId(transactionRequestBodySandboxTan.to.bank_id) + toAccountId = AccountId(transactionRequestBodySandboxTan.to.account_id) + (toAccount, callContext) <- NewStyle.function.checkBankAccountExists(toBankId, toAccountId, callContext) + + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodySandboxTan)(Serialization.formats(NoTypeHints)) + } + + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + toAccount, + transactionRequestType, + transactionRequestBodySandboxTan, + transDetailsSerialized, + sharedChargePolicy.toString, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) //in ACCOUNT, ChargePolicy set default "SHARED" + } yield (createdTransactionRequest, callContext) + } + case COUNTERPARTY => { + for { + //For COUNTERPARTY, Use the counterpartyId to find the toCounterparty and set up the toAccount + transactionRequestBodyCounterparty <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $COUNTERPARTY json format", 400, callContext) { + json.extract[TransactionRequestBodyCounterpartyJSON] + } + toCounterpartyId = transactionRequestBodyCounterparty.to.counterparty_id + (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext) + toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) + // Check we can send money to it. + _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { + toCounterparty.isBeneficiary + } + chargePolicy = transactionRequestBodyCounterparty.charge_policy + _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { + ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) + } + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodyCounterparty)(Serialization.formats(NoTypeHints)) + } + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + toAccount, + transactionRequestType, + transactionRequestBodyCounterparty, + transDetailsSerialized, + chargePolicy, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) + } yield (createdTransactionRequest, callContext) + } + case AGENT_CASH_WITHDRAWAL => { + for { + //For Agent, Use the agentId to find the agent and set up the toAccount + transactionRequestBodyAgent <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $AGENT_CASH_WITHDRAWAL json format", 400, callContext) { + json.extract[TransactionRequestBodyAgentJsonV510] + } + toAgentId = transactionRequestBodyAgent.to.agent_id + (customerAccountLinks, callContext) <- NewStyle.function.getCustomerAccountLinksByCustomerId(toAgentId, callContext) + customerAccountLink <- NewStyle.function.tryons(AgentAccountLinkNotFound, 400, callContext) { + customerAccountLinks.head + } + (toAccount, callContext) <- NewStyle.function.getBankAccount(BankId(customerAccountLink.bankId), AccountId(customerAccountLink.accountId), callContext) + chargePolicy = transactionRequestBodyAgent.charge_policy + _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { + ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) + } + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodyAgent)(Serialization.formats(NoTypeHints)) + } + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + toAccount, + transactionRequestType, + transactionRequestBodyAgent, + transDetailsSerialized, + chargePolicy, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) + } yield (createdTransactionRequest, callContext) + } + case CARD => { + for { + //2rd: get toAccount from counterpartyId + transactionRequestBodyCard <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $CARD json format", 400, callContext) { + json.extract[TransactionRequestBodyCardJsonV400] + } + toCounterpartyId = transactionRequestBodyCard.to.counterparty_id + (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByCounterpartyId(CounterpartyId(toCounterpartyId), callContext) + toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) + // Check we can send money to it. + _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { + toCounterparty.isBeneficiary + } + chargePolicy = ChargePolicy.RECEIVER.toString + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodyCard)(Serialization.formats(NoTypeHints)) + } + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + toAccount, + transactionRequestType, + transactionRequestBodyCard, + transDetailsSerialized, + chargePolicy, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) + } yield (createdTransactionRequest, callContext) + + } + case SIMPLE => { + for { + //For SAMPLE, we will create/get toCounterparty on site and set up the toAccount + transactionRequestBodySimple <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SIMPLE json format", 400, callContext) { + json.extract[TransactionRequestBodySimpleJsonV400] + } + (toCounterparty, callContext) <- NewStyle.function.getOrCreateCounterparty( + name = transactionRequestBodySimple.to.name, + description = transactionRequestBodySimple.to.description, + currency = transactionRequestBodySimple.value.currency, + createdByUserId = u.userId, + thisBankId = bankId.value, + thisAccountId = accountId.value, + thisViewId = viewId.value, + otherBankRoutingScheme = transactionRequestBodySimple.to.other_bank_routing_scheme, + otherBankRoutingAddress = transactionRequestBodySimple.to.other_bank_routing_address, + otherBranchRoutingScheme = transactionRequestBodySimple.to.other_branch_routing_scheme, + otherBranchRoutingAddress = transactionRequestBodySimple.to.other_branch_routing_address, + otherAccountRoutingScheme = transactionRequestBodySimple.to.other_account_routing_scheme, + otherAccountRoutingAddress = transactionRequestBodySimple.to.other_account_routing_address, + otherAccountSecondaryRoutingScheme = transactionRequestBodySimple.to.other_account_secondary_routing_scheme, + otherAccountSecondaryRoutingAddress = transactionRequestBodySimple.to.other_account_secondary_routing_address, + callContext: Option[CallContext], + ) + toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) + // Check we can send money to it. + _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { + toCounterparty.isBeneficiary + } + chargePolicy = transactionRequestBodySimple.charge_policy + _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { + ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) + } + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodySimple)(Serialization.formats(NoTypeHints)) + } + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + toAccount, + transactionRequestType, + transactionRequestBodySimple, + transDetailsSerialized, + chargePolicy, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) + } yield (createdTransactionRequest, callContext) + + } + case SEPA => { + for { + //For SEPA, Use the IBAN to find the toCounterparty and set up the toAccount + transDetailsSEPAJson <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $SEPA json format", 400, callContext) { + json.extract[TransactionRequestBodySEPAJsonV400] + } + toIban = transDetailsSEPAJson.to.iban + (toCounterparty, callContext) <- NewStyle.function.getCounterpartyByIbanAndBankAccountId(toIban, fromAccount.bankId, fromAccount.accountId, callContext) + toAccount <- NewStyle.function.getBankAccountFromCounterparty(toCounterparty, true, callContext) + _ <- Helper.booleanToFuture(s"$CounterpartyBeneficiaryPermit", cc=callContext) { + toCounterparty.isBeneficiary + } + chargePolicy = transDetailsSEPAJson.charge_policy + _ <- Helper.booleanToFuture(s"$InvalidChargePolicy", cc=callContext) { + ChargePolicy.values.contains(ChargePolicy.withName(chargePolicy)) + } + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transDetailsSEPAJson)(Serialization.formats(NoTypeHints)) + } + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + toAccount, + transactionRequestType, + transDetailsSEPAJson, + transDetailsSerialized, + chargePolicy, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + transDetailsSEPAJson.reasons.map(_.map(_.transform)), + callContext) + } yield (createdTransactionRequest, callContext) + } + case FREE_FORM => { + for { + transactionRequestBodyFreeForm <- NewStyle.function.tryons(s"${InvalidJsonFormat}, it should be $FREE_FORM json format", 400, callContext) { + json.extract[TransactionRequestBodyFreeFormJSON] + } + // Following lines: just transfer the details body, add Bank_Id and Account_Id in the Detail part. This is for persistence and 'answerTransactionRequestChallenge' + transactionRequestAccountJSON = TransactionRequestAccountJsonV140(bankId.value, accountId.value) + transDetailsSerialized <- NewStyle.function.tryons(UnknownError, 400, callContext) { + write(transactionRequestBodyFreeForm)(Serialization.formats(NoTypeHints)) + } + (createdTransactionRequest, callContext) <- NewStyle.function.createTransactionRequestv400(u, + viewId, + fromAccount, + fromAccount, + transactionRequestType, + transactionRequestBodyFreeForm, + transDetailsSerialized, + sharedChargePolicy.toString, + Some(OBP_TRANSACTION_REQUEST_CHALLENGE), + getScaMethodAtInstance(transactionRequestType.value).toOption, + None, + callContext) + } yield + (createdTransactionRequest, callContext) + } + } + (challenges, callContext) <- NewStyle.function.getChallengesByTransactionRequestId(createdTransactionRequest.id.value, callContext) + } yield { + (JSONFactory400.createTransactionRequestWithChargeJSON(createdTransactionRequest, challenges), HttpCode.`201`(callContext)) + } + } + } diff --git a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala index 74beb10c3..e123717a0 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/APIMethods500.scala @@ -1290,7 +1290,7 @@ trait APIMethods500 { case "banks" :: BankId(bankId) :: "customers" :: Nil JsonPost json -> _ => { cc => implicit val ec = EndpointContext(Some(cc)) for { - postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerJsonV310 ", 400, cc.callContext) { + postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostCustomerJsonV500 ", 400, cc.callContext) { json.extract[PostCustomerJsonV500] } _ <- Helper.booleanToFuture(failMsg = InvalidJsonContent + s" The field dependants(${postedData.dependants.getOrElse(0)}) not equal the length(${postedData.dob_of_dependants.getOrElse(Nil).length }) of dob_of_dependants array", 400, cc.callContext) { diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index bbf7e0976..a3024f616 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -18,15 +18,13 @@ import code.api.util.newstyle.RegulatedEntityNewStyle.{createRegulatedEntityNewS import code.api.v2_1_0.ConsumerRedirectUrlJSON import code.api.v3_0_0.JSONFactory300 import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson -import code.api.v3_1_0.ConsentJsonV310 +import code.api.v3_1_0.{ConsentJsonV310, JSONFactory310} import code.api.v3_1_0.JSONFactory310.createBadLoginStatusJson +import code.api.v4_0_0.APIMethods400.{createTransactionRequest, transactionRequestGeneralText} import code.api.v4_0_0.JSONFactory400.{createAccountBalancesJson, createBalancesJson, createNewCoreBankAccountJson} import code.api.v4_0_0.{JSONFactory400, PostAccountAccessJsonV400, PostApiCollectionJson400, RevokedJsonV400} -import code.api.v5_0_0.{JSONFactory500, PostConsentRequestJsonV500} -import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson} -import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson} import code.api.v5_0_0.JSONFactory500 -import code.api.v5_1_0.JSONFactory510.{createRegulatedEntitiesJson, createRegulatedEntityJson} +import code.api.v5_1_0.JSONFactory510.{createConsentsInfoJsonV510, createConsentsJsonV510, createRegulatedEntitiesJson, createRegulatedEntityJson} import code.atmattribute.AtmAttribute import code.bankconnectors.Connector import code.consent.{ConsentRequests, Consents} @@ -345,6 +343,178 @@ trait APIMethods510 { } } } + + staticResourceDocs += ResourceDoc( + createAgent, + implementedInApiVersion, + nameOf(createAgent), + "POST", + "/banks/BANK_ID/agents", + "Create Agent", + s""" + |The Customer resource stores the customer number (which is set by the backend), legal name, email, phone number, their date of birth, relationship status, education attained, a url for a profile image, KYC status etc. + |Dates need to be in the format 2013-01-21T23:08:00Z + | + |Note: If you need to set a specific customer number, use the Update Customer Number endpoint after this call. + | + |${authenticationRequiredMessage(true)} + |""", + postAgentJsonV510, + agentJsonV510, + List( + $UserNotLoggedIn, + $BankNotFound, + InvalidJsonFormat, + CustomerNumberAlreadyExists, + UserNotFoundById, + CustomerAlreadyExistsForUser, + CreateConsumerError, + UnknownError + ), + List(apiTagCustomer, apiTagPerson) + ) + + lazy val createAgent : OBPEndpoint = { + case "banks" :: BankId(bankId) :: "agents" :: Nil JsonPost json -> _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + postedData <- NewStyle.function.tryons(s"$InvalidJsonFormat The Json body should be the $PostAgentJsonV510 ", 400, cc.callContext) { + json.extract[PostAgentJsonV510] + } + (_, callContext) <- NewStyle.function.checkCustomerNumberAvailable(bankId, postedData.agent_number, cc.callContext) + (customer, callContext) <- NewStyle.function.createCustomerC2( + bankId, + postedData.legal_name, + postedData.agent_number, + postedData.mobile_phone_number, + "", + CustomerFaceImage(null, ""), + null, + "", + 0, + Nil, + "", + "", + false, + null, + None, + None, + "", + "", + "", + callContext, + ) + (bankAccount, callContext) <- NewStyle.function.createBankAccount( + bankId, + AccountId(APIUtil.generateUUID()), + "AGENT", + "AGENT", + postedData.currency, + 0, + postedData.legal_name, + null, + Nil, + callContext + ) + (_, callContext) <- NewStyle.function.createCustomerAccountLink(customer.customerId, bankAccount.bankId.value, bankAccount.accountId.value, "Owner", callContext) + } yield { + (JSONFactory510.createAgentJson(customer, bankAccount), HttpCode.`201`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getAgent, + implementedInApiVersion, + nameOf(getAgent), + "GET", + "/banks/BANK_ID/agents/AGENT_ID", + "Get Agent", + s"""Get Agent. + | + |${authenticationRequiredMessage(true)} + |""".stripMargin, + EmptyBody, + agentJsonV510, + List( + $UserNotLoggedIn, + $BankNotFound, + UnknownError + ), + List(apiTagAccount) + ) + + lazy val getAgent: OBPEndpoint = { + case "banks" :: BankId(bankId) :: "agents" :: agentId :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- SS.user + (agent, callContext) <- NewStyle.function.getAgentByAgentId(agentId, callContext) + (customerAccountLinks, callContext) <- NewStyle.function.getCustomerAccountLinksByCustomerId(agentId, callContext) + customerAccountLink <- NewStyle.function.tryons(AgentAccountLinkNotFound, 400, cc.callContext) { + customerAccountLinks.head + } + (bankAccount, callContext) <- NewStyle.function.getBankAccount(BankId(customerAccountLink.bankId), AccountId(customerAccountLink.accountId), callContext) + } yield { + (JSONFactory510.createAgentJson(agent, bankAccount), HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + createTransactionRequestAgent, + implementedInApiVersion, + nameOf(createTransactionRequestAgent), + "POST", + "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transaction-request-types/AGENT/transaction-requests", + "Create Transaction Request (AGENT)", + s""" + | + |Either the `from` or the `to` field must be filled. Those fields refers to the information about the party that will be refunded. + | + |In case the `from` object is used, it means that the refund comes from the part that sent you a transaction. + |In the `from` object, you have two choices : + |- Use `bank_id` and `account_id` fields if the other account is registered on the OBP-API + |- Use the `counterparty_id` field in case the counterparty account is out of the OBP-API + | + |In case the `to` object is used, it means you send a request to a counterparty to ask for a refund on a previous transaction you sent. + |(This case is not managed by the OBP-API and require an external adapter) + | + | + |$transactionRequestGeneralText + | + """.stripMargin, + transactionRequestBodyAgentJsonV510, + transactionRequestWithChargeJSON400, + List( + $UserNotLoggedIn, + InvalidBankIdFormat, + InvalidAccountIdFormat, + InvalidJsonFormat, + $BankNotFound, + AccountNotFound, + $BankAccountNotFound, + InsufficientAuthorisationToCreateTransactionRequest, + InvalidTransactionRequestType, + InvalidJsonFormat, + InvalidNumber, + NotPositiveAmount, + InvalidTransactionRequestCurrency, + TransactionDisabled, + UnknownError + ), + List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2) + ) + + lazy val createTransactionRequestAgent: OBPEndpoint = { + case "banks" :: BankId(bankId) :: "accounts" :: AccountId(accountId) :: ViewId(viewId) :: "transaction-request-types" :: + "AGENT-CASH-WITHDRAWAL" :: "transaction-requests" :: Nil JsonPost json -> _ => + cc => + implicit val ec = EndpointContext(Some(cc)) + val transactionRequestType = TransactionRequestType("ACCOUNT") + createTransactionRequest(bankId, accountId, viewId, transactionRequestType, json) + } + staticResourceDocs += ResourceDoc( createNonPersonalUserAttribute, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index cc456d665..8e7146921 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -40,7 +40,7 @@ import code.atmattribute.AtmAttribute import code.atms.Atms.Atm import code.users.{UserAttribute, Users} import code.views.system.{AccountAccess, ViewDefinition} -import com.openbankproject.commons.model.{AccountRoutingJsonV121, Address, AtmId, AtmT, BankId, BankIdAccountId, BranchRoutingJsonV141, CreateViewJson, Customer, Location, Meta, RegulatedEntityTrait, UpdateViewJSON, View} +import com.openbankproject.commons.model.{AccountRoutingJsonV121, Address, AmountOfMoneyJsonV121, AtmId, AtmT, BankAccount, BankId, BankIdAccountId, BranchRoutingJsonV141, CreateViewJson, Customer, Location, Meta, RegulatedEntityTrait, TransactionRequestCommonBodyJSON, UpdateViewJSON, View} import com.openbankproject.commons.util.{ApiVersion, ScannedApiVersion} import java.util.Date @@ -317,6 +317,38 @@ case class UserAttributesResponseJsonV510( ) case class CustomerIdJson(id: String) +case class AgentJson( + id: String, + name:String +) + +case class AgentIdJson( + agent_id: String +) + +case class TransactionRequestBodyAgentJsonV510( + to: AgentIdJson, + value: AmountOfMoneyJsonV121, + description: String, + charge_policy: String, + future_date: Option[String] = None +) extends TransactionRequestCommonBodyJSON + +case class PostAgentJsonV510( + legal_name: String, + mobile_phone_number: String, + agent_number: String, + currency: String +) + +case class AgentJsonV510( + agent_id: String, + legal_name: String, + mobile_phone_number: String, + agent_number: String, + currency: String +) + case class CustomersIdsJsonV510(customers: List[CustomerIdJson]) case class PostCustomerLegalNameJsonV510(legal_name: String) @@ -912,6 +944,15 @@ object JSONFactory510 extends CustomJsonFormats { ConsumersJsonV510(consumers.map(createConsumerJSON(_,None))) } + def createAgentJson(customer: Customer, bankAccount: BankAccount): AgentJsonV510 = { + AgentJsonV510( + agent_id = customer.customerId, + legal_name = customer.legalName, + mobile_phone_number = customer.mobileNumber, + agent_number = customer.number, + currency = bankAccount.currency + ) + } } diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala index e65d502d3..0372048c8 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/enums/Enumerations.scala @@ -105,6 +105,7 @@ object TransactionRequestTypes extends OBPEnumeration[TransactionRequestTypes]{ object TARGET_2_PAYMENTS extends Value object CROSS_BORDER_CREDIT_TRANSFERS extends Value object REFUND extends Value + object AGENT_CASH_WITHDRAWAL extends Value } sealed trait StrongCustomerAuthentication extends EnumValue