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 4791ec703..f76651a99 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 @@ -16,7 +16,7 @@ import code.api.v3_0_0.JSONFactory300.createBranchJsonV300 import code.api.v3_0_0.custom.JSONFactoryCustom300 import code.api.v3_0_0.{LobbyJsonV330, _} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, CustomerWithAttributesJsonV310, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} -import code.api.v4_0_0.{AccountMinimalJson400, BankAttributeBankResponseJsonV400, CustomerMinimalJsonV400, FastFirehoseAccountsJsonV400, PostHistoricalTransactionAtBankJson, _} +import code.api.v4_0_0.{AccountMinimalJson400, BankAttributeBankResponseJsonV400, CardJsonV400, CustomerMinimalJsonV400, FastFirehoseAccountsJsonV400, PostHistoricalTransactionAtBankJson, _} import code.api.v3_1_0.{AccountBalanceV310, AccountsBalancesV310Json, BadLoginStatusJson, ContactDetailsJson, InviteeJson, ObpApiLoopbackJson, PhysicalCardWithAttributesJsonV310, PutUpdateCustomerEmailJsonV310, _} import code.api.v5_0_0._ import code.branches.Branches.{Branch, DriveUpString, LobbyString} @@ -4196,6 +4196,22 @@ object SwaggerDefinitionsJSON { refund = RefundJson(transactionIdExample.value, transactionRequestRefundReasonCodeExample.value) ) + val cardJsonV400 = CardJsonV400( + card_type = cardTypeExample.value, + brand = brandExample.value, + cvv = cvvExample.value, + card_number = cardNumberExample.value, + name_on_card = nameOnCardExample.value, + expiry_year = expiryYearExample.value, + expiry_month = expiryMonthExample.value, + ) + + val transactionRequestBodyCardJsonV400 = TransactionRequestBodyCardJsonV400( + card = cardJsonV400, + to = counterpartyIdJson, + value = amountOfMoneyJsonV121, + description = "A card payment description. " + ) val customerAttributesResponseJson = CustomerAttributesResponseJson ( customer_attributes = List(customerAttributeResponseJson) ) @@ -4743,7 +4759,79 @@ object SwaggerDefinitionsJSON { valid_from = Some(new Date()), time_to_live = Some(3600) ) - + + val createPhysicalCardJsonV500 = CreatePhysicalCardJsonV500( + card_number = bankCardNumberExample.value, + card_type = cardTypeExample.value, + name_on_card = nameOnCardExample.value, + issue_number = issueNumberExample.value, + serial_number = serialNumberExample.value, + valid_from_date = DateWithDayExampleObject, + expires_date = DateWithDayExampleObject, + enabled = true, + technology = technologyExample.value, + networks = networksExample.value.split("[,;]").toList, + allows = List(CardAction.CREDIT.toString.toLowerCase, CardAction.DEBIT.toString.toLowerCase), + account_id =accountIdExample.value, + replacement = Some(replacementJSON), + pin_reset = List(pinResetJSON, pinResetJSON1), + collected = Some(DateWithDayExampleObject), + posted = Some(DateWithDayExampleObject), + customer_id = customerIdExample.value, + brand = brandExample.value + ) + + val physicalCardJsonV500 = PhysicalCardJsonV500( + card_id = cardIdExample.value, + bank_id = bankIdExample.value, + card_number = bankCardNumberExample.value, + card_type = cardTypeExample.value, + name_on_card = nameOnCardExample.value, + issue_number = issueNumberExample.value, + serial_number = serialNumberExample.value, + valid_from_date = DateWithDayExampleObject, + expires_date = DateWithDayExampleObject, + enabled = true, + cancelled = true, + on_hot_list = true, + technology = technologyExample.value, + networks = networksExample.value.split("[,;]").toList, + allows = List(CardAction.CREDIT.toString.toLowerCase, CardAction.DEBIT.toString.toLowerCase), + account = accountJSON, + replacement = replacementJSON, + pin_reset = List(pinResetJSON), + collected = DateWithDayExampleObject, + posted = DateWithDayExampleObject, + customer_id = customerIdExample.value, + cvv = cvvExample.value, + brand = brandExample.value + ) + + val physicalCardWithAttributesJsonV500 = PhysicalCardWithAttributesJsonV500( + card_id = cardIdExample.value, + bank_id = bankIdExample.value, + card_number = bankCardNumberExample.value, + card_type = cardTypeExample.value, + name_on_card = nameOnCardExample.value, + issue_number = issueNumberExample.value, + serial_number = serialNumberExample.value, + valid_from_date = DateWithDayExampleObject, + expires_date = DateWithDayExampleObject, + enabled = true, + cancelled = true, + on_hot_list = true, + technology = technologyExample.value, + networks = networksExample.value.split("[,;]").toList, + allows = List(CardAction.CREDIT.toString.toLowerCase, CardAction.DEBIT.toString.toLowerCase), + account = accountBasicV310, + replacement = replacementJSON, + pin_reset = List(pinResetJSON), + collected = DateWithDayExampleObject, + posted = DateWithDayExampleObject, + customer_id = customerIdExample.value, + card_attributes = List(cardAttributeCommons), + brand = brandExample.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/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 0243bb7f3..c0cad0d2d 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -118,6 +118,8 @@ import scala.xml.{Elem, XML} object APIUtil extends MdcLoggable with CustomJsonFormats{ + val DateWithYear = "yyyy" + val DateWithMonth = "yyyy-MM" val DateWithDay = "yyyy-MM-dd" val DateWithDay2 = "yyyyMMdd" val DateWithDay3 = "dd/MM/yyyy" @@ -126,11 +128,15 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ val DateWithMs = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" val DateWithMsRollback = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" //?? what does this `Rollback` mean ?? + val DateWithYearFormat = new SimpleDateFormat(DateWithYear) + val DateWithMonthFormat = new SimpleDateFormat(DateWithMonth) val DateWithDayFormat = new SimpleDateFormat(DateWithDay) val DateWithSecondsFormat = new SimpleDateFormat(DateWithSeconds) val DateWithMsFormat = new SimpleDateFormat(DateWithMs) val DateWithMsRollbackFormat = new SimpleDateFormat(DateWithMsRollback) + val DateWithYearExampleString: String = "1100" + val DateWithMonthExampleString: String = "1100-01" val DateWithDayExampleString: String = "1100-01-01" val DateWithSecondsExampleString: String = "1100-01-01T01:01:01Z" val DateWithMsExampleString: String = "1100-01-01T01:01:01.000Z" 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 1cbceecc1..7eb89e727 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -398,6 +398,8 @@ object ErrorMessages { val ProductFeeNotFoundById = "OBP-30117: Product Fee not found. Please specify a valid value for PRODUCT_FEE_ID." val CreateProductFeeError = "OBP-30118: Could not insert the Product Fee." 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 EntitlementIsBankRole = "OBP-30205: This entitlement is a Bank Role. Please set bank_id to a valid bank id." 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 0fba0d74c..6a9d623fb 100644 --- a/obp-api/src/main/scala/code/api/util/ExampleValue.scala +++ b/obp-api/src/main/scala/code/api/util/ExampleValue.scala @@ -372,7 +372,19 @@ object ExampleValue { glossaryItems += makeGlossaryItem("Adapter.card_id", cardIdExample) lazy val nameOnCardExample = ConnectorField(owner1Example.value, s"The name on the physical card") - glossaryItems += makeGlossaryItem("Adapter.name_on_card", nameOnCardExample) + glossaryItems += makeGlossaryItem("Adapter.name_on_card", nameOnCardExample) + + lazy val cvvExample = ConnectorField("123", s"Card Verification Value") + glossaryItems += makeGlossaryItem("Adapter.cvv", nameOnCardExample) + + lazy val brandExample = ConnectorField("Visa", s"The brand of the card, eg: Visa, Mastercard") + glossaryItems += makeGlossaryItem("Adapter.brand", nameOnCardExample) + + lazy val expiryYearExample = ConnectorField("2023", s"The expiry year of the card") + glossaryItems += makeGlossaryItem("Adapter.expiry_year", expiryYearExample) + + lazy val expiryMonthExample = ConnectorField("01", s"The expiry month of the card") + glossaryItems += makeGlossaryItem("Adapter.expiry_month", expiryMonthExample) lazy val issueNumberExample = ConnectorField("1", s"The issue number of the physical card, eg 1,2,3,4 ....") glossaryItems += makeGlossaryItem("Adapter.issue_number", issueNumberExample) 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 5e3950ec6..d8a1e204f 100644 --- a/obp-api/src/main/scala/code/api/util/NewStyle.scala +++ b/obp-api/src/main/scala/code/api/util/NewStyle.scala @@ -2591,6 +2591,8 @@ object NewStyle extends MdcLoggable{ collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext] ): OBPReturnType[PhysicalCard] = { validateBankId(bankId, callContext) @@ -2616,6 +2618,8 @@ object NewStyle extends MdcLoggable{ collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext] ) map { i => (unboxFullOrFail(i._1, callContext, s"$CreateCardError"), i._2) @@ -2681,6 +2685,12 @@ object NewStyle extends MdcLoggable{ i => (unboxFullOrFail(i._1, callContext, CardNotFound), i._2) } + def getPhysicalCardByCardNumber(bankCardNumber: String, callContext:Option[CallContext]) : OBPReturnType[PhysicalCardTrait] = { + Connector.connector.vend.getPhysicalCardByCardNumber(bankCardNumber: String, callContext:Option[CallContext]) map { + i => (unboxFullOrFail(i._1, callContext, InvalidCardNumber), i._2) + } + } + def getPhysicalCardForBank(bankId: BankId, cardId:String ,callContext:Option[CallContext]) : OBPReturnType[PhysicalCardTrait] = Connector.connector.vend.getPhysicalCardForBank(bankId: BankId, cardId: String, callContext:Option[CallContext]) map { i => (unboxFullOrFail(i._1, callContext, s"$CardNotFound Current CardId($cardId)"), i._2) diff --git a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala index 1640c05cb..fe76e9c57 100644 --- a/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala +++ b/obp-api/src/main/scala/code/api/v2_1_0/APIMethods210.scala @@ -1001,6 +1001,8 @@ trait APIMethods210 { collected= Option(CardCollectionInfo(postJson.collected)), posted= Option(CardPostedInfo(postJson.posted)), customerId = "",// this field is introduced from V310 + cvv = "",// this field is introduced from V500 + brand = "",// this field is introduced from V500 callContext ) diff --git a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala index d6983df5f..87d619dec 100644 --- a/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala +++ b/obp-api/src/main/scala/code/api/v3_1_0/APIMethods310.scala @@ -4814,6 +4814,8 @@ trait APIMethods310 { collected = collected, posted = posted, customerId = postJson.customer_id, + cvv = "",//added from v500 + brand = "",//added from v500 callContext ) } yield { @@ -4841,7 +4843,7 @@ trait APIMethods310 { UnknownError ), List(apiTagCard, apiTagNewStyle), - Some(List(canCreateCardsForBank))) + Some(List(canUpdateCardsForBank))) lazy val updatedCardForBank: OBPEndpoint = { case "management" :: "banks" :: BankId(bankId) :: "cards" :: cardId :: Nil JsonPut json -> _ => { cc => @@ -4957,7 +4959,8 @@ trait APIMethods310 { emptyObjectJson, physicalCardWithAttributesJsonV310, List(UserNotLoggedIn,BankNotFound, UnknownError), - List(apiTagCard, apiTagNewStyle)) + List(apiTagCard, apiTagNewStyle), + Some(List(canGetCardsForBank))) lazy val getCardForBank : OBPEndpoint = { case "management" :: "banks" :: BankId(bankId) :: "cards" :: cardId :: Nil JsonGet _ => { cc => { 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 47a3a51f5..68b6c611d 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 @@ -914,26 +914,77 @@ trait APIMethods400 { def createTransactionRequest(bankId: BankId, accountId: AccountId, viewId: ViewId, transactionRequestType: TransactionRequestType, json: JValue): Future[(TransactionRequestWithChargeJSON400, Option[CallContext])] = { for { - (Full(u), fromAccount, callContext) <- SS.userAccount - _ <- NewStyle.function.isEnabledTransactionRequests(callContext) - _ <- Helper.booleanToFuture(InvalidAccountIdFormat, cc=callContext) { - isValidID(accountId.value) - } - _ <- Helper.booleanToFuture(InvalidBankIdFormat, cc=callContext) { - isValidID(bankId.value) - } + (Full(u), callContext) <- SS.user - account = BankIdAccountId(bankId, accountId) - _ <- NewStyle.function.checkAuthorisationToCreateTransactionRequest(viewId, account, u, callContext) + 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) } - transactionRequestTypeValue <- NewStyle.function.tryons(s"$InvalidTransactionRequestType: '${transactionRequestType.value}'. OBP does not support it.", 400, callContext) { - TransactionRequestTypes.withName(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] @@ -1152,6 +1203,38 @@ trait APIMethods400 { 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, + None, + callContext) + } yield (createdTransactionRequest, callContext) } case SIMPLE => { @@ -1330,6 +1413,53 @@ trait APIMethods400 { } + staticResourceDocs += ResourceDoc( + createTransactionRequestCard, + implementedInApiVersion, + nameOf(createTransactionRequestCard), + "POST", + "/transaction-request-types/CARD/transaction-requests", + "Create Transaction Request (CARD)", + s""" + | + |When using CARD, the payee is set in the request body . + | + |Money goes into the Counterparty in the request body. + | + |$transactionRequestGeneralText + | + """.stripMargin, + transactionRequestBodyCardJsonV400, + transactionRequestWithChargeJSON400, + List( + $UserNotLoggedIn, + InvalidBankIdFormat, + InvalidAccountIdFormat, + InvalidJsonFormat, + $BankNotFound, + AccountNotFound, + $BankAccountNotFound, + InsufficientAuthorisationToCreateTransactionRequest, + InvalidTransactionRequestType, + InvalidJsonFormat, + InvalidNumber, + NotPositiveAmount, + InvalidTransactionRequestCurrency, + TransactionDisabled, + UnknownError + ), + List(apiTagTransactionRequest, apiTagPSD2PIS, apiTagPsd2, apiTagNewStyle) + ) + + lazy val createTransactionRequestCard: OBPEndpoint = { + case "transaction-request-types" :: "CARD" :: "transaction-requests" :: Nil JsonPost json -> _ => + cc => + val transactionRequestType = TransactionRequestType("CARD") + createTransactionRequest(BankId(""), AccountId(""), ViewId("owner"), transactionRequestType, json) + } + + + staticResourceDocs += ResourceDoc( answerTransactionRequestChallenge, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala index c21cfd92e..1d918551b 100644 --- a/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala +++ b/obp-api/src/main/scala/code/api/v4_0_0/JSONFactory4.0.0.scala @@ -37,7 +37,7 @@ import code.api.v1_2_1.{BankRoutingJsonV121, JSONFactory, UserJSONV121, ViewJSON import code.api.v1_4_0.JSONFactory1_4_0.{LocationJsonV140, MetaJsonV140, TransactionRequestAccountJsonV140, transformToLocationFromV140, transformToMetaFromV140} import code.api.v2_0_0.JSONFactory200.UserJsonV200 import code.api.v2_0_0.{CreateEntitlementJSON, EntitlementJSONs, JSONFactory200, TransactionRequestChargeJsonV200} -import code.api.v2_1_0.{IbanJson, JSONFactory210, PostCounterpartyBespokeJson, ResourceUserJSON, TransactionRequestBodyCounterpartyJSON} +import code.api.v2_1_0.{CounterpartyIdJson, IbanJson, JSONFactory210, PostCounterpartyBespokeJson, ResourceUserJSON, TransactionRequestBodyCounterpartyJSON} import code.api.v2_2_0.CounterpartyMetadataJson import code.api.v3_0_0.JSONFactory300._ import code.api.v3_0_0._ @@ -54,7 +54,6 @@ import code.model.dataAccess.ResourceUser import code.model.{Consumer, ModeratedBankAccount, ModeratedBankAccountCore} import code.ratelimiting.RateLimiting import code.standingorders.StandingOrderTrait - import code.userlocks.UserLocks import code.users.{UserAgreement, UserAttribute, UserInvitation} import code.views.system.AccountAccess @@ -411,6 +410,23 @@ case class TransactionRequestBodySimpleJsonV400( future_date: Option[String] = None ) extends TransactionRequestCommonBodyJSON +case class CardJsonV400( + card_type: String, + brand: String, + cvv: String, + card_number: String, + name_on_card: String, + expiry_year: String, + expiry_month: String, +) + +case class TransactionRequestBodyCardJsonV400( + card: CardJsonV400, + to: CounterpartyIdJson, + value: AmountOfMoneyJsonV121, + description: String, +) extends TransactionRequestCommonBodyJSON + case class TransactionRequestReasonJsonV400( code: String, document_number: Option[String], 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 c45c0d7e6..55eb82929 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 @@ -13,6 +13,7 @@ import code.api.v3_0_0.JSONFactory300 import code.api.v3_1_0._ import code.api.v4_0_0.JSONFactory400.createCustomersMinimalJson import code.api.v4_0_0.{JSONFactory400, PutProductJsonV400} +import code.api.v5_0_0.JSONFactory500.createPhysicalCardJson import code.bankconnectors.Connector import code.consent.{ConsentRequests, Consents} import code.entitlement.Entitlement @@ -23,7 +24,7 @@ import code.views.Views import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.ExecutionContext.Implicits.global import com.openbankproject.commons.model.enums.StrongCustomerAuthentication -import com.openbankproject.commons.model.{AccountId, AccountRouting, BankId, CreditLimit, CreditRating, CustomerFaceImage, CustomerId, ProductCode, UserAuthContextUpdateStatus} +import com.openbankproject.commons.model.{AccountId, AccountRouting, BankId, BankIdAccountId, CardAction, CardAttributeCommons, CardCollectionInfo, CardPostedInfo, CardReplacementInfo, CardReplacementReason, CreditLimit, CreditRating, CustomerFaceImage, CustomerId, PinResetInfo, PinResetReason, ProductCode, TransactionRequestType, UserAuthContextUpdateStatus, View, ViewId} import com.openbankproject.commons.util.ApiVersion import net.liftweb.common.{Empty, Full} import net.liftweb.http.Req @@ -32,6 +33,7 @@ import net.liftweb.json import net.liftweb.json.{Extraction, compactRender, prettyRender} import net.liftweb.util.Helpers.tryo import net.liftweb.util.Props +import java.util.concurrent.ThreadLocalRandom import scala.collection.immutable.{List, Nil} import scala.collection.mutable.ArrayBuffer @@ -1290,6 +1292,104 @@ trait APIMethods500 { + staticResourceDocs += ResourceDoc( + addCardForBank, + implementedInApiVersion, + nameOf(addCardForBank), + "POST", + "/management/banks/BANK_ID/cards", + "Create Card", + s"""Create Card at bank specified by BANK_ID . + | + |${authenticationRequiredMessage(true)} + |""", + createPhysicalCardJsonV500, + physicalCardJsonV500, + List( + $UserNotLoggedIn, + $BankNotFound, + UserHasMissingRoles, + AllowedValuesAre, + UnknownError + ), + List(apiTagCard, apiTagNewStyle), + Some(List(canCreateCardsForBank))) + lazy val addCardForBank: OBPEndpoint = { + case "management" :: "banks" :: BankId(bankId) :: "cards" :: Nil JsonPost json -> _ => { + cc => + for { + (Full(u), _,callContext) <- SS.userBank + + failMsg = s"$InvalidJsonFormat The Json body should be the $CreatePhysicalCardJsonV500 " + postJson <- NewStyle.function.tryons(failMsg, 400, callContext) {json.extract[CreatePhysicalCardJsonV500]} + + _ <- postJson.allows match { + case List() => Future {true} + case _ => Helper.booleanToFuture(AllowedValuesAre + CardAction.availableValues.mkString(", "), cc=callContext)(postJson.allows.forall(a => CardAction.availableValues.contains(a))) + } + + failMsg = AllowedValuesAre + CardReplacementReason.availableValues.mkString(", ") + cardReplacementReason <- NewStyle.function.tryons(failMsg, 400, callContext) { + postJson.replacement match { + case Some(value) => CardReplacementReason.valueOf(value.reason_requested) + case None => CardReplacementReason.valueOf(CardReplacementReason.FIRST.toString) + } + } + + _<-Helper.booleanToFuture(s"${maximumLimitExceeded.replace("10000", "10")} Current issue_number is ${postJson.issue_number}", cc=callContext)(postJson.issue_number.length<= 10) + + (_, callContext)<- NewStyle.function.getBankAccount(bankId, AccountId(postJson.account_id), callContext) + + (_, callContext)<- NewStyle.function.getCustomerByCustomerId(postJson.customer_id, callContext) + + replacement = postJson.replacement match { + case Some(replacement) => + Some(CardReplacementInfo(requestedDate = replacement.requested_date, cardReplacementReason)) + case None => None + } + collected = postJson.collected match { + case Some(collected) => Some(CardCollectionInfo(collected)) + case None => None + } + posted = postJson.posted match { + case Some(posted) => Option(CardPostedInfo(posted)) + case None => None + } + + cvv = ThreadLocalRandom.current().nextLong(100, 999) + + (card, callContext) <- NewStyle.function.createPhysicalCard( + bankCardNumber=postJson.card_number, + nameOnCard=postJson.name_on_card, + cardType = postJson.card_type, + issueNumber=postJson.issue_number, + serialNumber=postJson.serial_number, + validFrom=postJson.valid_from_date, + expires=postJson.expires_date, + enabled=postJson.enabled, + cancelled=false, + onHotList=false, + technology=postJson.technology, + networks= postJson.networks, + allows= postJson.allows, + accountId= postJson.account_id, + bankId=bankId.value, + replacement = replacement, + pinResets= postJson.pin_reset.map(e => PinResetInfo(e.requested_date, PinResetReason.valueOf(e.reason_requested.toUpperCase))), + collected = collected, + posted = posted, + customerId = postJson.customer_id, + cvv = cvv.toString, + brand = postJson.brand, + callContext + ) + } yield { + //NOTE: OBP do not store the 3 digits cvv, only the hash, so we copy it here. + (createPhysicalCardJson(card, u).copy(cvv=cvv.toString), HttpCode.`201`(callContext)) + } + } + } + } } diff --git a/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala b/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala index f93a24c39..9f2f74d2d 100644 --- a/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala +++ b/obp-api/src/main/scala/code/api/v5_0_0/JSONFactory5.0.0.scala @@ -27,15 +27,17 @@ package code.api.v5_0_0 import java.util.Date - -import code.api.util.APIUtil.stringOrNull +import code.api.util.APIUtil.{stringOptionOrNull, stringOrNull} import code.api.v1_2_1.BankRoutingJsonV121 import code.api.v1_4_0.JSONFactory1_4_0.{CustomerFaceImageJson, MetaJsonV140} +import code.api.v1_3_0.JSONFactory1_3_0.{cardActionsToString, createAccountJson, createPinResetJson, createReplacementJson} +import code.api.v1_3_0.{PinResetJSON, ReplacementJSON} +import code.api.v1_4_0.JSONFactory1_4_0.CustomerFaceImageJson import code.api.v2_1_0.CustomerCreditRatingJSON -import code.api.v3_1_0.PostConsentEntitlementJsonV310 +import code.api.v3_1_0.{AccountBasicV310, PhysicalCardWithAttributesJsonV310, PostConsentEntitlementJsonV310} import code.api.v4_0_0.BankAttributeBankResponseJsonV400 import code.bankattribute.BankAttribute -import com.openbankproject.commons.model.{AccountRoutingJsonV121, AmountOfMoneyJsonV121, Bank, UserAuthContext, UserAuthContextUpdate} +import com.openbankproject.commons.model.{AccountRoutingJsonV121, AmountOfMoneyJsonV121, Bank, CardAttribute, PhysicalCardTrait, User, UserAuthContext, UserAuthContextUpdate, View, ViewBasic} import net.liftweb.json.JsonAST.JValue import scala.collection.immutable.List @@ -149,6 +151,125 @@ case class PostConsentRequestJsonV500( ) case class ConsentJsonV500(consent_id: String, jwt: String, status: String, consent_request_id: Option[String]) + +case class CreatePhysicalCardJsonV500( + card_number: String, + card_type: String, + name_on_card: String, + issue_number: String, + serial_number: String, + valid_from_date: Date, + expires_date: Date, + enabled: Boolean, + technology: String, + networks: List[String], + allows: List[String], + account_id: String, + replacement: Option[ReplacementJSON], + pin_reset: List[PinResetJSON], + collected: Option[Date], + posted: Option[Date], + customer_id: String, + brand: String +) + +case class PhysicalCardJsonV500( + card_id: String, + bank_id: String, + card_number: String, + card_type: String, + name_on_card: String, + issue_number: String, + serial_number: String, + valid_from_date: Date, + expires_date: Date, + enabled: Boolean, + cancelled: Boolean, + on_hot_list: Boolean, + technology: String, + networks: List[String], + allows: List[String], + account: code.api.v1_2_1.AccountJSON, + replacement: ReplacementJSON, + pin_reset: List[PinResetJSON], + collected: Date, + posted: Date, + customer_id: String, + cvv: String, + brand: String +) + +case class UpdatedPhysicalCardJsonV500( + card_id: String, + bank_id: String, + card_number: String, + card_type: String, + name_on_card: String, + issue_number: String, + serial_number: String, + valid_from_date: Date, + expires_date: Date, + enabled: Boolean, + cancelled: Boolean, + on_hot_list: Boolean, + technology: String, + networks: List[String], + allows: List[String], + account: code.api.v1_2_1.AccountJSON, + replacement: ReplacementJSON, + pin_reset: List[PinResetJSON], + collected: Date, + posted: Date, + customer_id: String, + brand: String +) + +case class PhysicalCardWithAttributesJsonV500( + card_id: String, + bank_id: String, + card_number: String, + card_type: String, + name_on_card: String, + issue_number: String, + serial_number: String, + valid_from_date: Date, + expires_date: Date, + enabled: Boolean, + cancelled: Boolean, + on_hot_list: Boolean, + technology: String, + networks: List[String], + allows: List[String], + account: AccountBasicV310, + replacement: ReplacementJSON, + pin_reset: List[PinResetJSON], + collected: Date, + posted: Date, + customer_id: String, + card_attributes: List[CardAttribute], + brand: String +) + +case class UpdatePhysicalCardJsonV500( + card_type: String, + name_on_card: String, + issue_number: String, + serial_number: String, + valid_from_date: Date, + expires_date: Date, + enabled: Boolean, + technology: String, + networks: List[String], + allows: List[String], + account_id: String, + replacement: ReplacementJSON, + pin_reset: List[PinResetJSON], + collected: Date, + posted: Date, + customer_id: String, + brand: String +) + object JSONFactory500 { def createUserAuthContextJson(userAuthContext: UserAuthContext): UserAuthContextJsonV500 = { @@ -200,6 +321,64 @@ object JSONFactory500 { ) ) } - + + def createPhysicalCardWithAttributesJson(card: PhysicalCardTrait, cardAttributes: List[CardAttribute],user : User, views: List[View]): PhysicalCardWithAttributesJsonV500 = { + PhysicalCardWithAttributesJsonV500( + card_id = stringOrNull(card.cardId), + bank_id = stringOrNull(card.bankId), + card_number = stringOrNull(card.bankCardNumber), + card_type = stringOrNull(card.cardType), + name_on_card = stringOrNull(card.nameOnCard), + issue_number = stringOrNull(card.issueNumber), + serial_number = stringOrNull(card.serialNumber), + valid_from_date = card.validFrom, + expires_date = card.expires, + enabled = card.enabled, + cancelled = card.cancelled, + on_hot_list = card.onHotList, + technology = stringOrNull(card.technology), + networks = card.networks, + allows = card.allows.map(cardActionsToString).toList, + account = AccountBasicV310( + card.account.accountId.value, + card.account.label, + views.map(view => ViewBasic(view.viewId.value, view.name, view.description)), + card.account.bankId.value), + replacement = card.replacement.map(createReplacementJson).getOrElse(null), + pin_reset = card.pinResets.map(createPinResetJson), + collected = card.collected.map(_.date).getOrElse(null), + posted = card.posted.map(_.date).getOrElse(null), + customer_id = stringOrNull(card.customerId), + card_attributes = cardAttributes, + brand = stringOptionOrNull(card.brand), + ) + } + def createPhysicalCardJson(card: PhysicalCardTrait, user : User): PhysicalCardJsonV500 = { + PhysicalCardJsonV500( + card_id = stringOrNull(card.cardId), + bank_id = stringOrNull(card.bankId), + card_number = stringOrNull(card.bankCardNumber), + card_type = stringOrNull(card.cardType), + name_on_card = stringOrNull(card.nameOnCard), + issue_number = stringOrNull(card.issueNumber), + serial_number = stringOrNull(card.serialNumber), + valid_from_date = card.validFrom, + expires_date = card.expires, + enabled = card.enabled, + cancelled = card.cancelled, + on_hot_list = card.onHotList, + technology = stringOrNull(card.technology), + networks = card.networks, + allows = card.allows.map(cardActionsToString).toList, + account = createAccountJson(card.account, user), + replacement = card.replacement.map(createReplacementJson).getOrElse(null), + pin_reset = card.pinResets.map(createPinResetJson), + collected = card.collected.map(_.date).getOrElse(null), + posted = card.posted.map(_.date).getOrElse(null), + customer_id = stringOrNull(card.customerId), + cvv = stringOptionOrNull(card.cvv), + brand = stringOptionOrNull(card.brand) + ) + } } diff --git a/obp-api/src/main/scala/code/bankconnectors/Connector.scala b/obp-api/src/main/scala/code/bankconnectors/Connector.scala index ac85cb8fb..4c159e305 100644 --- a/obp-api/src/main/scala/code/bankconnectors/Connector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/Connector.scala @@ -628,6 +628,9 @@ trait Connector extends MdcLoggable { def getPhysicalCardsForUser(user : User, callContext: Option[CallContext] = None) : OBPReturnType[Box[List[PhysicalCard]]] = Future{(Failure(setUnimplementedError), callContext)} def getPhysicalCardForBank(bankId: BankId, cardId: String, callContext:Option[CallContext]) : OBPReturnType[Box[PhysicalCardTrait]] = Future{(Failure(setUnimplementedError), callContext)} + + def getPhysicalCardByCardNumber(bankCardNumber: String, callContext:Option[CallContext]) : OBPReturnType[Box[PhysicalCardTrait]] = Future{(Failure(setUnimplementedError), callContext)} + def deletePhysicalCardForBank(bankId: BankId, cardId: String, callContext:Option[CallContext]) : OBPReturnType[Box[Boolean]] = Future{(Failure(setUnimplementedError), callContext)} def getPhysicalCardsForBankLegacy(bank: Bank, user : User, queryParams: List[OBPQueryParam]) : Box[List[PhysicalCard]] = Failure(setUnimplementedError) @@ -654,6 +657,8 @@ trait Connector extends MdcLoggable { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext] ): Box[PhysicalCard] = Failure(setUnimplementedError) @@ -678,6 +683,8 @@ trait Connector extends MdcLoggable { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext] ): OBPReturnType[Box[PhysicalCard]] = Future{(Failure{setUnimplementedError}, callContext)} @@ -1415,7 +1422,7 @@ trait Connector extends MdcLoggable { }yield{ (createdTransactionId,callContext) } - case transactionRequestType => Future((throw new Exception(s"${InvalidTransactionRequestType}: '${transactionRequestType}'. Not supported in this version.")), callContext) + case _ => Future((throw new Exception(s"${InvalidTransactionRequestType}: '${transactionRequestType}'. Not completed in this version.")), callContext) } didSaveTransId <- Future{saveTransactionRequestTransaction(transactionRequestId, transactionId).openOrThrowException(attemptedToOpenAnEmptyBox)} diff --git a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala index 61a2d6631..40844bc39 100644 --- a/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala +++ b/obp-api/src/main/scala/code/bankconnectors/LocalMappedConnector.scala @@ -1204,7 +1204,9 @@ object LocalMappedConnector extends Connector with MdcLoggable { pinResets = l.pinResets, collected = l.collected, posted = l.posted, - customerId = l.customerId + customerId = l.customerId, + cvv = l.cvv, + brand = l.brand ) (Full(cardList), callContext) } @@ -1216,6 +1218,13 @@ object LocalMappedConnector extends Connector with MdcLoggable { ) } + override def getPhysicalCardByCardNumber(bankCardNumber: String, callContext:Option[CallContext]) : OBPReturnType[Box[PhysicalCardTrait]] = Future { + ( + code.cards.PhysicalCard.physicalCardProvider.vend.getPhysicalCardByCardNumber(bankCardNumber: String, callContext: Option[CallContext]), + callContext + ) + } + override def getPhysicalCardsForBankLegacy(bank: Bank, user: User, queryParams: List[OBPQueryParam]): Box[List[PhysicalCard]] = { val list = code.cards.PhysicalCard.physicalCardProvider.vend.getPhysicalCardsForBank(bank, user, queryParams) val cardList = for (l <- list) yield @@ -1240,7 +1249,9 @@ object LocalMappedConnector extends Connector with MdcLoggable { pinResets = l.pinResets, collected = l.collected, posted = l.posted, - customerId = l.customerId + customerId = l.customerId, + cvv = l.cvv, + brand = l.brand ) Full(cardList) } @@ -1276,6 +1287,8 @@ object LocalMappedConnector extends Connector with MdcLoggable { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext]): OBPReturnType[Box[PhysicalCard]] = Future { (createPhysicalCardLegacy( bankCardNumber: String, @@ -1298,6 +1311,8 @@ object LocalMappedConnector extends Connector with MdcLoggable { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext]), callContext) } @@ -1324,6 +1339,8 @@ object LocalMappedConnector extends Connector with MdcLoggable { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext]): Box[PhysicalCard] = { val physicalCardBox: Box[MappedPhysicalCard] = code.cards.PhysicalCard.physicalCardProvider.vend.createPhysicalCard( bankCardNumber: String, @@ -1346,6 +1363,8 @@ object LocalMappedConnector extends Connector with MdcLoggable { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext]) for (l <- physicalCardBox) yield @@ -1370,7 +1389,9 @@ object LocalMappedConnector extends Connector with MdcLoggable { pinResets = l.pinResets, collected = l.collected, posted = l.posted, - customerId = l.customerId + customerId = l.customerId, + cvv = l.cvv, + brand = l.brand, ) } @@ -5088,7 +5109,7 @@ object LocalMappedConnector extends Connector with MdcLoggable { } yield { (transactionId, callContext) } - case COUNTERPARTY => + case COUNTERPARTY | CARD => for { bodyToCounterparty <- NewStyle.function.tryons(s"$TransactionRequestDetailsExtractException It can not extract to $TransactionRequestBodyCounterpartyJSON", 400, callContext) { body.to_counterparty.get diff --git a/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala b/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala index 2670128c0..8c0ed2a9f 100644 --- a/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala +++ b/obp-api/src/main/scala/code/bankconnectors/akka/AkkaConnector_vDec2018.scala @@ -1430,7 +1430,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value))) + customerId=customerIdExample.value + ))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1496,7 +1497,8 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value)) + customerId=customerIdExample.value + )) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1652,7 +1654,9 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value) + customerId=customerIdExample.value, + cvv = cvvExample.value, + brand = brandExample.value) ), exampleInboundMessage = ( InBoundCreatePhysicalCard(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, @@ -1701,9 +1705,14 @@ object AkkaConnector_vDec2018 extends Connector with AkkaConnectorActorInit { adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) - override def createPhysicalCard(bankCardNumber: String, nameOnCard: String, cardType: String, issueNumber: String, serialNumber: String, validFrom: Date, expires: Date, enabled: Boolean, cancelled: Boolean, onHotList: Boolean, technology: String, networks: List[String], allows: List[String], accountId: String, bankId: String, replacement: Option[CardReplacementInfo], pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, callContext: Option[CallContext]): OBPReturnType[Box[PhysicalCard]] = { + override def createPhysicalCard(bankCardNumber: String, nameOnCard: String, cardType: String, issueNumber: String, + serialNumber: String, validFrom: Date, expires: Date, enabled: Boolean, cancelled: Boolean, onHotList: Boolean, + technology: String, networks: List[String], allows: List[String], accountId: String, bankId: String, + replacement: Option[CardReplacementInfo], pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], + posted: Option[CardPostedInfo], customerId: String, cvv: String, brand: String, + callContext: Option[CallContext]): OBPReturnType[Box[PhysicalCard]] = { import com.openbankproject.commons.dto.{InBoundCreatePhysicalCard => InBound, OutBoundCreatePhysicalCard => OutBound} - val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, bankCardNumber, nameOnCard, cardType, issueNumber, serialNumber, validFrom, expires, enabled, cancelled, onHotList, technology, networks, allows, accountId, bankId, replacement, pinResets, collected, posted, customerId) + val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, bankCardNumber, nameOnCard, cardType, issueNumber, serialNumber, validFrom, expires, enabled, cancelled, onHotList, technology, networks, allows, accountId, bankId, replacement, pinResets, collected, posted, customerId, cvv, brand) val response: Future[Box[InBound]] = (southSideActor ? req).mapTo[InBound].recoverWith(recoverFunction).map(Box !! _) response.map(convertToTuple[PhysicalCard](callContext)) } diff --git a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala index d59ddcf87..af8eee7ee 100644 --- a/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/rest/RestConnector_vMar2019.scala @@ -1570,7 +1570,8 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value))) + customerId=customerIdExample.value + ))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1750,8 +1751,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value))) - ), + customerId=customerIdExample.value)))), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1792,8 +1792,9 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value) - ), + customerId=customerIdExample.value, + cvv = cvvExample.value, + brand = brandExample.value)), exampleInboundMessage = ( InBoundCreatePhysicalCard(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, status=MessageDocsSwaggerDefinitions.inboundStatus, @@ -1836,14 +1837,18 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value)) - ), + customerId=customerIdExample.value, + cvv = Some(cvvExample.value), + brand = Some(brandExample.value)))), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) - override def createPhysicalCard(bankCardNumber: String, nameOnCard: String, cardType: String, issueNumber: String, serialNumber: String, validFrom: Date, expires: Date, enabled: Boolean, cancelled: Boolean, onHotList: Boolean, technology: String, networks: List[String], allows: List[String], accountId: String, bankId: String, replacement: Option[CardReplacementInfo], pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, callContext: Option[CallContext]): OBPReturnType[Box[PhysicalCard]] = { - import com.openbankproject.commons.dto.{InBoundCreatePhysicalCard => InBound, OutBoundCreatePhysicalCard => OutBound} - val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, bankCardNumber, nameOnCard, cardType, issueNumber, serialNumber, validFrom, expires, enabled, cancelled, onHotList, technology, networks, allows, accountId, bankId, replacement, pinResets, collected, posted, customerId) + override def createPhysicalCard(bankCardNumber: String, nameOnCard: String, cardType: String, issueNumber: String, serialNumber: String, validFrom: Date, expires: Date, enabled: Boolean, + cancelled: Boolean, onHotList: Boolean, technology: String, networks: List[String], allows: List[String], accountId: String, bankId: String, replacement: Option[CardReplacementInfo], + pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, cvv: String, + brand: String,callContext: Option[CallContext]): OBPReturnType[Box[PhysicalCard]] = { + import com.openbankproject.commons.dto.{InBoundCreatePhysicalCard => InBound, OutBoundCreatePhysicalCard => OutBound} + val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, bankCardNumber, nameOnCard, cardType, issueNumber, serialNumber, validFrom, expires, enabled, cancelled, onHotList, technology, networks, allows, accountId, bankId, replacement, pinResets, collected, posted, customerId,cvv, brand) val response: Future[Box[InBound]] = sendRequest[InBound](getUrl(callContext, "createPhysicalCard"), HttpMethods.POST, req, callContext) response.map(convertToTuple[PhysicalCard](callContext)) } diff --git a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala index 3cd93481d..7a99aa810 100644 --- a/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala +++ b/obp-api/src/main/scala/code/bankconnectors/storedprocedure/StoredProcedureConnector_vDec2019.scala @@ -1615,7 +1615,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value)) + customerId=customerIdExample.value + )) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1729,7 +1730,8 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value))) + customerId=customerIdExample.value + ))) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) @@ -1771,7 +1773,9 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value) + customerId=customerIdExample.value, + cvv = cvvExample.value, + brand = brandExample.value) ), exampleInboundMessage = ( InBoundCreatePhysicalCard(inboundAdapterCallContext=MessageDocsSwaggerDefinitions.inboundAdapterCallContext, @@ -1815,14 +1819,20 @@ trait StoredProcedureConnector_vDec2019 extends Connector with MdcLoggable { reasonRequested=com.openbankproject.commons.model.PinResetReason.FORGOT)), collected=Some(CardCollectionInfo(toDate(collectedExample))), posted=Some(CardPostedInfo(toDate(postedExample))), - customerId=customerIdExample.value)) + customerId=customerIdExample.value + )) ), adapterImplementation = Some(AdapterImplementation("- Core", 1)) ) - override def createPhysicalCard(bankCardNumber: String, nameOnCard: String, cardType: String, issueNumber: String, serialNumber: String, validFrom: Date, expires: Date, enabled: Boolean, cancelled: Boolean, onHotList: Boolean, technology: String, networks: List[String], allows: List[String], accountId: String, bankId: String, replacement: Option[CardReplacementInfo], pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, callContext: Option[CallContext]): OBPReturnType[Box[PhysicalCard]] = { - import com.openbankproject.commons.dto.{InBoundCreatePhysicalCard => InBound, OutBoundCreatePhysicalCard => OutBound} - val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, bankCardNumber, nameOnCard, cardType, issueNumber, serialNumber, validFrom, expires, enabled, cancelled, onHotList, technology, networks, allows, accountId, bankId, replacement, pinResets, collected, posted, customerId) + override def createPhysicalCard(bankCardNumber: String, nameOnCard: String, cardType: String, issueNumber: String, serialNumber: String, + validFrom: Date, expires: Date, enabled: Boolean, cancelled: Boolean, onHotList: Boolean, technology: String, networks: List[String], + allows: List[String], accountId: String, bankId: String, replacement: Option[CardReplacementInfo], pinResets: List[PinResetInfo], + collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, cvv: String, + brand: String, callContext: Option[CallContext]): OBPReturnType[Box[PhysicalCard]] = { + import com.openbankproject.commons.dto.{InBoundCreatePhysicalCard => InBound, OutBoundCreatePhysicalCard => OutBound} + val req = OutBound(callContext.map(_.toOutboundAdapterCallContext).orNull, bankCardNumber, nameOnCard, cardType, issueNumber, + serialNumber, validFrom, expires, enabled, cancelled, onHotList, technology, networks, allows, accountId, bankId, replacement, pinResets, collected, posted, customerId, cvv, brand) val response: Future[Box[InBound]] = sendRequest[InBound]("obp_create_physical_card", req, callContext) response.map(convertToTuple[PhysicalCard](callContext)) } diff --git a/obp-api/src/main/scala/code/cards/MappedPhisicalCard.scala b/obp-api/src/main/scala/code/cards/MappedPhisicalCard.scala index a53a87c11..0dd125d84 100644 --- a/obp-api/src/main/scala/code/cards/MappedPhisicalCard.scala +++ b/obp-api/src/main/scala/code/cards/MappedPhisicalCard.scala @@ -139,6 +139,8 @@ object MappedPhysicalCardProvider extends PhysicalCardProvider { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext] ): Box[MappedPhysicalCard] = { @@ -196,6 +198,8 @@ object MappedPhysicalCardProvider extends PhysicalCardProvider { .mPosted(p.date) .mAccount(mappedBankAccountPrimaryKey) // Card <-MappedLongForeignKey-> BankAccount, so need the primary key here. .mCustomerId(customerId) + .mBrand(brand) + .mCVV(HashUtil.Sha256Hash(cvv)) .saveMe() } ?~! ErrorMessages.CreateCardError } @@ -231,6 +235,12 @@ object MappedPhysicalCardProvider extends PhysicalCardProvider { } cards } + + override def getPhysicalCardByCardNumber(bankCardNumber: String, callContext:Option[CallContext]) : Box[PhysicalCardTrait] = { + MappedPhysicalCard.find( + By(MappedPhysicalCard.mBankCardNumber, bankCardNumber), + ) + } def getPhysicalCardsForBank(bank: Bank, user: User, queryParams: List[OBPQueryParam]) = { val customerId: Option[Cmp[MappedPhysicalCard, String]] = queryParams.collect { case OBPCustomerId(value) => @@ -312,6 +322,9 @@ class MappedPhysicalCard extends PhysicalCardTrait with LongKeyedMapper[MappedPh //Maybe this will be first uesd for the initialization. and then we can add more `allows` for this card. object mCardType extends MappedString(this, 255) object mCustomerId extends MappedString(this, 255) + + object mBrand extends MappedString(this, 255) + object mCVV extends MappedString(this, 255) def bankId: String = mBankId.get def bankCardNumber: String = mBankCardNumber.get @@ -353,6 +366,8 @@ class MappedPhysicalCard extends PhysicalCardTrait with LongKeyedMapper[MappedPh def cardType: String = mCardType.get def cardId: String = mCardId.get def customerId: String = mCustomerId.get + override def cvv: Option[String] = Some(mCVV.get) + override def brand: Option[String] = Some(mBrand.get) } object MappedPhysicalCard extends MappedPhysicalCard with LongKeyedMetaMapper[MappedPhysicalCard] { diff --git a/obp-api/src/main/scala/code/cards/PhisicalCardProvider.scala b/obp-api/src/main/scala/code/cards/PhisicalCardProvider.scala index 13caef785..b13c66986 100644 --- a/obp-api/src/main/scala/code/cards/PhisicalCardProvider.scala +++ b/obp-api/src/main/scala/code/cards/PhisicalCardProvider.scala @@ -41,6 +41,8 @@ trait PhysicalCardProvider { collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], customerId: String, + cvv: String, + brand: String, callContext: Option[CallContext] ): Box[MappedPhysicalCard] @@ -76,6 +78,8 @@ trait PhysicalCardProvider { def getPhysicalCardForBank(bankId: BankId, cardId: String, callContext:Option[CallContext]) : Box[PhysicalCardTrait] def deletePhysicalCardForBank(bankId: BankId, cardId: String, callContext:Option[CallContext]) : Box[Boolean] + + def getPhysicalCardByCardNumber(bankCardNumber: String, callContext:Option[CallContext]) : Box[PhysicalCardTrait] } diff --git a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala index 9a9d19d47..c10aebc46 100644 --- a/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala +++ b/obp-api/src/main/scala/code/transactionrequests/MappedTransactionRequestProvider.scala @@ -273,7 +273,8 @@ class MappedTransactionRequest extends LongKeyedMapper[MappedTransactionRequest] else None - val t_to_counterparty = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.COUNTERPARTY){ + val t_to_counterparty = if (TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.COUNTERPARTY || + TransactionRequestTypes.withName(transactionType) == TransactionRequestTypes.CARD){ val counterpartyIdList: List[String] = for { JObject(child) <- parsedDetails JField("counterparty_id", JString(counterpartyId)) <- child diff --git a/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala b/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala index 5ff700756..662dc1fab 100644 --- a/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala +++ b/obp-api/src/main/scala/code/transactionrequests/TransactionRequests.scala @@ -17,7 +17,7 @@ object TransactionRequests extends SimpleInjector { object TransactionRequestTypes extends Enumeration { type TransactionRequestTypes = Value - val SANDBOX_TAN, ACCOUNT, ACCOUNT_OTP, COUNTERPARTY, SEPA, FREE_FORM, SIMPLE, + val SANDBOX_TAN, ACCOUNT, ACCOUNT_OTP, COUNTERPARTY, SEPA, FREE_FORM, SIMPLE, CARD, TRANSFER_TO_PHONE, TRANSFER_TO_ATM, TRANSFER_TO_ACCOUNT, TRANSFER_TO_REFERENCE_ACCOUNT, //The following are BerlinGroup Standard SEPA_CREDIT_TRANSFERS, INSTANT_SEPA_CREDIT_TRANSFERS, TARGET_2_PAYMENTS, CROSS_BORDER_CREDIT_TRANSFERS, REFUND = Value diff --git a/obp-api/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala b/obp-api/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala index 5de9236da..5cd1e1768 100644 --- a/obp-api/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala +++ b/obp-api/src/test/scala/code/api/v1_3_0/PhysicalCardsTest.scala @@ -43,7 +43,7 @@ class PhysicalCardsTest extends ServerSetup with DefaultUsers with DefaultConne pinResets = Nil, collected = None, posted = None, - customerId = "" + customerId = "", ) val user1CardAtBank1 = createCard("1") diff --git a/obp-api/src/test/scala/code/api/v4_0_0/TransactionRequestsTest.scala b/obp-api/src/test/scala/code/api/v4_0_0/TransactionRequestsTest.scala index 87ebc6221..0573e22a3 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/TransactionRequestsTest.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/TransactionRequestsTest.scala @@ -1,19 +1,22 @@ package code.api.v4_0_0 -import java.util.UUID +import java.util.{Date, UUID} import code.api.ChargePolicy import code.api.Constant._ import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.createPhysicalCardJsonV500 import code.api.util.APIUtil.OAuth._ import code.api.util.APIUtil.extractErrorMessageCode -import code.api.util.ApiRole.CanCreateAnyTransactionRequest +import code.api.util.ApiRole.{CanCreateAnyTransactionRequest, CanCreateCardsForBank} import code.api.util.ErrorMessages._ import code.api.util.{APIUtil, ErrorMessages} import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJsonV140} import code.api.v2_0_0.TransactionRequestBodyJsonV200 import code.api.v2_1_0._ import code.api.v4_0_0.APIMethods400.Implementations4_0_0 +import code.api.v5_0_0.PhysicalCardJsonV500 import code.bankconnectors.Connector +import code.entitlement.Entitlement import code.fx.fx import code.model.BankAccountX import code.setup.{APIResponse, DefaultUsers} @@ -46,6 +49,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { object ApiEndpoint7 extends Tag(nameOf(Implementations4_0_0.createTransactionRequestFreeForm)) object ApiEndpoint8 extends Tag(nameOf(Implementations4_0_0.answerTransactionRequestChallenge)) object ApiEndpoint9 extends Tag(nameOf(Implementations4_0_0.createTransactionRequestSimple)) + object ApiEndpoint10 extends Tag(nameOf(Implementations4_0_0.createTransactionRequestCard)) def transactionCount(accounts: BankAccount*): Int = { accounts.foldLeft(0)((accumulator, account) => { @@ -133,6 +137,33 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { other_account_secondary_routing_address= counterpartyCounterparty.otherAccountSecondaryRoutingAddress ), bodyValue, description, sharedChargePolicy) + val customerId = createAndGetCustomerIdViaEndpoint(bankId.value, user1) + val requestWithAuthUser = (v5_0_0_Request / "management" /"banks" / bankId.value / "cards" ).POST <@ (user1) + val properCardJson = createPhysicalCardJsonV500.copy( + account_id = fromAccount.accountId.value, + issue_number = "123", + customer_id = customerId, + expires_date = org.apache.commons.lang3.time.DateUtils.addMonths(new Date(), 3) + ) + Entitlement.entitlement.vend.addEntitlement(bankId.value, resourceUser1.userId, CanCreateCardsForBank.toString) + val responseProper = makePostRequest(requestWithAuthUser, write(properCardJson)) + val cardJsonV500 = responseProper.body.extract[PhysicalCardJsonV500] + + var transactionRequestBodyCard = TransactionRequestBodyCardJsonV400( + card = CardJsonV400( + card_type = cardJsonV500.card_type, + brand = cardJsonV500.brand, + cvv = cardJsonV500.cvv, + card_number = cardJsonV500.card_number, + name_on_card = cardJsonV500.name_on_card, + expiry_year = (cardJsonV500.expires_date.getYear+1900).toString, + expiry_month = (cardJsonV500.expires_date.getMonth+1).toString + ), + CounterpartyIdJson(counterpartyCounterparty.counterpartyId), + bodyValue, + description + ) + def setAnswerTransactionRequest(challengeId: String = this.challengeId, transRequestId: String = this.transRequestId, consumerAndToken: Option[(Consumer, Token)] = user1 , challengeAnswer:String = "123") = { this.challengeId = challengeId this.transRequestId = transRequestId @@ -154,11 +185,14 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { */ var createTransReqRequest = (v4_0_0_Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / CUSTOM_OWNER_VIEW_ID / "transaction-request-types" / transactionRequestType / "transaction-requests").POST <@ (user1) - + + var createTransReqRequestCard = (v4_0_0_Request / "transaction-request-types" / "CARD" / "transaction-requests").POST <@ (user1) + def makeCreateTransReqRequest: APIResponse = makePostRequest(createTransReqRequest, write(transactionRequestBody)) def makeCreateTransReqRequestSEPA: APIResponse = makePostRequest(createTransReqRequest, write(transactionRequestBodySEPA)) def makeCreateTransReqRequestCounterparty: APIResponse = makePostRequest(createTransReqRequest, write(transactionRequestBodyCounterparty)) def makeCreateTransReqRequestSimple: APIResponse = makePostRequest(createTransReqRequest, write(transactionRequestBodySimple)) + def makeCreateTransReqRequestCard: APIResponse = makePostRequest(createTransReqRequestCard, write(transactionRequestBodyCard)) def checkAllCreateTransReqResBodyField(createTransactionRequestResponse: APIResponse, withChallenge: Boolean): Unit = { Then("we should get a 201 created code") @@ -397,9 +431,9 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { Then("we should get a 400 created code") response.code should equal(400) - + response.body.extract[ErrorMessage].message should startWith(ErrorMessages.InvalidTransactionRequestType) - + } } @@ -521,7 +555,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { Then("we need check the account amount info") helper.checkBankAccountBalance(true) } - + scenario("With challenge, No FX, test the allowed_attempts times ", ApiEndpoint1, ApiEndpoint2) { When("we prepare all the conditions for a normal success -- V400 Create Transaction Request") val helper = defaultSetup() @@ -539,7 +573,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { And("We checked all the fields of createTransactionRequestResponse body ") helper.checkAllCreateTransReqResBodyField(createTransactionRequestResponse, true) - + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") And("we prepare the parameters for it") helper.setAnswerTransactionRequest(challengeAnswer ="1234") @@ -594,7 +628,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { When("We checked all the data in database, we need check the account amount info") helper.checkBankAccountBalance(false) - + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") And("we prepare the parameters for it") helper.setAnswerTransactionRequest() @@ -602,7 +636,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { val ansReqResponse = helper.makeAnswerRequest And("We check the all the fields of getAnsReqResponse body ") helper.checkAllAnsTransReqBodyFields(ansReqResponse, true) - + Then("we need check the account amount info") helper.checkBankAccountBalance(true) } @@ -619,7 +653,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { When("we prepare all the conditions for a normal success -- V400 Create Transaction Request") val helper = defaultSetup(FREE_FORM.toString) addEntitlement(helper.bankId.value, resourceUser1.userId, CanCreateAnyTransactionRequest.toString) - + Then("we call the 'V400 Create Transaction Request' endpoint") val createTransactionRequestResponse = helper.makeCreateTransReqRequest @@ -767,7 +801,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { When("We checked all the data in database, we need check the account amount info") helper.checkBankAccountBalance(false) - + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") And("we prepare the parameters for it") helper.setAnswerTransactionRequest() @@ -775,7 +809,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { val ansReqResponse = helper.makeAnswerRequest And("We check the all the fields of getAnsReqResponse body ") helper.checkAllAnsTransReqBodyFields(ansReqResponse, true) - + Then("we need check the account amount info") helper.checkBankAccountBalance(true) } @@ -936,7 +970,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { When("We checked all the data in database, we need check the account amount info") helper.checkBankAccountBalance(false) - + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") And("we prepare the parameters for it") helper.setAnswerTransactionRequest() @@ -944,7 +978,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { val ansReqResponse = helper.makeAnswerRequest And("We check the all the fields of getAnsReqResponse body ") helper.checkAllAnsTransReqBodyFields(ansReqResponse, true) - + Then("we need check the account amount info") helper.checkBankAccountBalance(true) } @@ -1105,7 +1139,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { When("We checked all the data in database, we need check the account amount info") helper.checkBankAccountBalance(false) - + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") And("we prepare the parameters for it") helper.setAnswerTransactionRequest() @@ -1113,7 +1147,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { val ansReqResponse = helper.makeAnswerRequest And("We check the all the fields of getAnsReqResponse body ") helper.checkAllAnsTransReqBodyFields(ansReqResponse, true) - + Then("we need check the account amount info") helper.checkBankAccountBalance(true) } @@ -1152,12 +1186,12 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { user1, PostViewJsonV400(view_id = "owner", is_system = true) ) - + Then("we call the 'V400 Create Transaction Request' endpoint") val createTransactionRequestResponse = helper.makeCreateTransReqRequestCounterparty val createTransactionRequestJsonResponse = createTransactionRequestResponse.body.extract[TransactionRequestWithChargeJSON400] createTransactionRequestJsonResponse.status should equal(TransactionRequestStatus.INITIATED.toString) - + val challengeOfUser1: Option[ChallengeJsonV400] = createTransactionRequestJsonResponse.challenges.find(_.user_id == resourceUser1.userId) val challengeOfUser2: Option[ChallengeJsonV400] = createTransactionRequestJsonResponse.challenges.find(_.user_id == resourceUser2.userId) @@ -1199,7 +1233,6 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { } - feature("we can create transaction requests -- SIMPLE") { if (APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false) == false) { @@ -1354,7 +1387,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { When("We checked all the data in database, we need check the account amount info") helper.checkBankAccountBalance(false) - + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") And("we prepare the parameters for it") helper.setAnswerTransactionRequest() @@ -1362,7 +1395,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { val ansReqResponse = helper.makeAnswerRequest And("We check the all the fields of getAnsReqResponse body ") helper.checkAllAnsTransReqBodyFields(ansReqResponse, true) - + Then("we need check the account amount info") helper.checkBankAccountBalance(true) } @@ -1401,15 +1434,15 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { user1, PostViewJsonV400(view_id = "owner", is_system = true) ) - + Then("we call the 'V400 Create Transaction Request' endpoint") val createTransactionRequestResponse = helper.makeCreateTransReqRequestSimple val createTransactionRequestJsonResponse = createTransactionRequestResponse.body.extract[TransactionRequestWithChargeJSON400] createTransactionRequestJsonResponse.status should equal(TransactionRequestStatus.INITIATED.toString) - + val challengeOfUser1: Option[ChallengeJsonV400] = createTransactionRequestJsonResponse.challenges.find(_.user_id == resourceUser1.userId) val challengeOfUser2: Option[ChallengeJsonV400] = createTransactionRequestJsonResponse.challenges.find(_.user_id == resourceUser2.userId) - + Then("We checked all the fields of createTransactionRequestResponse body ") helper.checkAllCreateTransReqResBodyField(createTransactionRequestResponse, true) @@ -1433,7 +1466,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { And("we call the endpoint") val ansReqResponseUser1 = helper.makeAnswerRequest ansReqResponseUser1.body.extract[ErrorMessage].message contains extractErrorMessageCode(NextChallengePending) should be (true) - + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") And("we prepare the parameters for it") helper.setAnswerTransactionRequest( @@ -1445,7 +1478,272 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { ansReqResponseUser2.body.extract[TransactionRequestWithChargeJSON400].status should equal(TransactionRequestStatus.COMPLETED.toString) } } + + } + + feature("we can create transaction requests -- CARD") { + + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD") + setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") + if (APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false) == false) { + ignore("No challenge, No FX ", ApiEndpoint10) {} + } else { + scenario("No challenge, No FX ", ApiEndpoint10) { + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD") + setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") + + When("we prepare all the conditions for a normal success -- V400 Create Transaction Request") + val helper = defaultSetup(CARD.toString) + + Then("we call the 'V400 Create Transaction Request' endpoint") + val createTransactionRequestResponse = helper.makeCreateTransReqRequestCard + + Then("We checked all the fields of createTransact dionRequestResponse body ") + helper.checkAllCreateTransReqResBodyField(createTransactionRequestResponse, false) + + When("we need check the 'Get all Transaction Requests. - V400' to double check it in database") + val getTransReqResponse = helper.makeGetTransReqRequest + + Then("We checked all the fields of getTransReqResponse body") + helper.checkAllGetTransReqResBodyField(getTransReqResponse, false) + + When("we need to check the 'Get Transactions for Account (Full) -V400' to check the transaction info ") + val getTransResponse = helper.makeGetTransRequest + Then("We checked all the fields of getTransResponse body") + helper.checkAllGetTransResBodyField(getTransResponse, false) + + When("We checked all the data in database, we need check the account amount info") + helper.checkBankAccountBalance(true) + } + } + + if (APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false) == false) { + ignore("No challenge, With FX ", ApiEndpoint10) {} + } else { + scenario("No challenge, With FX ", ApiEndpoint10) { + + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD") + setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") + + When("we prepare all the conditions for a normal success -- V400 Create Transaction Request") + val helper = defaultSetup(CARD.toString) + + Then("we call the 'V400 Create Transaction Request' endpoint") + val fromCurrency = "AED" + val toCurrency = "INR" + val amt = "10" + helper.setCurrencyAndAmt(fromCurrency, toCurrency, amt) + And("We set the special input JSON values for 'V400 Create Transaction Request' endpoint") + helper.bodyValue = AmountOfMoneyJsonV121(fromCurrency, amt.toString()) + helper.transactionRequestBodyCard = helper.transactionRequestBodyCard.copy(value=helper.bodyValue) + + Then("we call the 'V400 Create Transaction Request' endpoint") + val createTransactionRequestResponse = helper.makeCreateTransReqRequestCard + + Then("We checked all the fields of createTransactionRequestResponse body ") + helper.checkAllCreateTransReqResBodyField(createTransactionRequestResponse, false) + + When("we need check the 'Get all Transaction Requests. - V400' to double check it in database") + val getTransReqResponse = helper.makeGetTransReqRequest + + Then("We checked all the fields of getTransReqResponse body") + helper.checkAllGetTransReqResBodyField(getTransReqResponse, false) + + When("we need to check the 'Get Transactions for Account (Full) -V400' to check the transaction info ") + val getTransResponse = helper.makeGetTransRequest + Then("We checked all the fields of getTransResponse body") + helper.checkAllGetTransResBodyField(getTransResponse, false) + + When("We checked all the data in database, we need check the account amout info") + helper.checkBankAccountBalance(true) + + } + } + + if (APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false) == false) { + ignore("With challenge, No FX ", ApiEndpoint10) {} + } else { + scenario("With challenge, No FX ", ApiEndpoint10) { + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD") + setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") + + When("we prepare all the conditions for a normal success -- V400 Create Transaction Request") + val helper = defaultSetup(CARD.toString) + And("We set the special conditions for different currencies") + val fromCurrency = "AED" + val toCurrency = "AED" + val amt = "50000.00" + helper.setCurrencyAndAmt(fromCurrency, toCurrency, amt) + And("We set the special input JSON values for 'V400 Create Transaction Request' endpoint") + helper.bodyValue = AmountOfMoneyJsonV121(fromCurrency, amt.toString()) + helper.transactionRequestBodyCard = helper.transactionRequestBodyCard.copy(value=helper.bodyValue) + + Then("we call the 'V400 Create Transaction Request' endpoint") + val createTransactionRequestResponse = helper.makeCreateTransReqRequestCard + And("We checked all the fields of createTransactionRequestResponse body ") + helper.checkAllCreateTransReqResBodyField(createTransactionRequestResponse, true) + + Then("we need check the 'Get all Transaction Requests. - V400' to double check it in database") + val getTransReqResponse = helper.makeGetTransReqRequest + And("We checked all the fields of getTransReqResponse body") + helper.checkAllGetTransReqResBodyField(getTransReqResponse, true) + + Then("we need to check the 'Get Transactions for Account (Full) -V400' to check the transaction info ") + val getTransResponse = helper.makeGetTransRequest + And("We checked all the fields of getTransResponse body") + helper.checkAllGetTransResBodyField(getTransResponse, true) + + Then("we need check the account amount info") + helper.checkBankAccountBalance(false) + + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") + And("we prepare the parameters for it") + helper.setAnswerTransactionRequest() + And("we call the endpoint") + val ansReqResponse = helper.makeAnswerRequest + And("We check the all the fields of getAnsReqResponse body ") + helper.checkAllAnsTransReqBodyFields(ansReqResponse, true) + + Then("we need check the account amount info") + helper.checkBankAccountBalance(true) + } + } + + if (APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false) == false) { + ignore("With challenge, With FX", ApiEndpoint10) {} + } else { + scenario("With challenge, With FX", ApiEndpoint10) { + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD") + setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") + + When("we prepare all the conditions for a normal success -- V400 Create Transaction Request") + val helper = defaultSetup(CARD.toString) + + And("We set the special conditions for different currencies") + val fromCurrency = "AED" + val toCurrency = "INR" + val amt = "50000.00" + helper.setCurrencyAndAmt(fromCurrency, toCurrency, amt) + + And("We set the special input JSON values for 'V400 Create Transaction Request' endpoint") + helper.bodyValue = AmountOfMoneyJsonV121(fromCurrency, amt.toString()) + helper.transactionRequestBodyCard = helper.transactionRequestBodyCard.copy(value=helper.bodyValue) + + Then("we call the 'V400 Create Transaction Request' endpoint") + val createTransactionRequestResponse = helper.makeCreateTransReqRequestCard + + Then("We checked all the fields of createTransactionRequestResponse body ") + helper.checkAllCreateTransReqResBodyField(createTransactionRequestResponse, true) + + When("we need check the 'Get all Transaction Requests. - V400' to double check it in database") + val getTransReqResponse = helper.makeGetTransReqRequest + + Then("We checked all the fields of getTransReqResponse body") + helper.checkAllGetTransReqResBodyField(getTransReqResponse, true) + + When("we need to check the 'Get Transactions for Account (Full) -V400' to check the transaction info ") + val getTransResponse = helper.makeGetTransRequest + Then("We checked all the fields of getTransResponse body") + helper.checkAllGetTransResBodyField(getTransResponse, true) + + When("We checked all the data in database, we need check the account amount info") + helper.checkBankAccountBalance(false) + + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") + And("we prepare the parameters for it") + helper.setAnswerTransactionRequest() + And("we call the endpoint") + val ansReqResponse = helper.makeAnswerRequest + And("We check the all the fields of getAnsReqResponse body ") + helper.checkAllAnsTransReqBodyFields(ansReqResponse, true) + + Then("we need check the account amount info") + helper.checkBankAccountBalance(true) + } + } + + if (APIUtil.getPropsAsBoolValue("transactionRequests_enabled", false) == false) { + ignore("With N challenges, With FX", ApiEndpoint10) {} + } else { + scenario("With N challenges, With FX", ApiEndpoint10) { + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD") + setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") + + When("we prepare all the conditions for a normal success -- V400 Create Transaction Request") + val helper = defaultSetup(CARD.toString) + + And("We set the special conditions for different currencies") + val fromCurrency = "AED" + val toCurrency = "INR" + val amt = "50000.00" + helper.setCurrencyAndAmt(fromCurrency, toCurrency, amt) + + And("We set the special input JSON values for 'V400 Create Transaction Request' endpoint") + helper.bodyValue = AmountOfMoneyJsonV121(fromCurrency, amt.toString()) + helper.transactionRequestBodyCard = helper.transactionRequestBodyCard.copy(value=helper.bodyValue) + + createAccountAttributeViaEndpoint( + helper.bankId.value, + helper.accountId1.value, + "REQUIRED_CHALLENGE_ANSWERS", + "2", + "INTEGER", + Some("LKJL98769F") + ) + + val grantedView = grantUserAccessToViewViaEndpoint( + helper.bankId.value, + helper.accountId1.value, + resourceUser2.userId, + user1, + PostViewJsonV400(view_id = "owner", is_system = true) + ) + + Then("we call the 'V400 Create Transaction Request' endpoint") + val createTransactionRequestResponse = helper.makeCreateTransReqRequestCard + val createTransactionRequestJsonResponse = createTransactionRequestResponse.body.extract[TransactionRequestWithChargeJSON400] + createTransactionRequestJsonResponse.status should equal(TransactionRequestStatus.INITIATED.toString) + + val challengeOfUser1: Option[ChallengeJsonV400] = createTransactionRequestJsonResponse.challenges.find(_.user_id == resourceUser1.userId) + val challengeOfUser2: Option[ChallengeJsonV400] = createTransactionRequestJsonResponse.challenges.find(_.user_id == resourceUser2.userId) + + Then("We checked all the fields of createTransactionRequestResponse body ") + helper.checkAllCreateTransReqResBodyField(createTransactionRequestResponse, true) + + When("we need check the 'Get all Transaction Requests. - V400' to double check it in database") + val getTransReqResponse = helper.makeGetTransReqRequest + + Then("We checked all the fields of getTransReqResponse body") + helper.checkAllGetTransReqResBodyField(getTransReqResponse, true) + + When("we need to check the 'Get Transactions for Account (Full) -V400' to check the transaction info ") + val getTransResponse = helper.makeGetTransRequest + Then("We checked all the fields of getTransResponse body") + helper.checkAllGetTransResBodyField(getTransResponse, true) + + When("We checked all the data in database, we need check the account amount info") + helper.checkBankAccountBalance(false) + + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") + And("we prepare the parameters for it") + helper.setAnswerTransactionRequest(challengeId = challengeOfUser1.map(_.id).getOrElse("")) + And("we call the endpoint") + val ansReqResponseUser1 = helper.makeAnswerRequest + ansReqResponseUser1.body.extract[ErrorMessage].message contains extractErrorMessageCode(NextChallengePending) should be (true) + + Then("We call 'Answer Transaction Request Challenge - V400' to finish the request") + And("we prepare the parameters for it") + helper.setAnswerTransactionRequest( + challengeId = challengeOfUser2.map(_.id).getOrElse(""), + consumerAndToken = user2 + ) + And("we call the endpoint") + val ansReqResponseUser2 = helper.makeAnswerRequest + ansReqResponseUser2.body.extract[TransactionRequestWithChargeJSON400].status should equal(TransactionRequestStatus.COMPLETED.toString) + } + } + } feature(s"test $ApiEndpoint3 version $VersionOfApi - Unauthorized access") { @@ -1453,11 +1751,11 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { lazy val bankId = testBankId1.value lazy val accountId = testAccountId1.value lazy val view = "owner" - + scenario("We will call the endpoint WITHOUT user credentials", ApiEndpoint1, VersionOfApi) { val transactionRequestId = randomTransactionRequestViaEndpoint(bankId, accountId, view, user1).id - + When("We make a request v4.0.0") val request400 = (v4_0_0_Request / "banks" / bankId / "accounts"/ accountId / view / "transaction-requests" / transactionRequestId).GET val response400 = makeGetRequest(request400) @@ -1467,9 +1765,9 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { } scenario("We will call the endpoint WITH user credentials", ApiEndpoint1, VersionOfApi) { - + val transactionRequestId = randomTransactionRequestViaEndpoint(bankId, accountId, view, user1).id - + When("We make a request v4.0.0") val request400 = (v4_0_0_Request / "banks" / bankId / "accounts"/ accountId / view / "transaction-requests" / transactionRequestId).GET <@ (user1) val response400 = makeGetRequest(request400) @@ -1477,7 +1775,7 @@ class TransactionRequestsTest extends V400ServerSetup with DefaultUsers { response400.code should equal(200) response400.body.extract[TransactionRequestWithChargeJSON210].id should equal(transactionRequestId) } - + } } diff --git a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala index 9baf55342..ac6060259 100644 --- a/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala +++ b/obp-api/src/test/scala/code/api/v4_0_0/V400ServerSetup.scala @@ -36,6 +36,7 @@ import scala.util.Random.nextInt trait V400ServerSetup extends ServerSetupWithTestData with DefaultUsers { def v4_0_0_Request: Req = baseRequest / "obp" / "v4.0.0" + def v5_0_0_Request: Req = baseRequest / "obp" / "v5.0.0" def dynamicEndpoint_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-endpoint`.toString def dynamicEntity_Request: Req = baseRequest / "obp" / ApiShortVersions.`dynamic-entity`.toString diff --git a/obp-api/src/test/scala/code/api/v5_0_0/CardTest.scala b/obp-api/src/test/scala/code/api/v5_0_0/CardTest.scala new file mode 100644 index 000000000..948bb22f6 --- /dev/null +++ b/obp-api/src/test/scala/code/api/v5_0_0/CardTest.scala @@ -0,0 +1,158 @@ +package code.api.v5_0_0 + +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON +import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON.{createPhysicalCardJsonV500} +import code.api.util.ApiRole +import code.api.util.APIUtil.OAuth._ +import code.api.util.ApiRole.CanCreateCustomer +import code.api.util.ErrorMessages._ +import code.api.v1_3_0.ReplacementJSON +import code.api.v3_1_0.CustomerJsonV310 +import code.api.v5_0_0.APIMethods500.Implementations5_0_0 +import code.entitlement.Entitlement +import code.setup.DefaultUsers +import com.github.dwickern.macros.NameOf.nameOf +import com.openbankproject.commons.model.{CardAction, CardReplacementReason} +import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write +import org.scalatest.Tag + +import java.util.Date + +class CardTest extends V500ServerSetupAsync with DefaultUsers { + + object VersionOfApi extends Tag(ApiVersion.v5_0_0.toString) + object ApiEndpointAddCardForBank extends Tag(nameOf(Implementations5_0_0.addCardForBank)) + + + + feature("test Card APIs") { + scenario("We will create Card with many error cases", + ApiEndpointAddCardForBank, + VersionOfApi + ) { + Given("The test bank and test account") + val testBank = testBankId1 + val testAccount = testAccountId1 + val dummyCard = createPhysicalCardJsonV500 + + And("We need to prepare the Customer Info") + + Then("We prepare the Customer data") + val request310 = (v5_0_0_Request / "banks" / testBankId1.value / "customers").POST <@(user1) + val postCustomerJson = SwaggerDefinitionsJSON.postCustomerJsonV310 + Entitlement.entitlement.vend.addEntitlement(testBank.value, resourceUser1.userId, CanCreateCustomer.toString) + val responseCustomer310 = makePostRequest(request310, write(postCustomerJson)) + val customerId = responseCustomer310.body.extract[CustomerJsonV310].customer_id + + val properCardJson = dummyCard.copy(account_id = testAccount.value, issue_number = "123", customer_id = customerId) + + val requestAnonymous = (v5_0_0_Request / "management"/"banks" / testBank.value / "cards" ).POST + val requestWithAuthUser = (v5_0_0_Request / "management" /"banks" / testBank.value / "cards" ).POST <@ (user1) + + Then(s"We test with anonymous user.") + val responseAnonymous = makePostRequest(requestAnonymous, write(properCardJson)) + And(s"We should get 401 and get the authentication error") + responseAnonymous.code should equal(401) + responseAnonymous.body.toString contains(s"$UserNotLoggedIn") should be (true) + + Then(s"We call the authentication user, but without proper role: ${ApiRole.canCreateCardsForBank}") + val responseUserButNoRole = makePostRequest(requestWithAuthUser, write(properCardJson)) + And(s"We should get 403 and the error message missing can ${ApiRole.canCreateCardsForBank} role") + responseUserButNoRole.code should equal(403) + responseUserButNoRole.body.toString contains(s"${ApiRole.canCreateCardsForBank}") should be (true) + + Then(s"We grant the user ${ApiRole.canCreateCardsForBank} role, but with the wrong AccountId") + Entitlement.entitlement.vend.addEntitlement(testBankId1.value, resourceUser1.userId, ApiRole.canCreateCardsForBank.toString) + val wrongAccountCardJson = dummyCard + val responseHasRoleWrongAccountId = makePostRequest(requestWithAuthUser, write(wrongAccountCardJson)) + And(s"We should get 404 and get the error message: $BankAccountNotFound.") + responseHasRoleWrongAccountId.code should equal(404) + responseHasRoleWrongAccountId.body.toString contains(s"$BankAccountNotFound") + + Then(s"We call the authentication user, but totally wrong Json format.") + val wrongPostJsonFormat = testBankId1 + val responseWrongJsonFormat = makePostRequest(requestWithAuthUser, write(wrongPostJsonFormat)) + And(s"We should get 400 and get the error message: $InvalidJsonFormat.") + responseWrongJsonFormat.code should equal(400) + responseWrongJsonFormat.body.toString contains(s"$InvalidJsonFormat") + + Then(s"We call the authentication user, but wrong card.allows value") + val withWrongVlaueForAllows = properCardJson.copy(allows = List("123")) + val responseWithWrongVlaueForAllows = makePostRequest(requestWithAuthUser, write(withWrongVlaueForAllows)) + And(s"We should get 400 and get the error message") + responseWithWrongVlaueForAllows.code should equal(400) + responseWithWrongVlaueForAllows.body.toString contains(AllowedValuesAre++ CardAction.availableValues.mkString(", ")) + + Then(s"We call the authentication user, but wrong card.replacement value") + val wrongCardReplacementReasonJson = dummyCard.copy(replacement = Some(ReplacementJSON(new Date(),"Wrong"))) // The replacement must be Enum of `CardReplacementReason` + val responseWrongCardReplacementReasonJson = makePostRequest(requestWithAuthUser, write(wrongCardReplacementReasonJson)) + And(s"We should get 400 and get the error message") + responseWrongCardReplacementReasonJson.code should equal(400) + responseWrongCardReplacementReasonJson.body.toString contains(AllowedValuesAre + CardReplacementReason.availableValues.mkString(", ")) + + Then(s"We call the authentication user, but too long issue number.") + val properCardJsonTooLongIssueNumber = dummyCard.copy(account_id = testAccount.value, issue_number = "01234567891") + val responseUserWrongIssueNumber = makePostRequest(requestWithAuthUser, write(properCardJsonTooLongIssueNumber)) + And(s"We should get 400 and the error message missing can ${ApiRole.canCreateCardsForBank} role") + responseUserWrongIssueNumber.code should equal(400) + responseUserWrongIssueNumber.body.toString contains(s"${maximumLimitExceeded.replace("10000", "10")}") should be (true) + + Then(s"We grant the user ${ApiRole.canCreateCardsForBank} role, but wrong customerId") + val wrongCustomerCardJson = properCardJson.copy(customer_id = "wrongId") + val responsewrongCustomerCardJson = makePostRequest(requestWithAuthUser, write(wrongCustomerCardJson)) + And(s"We should get 400 and get the error message: $CustomerNotFoundByCustomerId.") + responsewrongCustomerCardJson.code should equal(404) + responsewrongCustomerCardJson.body.toString contains(s"$CustomerNotFoundByCustomerId") should be (true) + + Then(s"We test the success case, prepare all stuff.") + val responseProper = makePostRequest(requestWithAuthUser, write(properCardJson)) + And("We should get 400 and get the error message: Not Found the BankAccount.") + responseProper.code should equal(201) + val cardJsonV500 = responseProper.body.extract[PhysicalCardJsonV500] + cardJsonV500.card_number should be (properCardJson.card_number) + cardJsonV500.card_type should be (properCardJson.card_type) + cardJsonV500.name_on_card should be (properCardJson.name_on_card) + cardJsonV500.issue_number should be (properCardJson.issue_number) + cardJsonV500.serial_number should be (properCardJson.serial_number ) + cardJsonV500.valid_from_date should be (properCardJson.valid_from_date ) + cardJsonV500.expires_date should be (properCardJson.expires_date ) + cardJsonV500.enabled should be (properCardJson.enabled ) + cardJsonV500.cancelled should be (false) + cardJsonV500.on_hot_list should be (false) + cardJsonV500.technology should be (properCardJson.technology ) + cardJsonV500.networks should be (properCardJson.networks ) + cardJsonV500.allows should be (properCardJson.allows ) + cardJsonV500.account.id should be (properCardJson.account_id ) + cardJsonV500.replacement should be { + properCardJson.replacement match { + case Some(x) => x + case None => null + } + } + cardJsonV500.pin_reset.toString() should be(properCardJson.pin_reset.toString()) + cardJsonV500.collected should be { + properCardJson.collected match { + case Some(x) => x + case None => null + } + } + cardJsonV500.posted should be { + properCardJson.posted match { + case Some(x) => x + case None => null + } + } + + cardJsonV500.cvv.length equals (3) should be(true) + cardJsonV500.brand equals ("Visa") should be(true) + + Then(s"We create the card with same bankId, cardNumber and issueNumber") + val responseDeplicated = makePostRequest(requestWithAuthUser, write(properCardJson)) + And("We should get 400 and get the error message: the card already existing .") + responseDeplicated.code should equal(400) + responseDeplicated.body.toString contains(s"$CardAlreadyExists") should be (true) + } + } + +} diff --git a/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data b/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data index a6ced7296..9a70a17d0 100644 Binary files a/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data and b/obp-api/src/test/scala/code/connector/RestConnector_vMar2019_frozen_meta_data differ diff --git a/obp-api/src/test/scala/code/setup/ServerSetup.scala b/obp-api/src/test/scala/code/setup/ServerSetup.scala index 9f29179d1..6e35e6904 100644 --- a/obp-api/src/test/scala/code/setup/ServerSetup.scala +++ b/obp-api/src/test/scala/code/setup/ServerSetup.scala @@ -51,6 +51,8 @@ trait ServerSetup extends FeatureSpec with SendServerRequests setPropsValues("dauth.host" -> "127.0.0.1") setPropsValues("jwt.token_secret"->"your-at-least-256-bit-secret-token") setPropsValues("jwt.public_key_rsa" -> "src/test/resources/cert/public_dauth.pem") + setPropsValues("transactionRequests_supported_types" -> "SEPA,SANDBOX_TAN,FREE_FORM,COUNTERPARTY,ACCOUNT,ACCOUNT_OTP,SIMPLE,CARD") + setPropsValues("CARD_OTP_INSTRUCTION_TRANSPORT" -> "DUMMY") val server = TestServer def baseRequest = host(server.host, server.port) diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala b/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala index 34fbb9a5b..b81c09d28 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/dto/JsonsTransfer.scala @@ -691,7 +691,8 @@ case class OutBoundUpdatePhysicalCard(outboundAdapterCallContext: OutboundAdapte pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], - customerId: String) extends TopicTrait + customerId: String + ) extends TopicTrait case class InBoundUpdatePhysicalCard(inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: PhysicalCard) extends InBoundTrait[PhysicalCard] case class OutBoundDeletePhysicalCardForBank(outboundAdapterCallContext: OutboundAdapterCallContext, @@ -734,7 +735,9 @@ case class OutBoundCreatePhysicalCard(outboundAdapterCallContext: OutboundAdapte pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], - customerId: String + customerId: String, + cvv: String, + brand: String ) extends TopicTrait case class InBoundCreatePhysicalCard(inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: PhysicalCard) extends InBoundTrait[PhysicalCard] @@ -947,7 +950,9 @@ case class OutBoundCreatePhysicalCardLegacy (outboundAdapterCallContext: Outboun pinResets: List[PinResetInfo], collected: Option[CardCollectionInfo], posted: Option[CardPostedInfo], - customerId: String) extends TopicTrait + customerId: String, + cvv: String = "", + brand: String = "") extends TopicTrait case class InBoundCreatePhysicalCardLegacy (inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: PhysicalCard) extends InBoundTrait[PhysicalCard] diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/model/PhysicalCardModel.scala b/obp-commons/src/main/scala/com/openbankproject/commons/model/PhysicalCardModel.scala index f4f2922c6..6a9e89106 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/model/PhysicalCardModel.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/model/PhysicalCardModel.scala @@ -61,6 +61,8 @@ trait PhysicalCardTrait { def collected: Option[CardCollectionInfo] def posted: Option[CardPostedInfo] def customerId: String + def cvv: Option[String] = None //added from V500 + def brand: Option[String] = None //added from V500 } case class PhysicalCard ( @@ -84,7 +86,9 @@ case class PhysicalCard ( val pinResets : List[PinResetInfo], val collected : Option[CardCollectionInfo], val posted : Option[CardPostedInfo], - val customerId: String + val customerId: String, + override val cvv: Option[String] = None, + override val brand: Option[String] = None ) extends PhysicalCardTrait